From bf4a2f9c3967cfdc7e13635e90c36124b8996249 Mon Sep 17 00:00:00 2001
From: ddCat <2695092045@qq.com>
Date: Sat, 15 Aug 2026 02:31:42 +0800
Subject: [PATCH] =?UTF-8?q?feat(canvas):=20=E7=94=BB=E5=B8=83=E5=88=9B?=
=?UTF-8?q?=E4=BD=9C=E5=AF=B9=E8=AF=9D=20-=20=E7=BB=9F=E4=B8=80=E6=B5=81?=
=?UTF-8?q?=E5=BC=8F=20Markdown=20=E5=B1=95=E7=A4=BA=E4=B8=8E=E4=BA=A4?=
=?UTF-8?q?=E4=BA=92=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/bun.lock | 112 ++++++-
web/package.json | 2 +
web/src/components/ai/ai-message-markdown.tsx | 50 +++
.../canvas/canvas-agent-chat-ui.tsx | 5 +-
.../canvas/canvas-assistant-panel.tsx | 2 +-
.../canvas/canvas-local-agent-panel.tsx | 2 +-
.../canvas/canvas-node-generation.ts | 47 ++-
.../canvas/canvas-node-prompt-panel.tsx | 13 +
.../canvas-resource-mention-textarea.tsx | 286 ++++++++++++++----
web/src/components/model-picker.tsx | 27 +-
.../lib/canvas/canvas-resource-references.ts | 26 ++
web/src/pages/canvas/project.tsx | 2 +
.../canvas/use-canvas-generation-executor.ts | 7 +-
.../canvas/use-canvas-generation-retry.ts | 8 +-
web/src/pages/create/index.tsx | 4 +-
web/src/styles/globals.css | 254 ++++++++++++++--
16 files changed, 730 insertions(+), 117 deletions(-)
create mode 100644 web/src/components/ai/ai-message-markdown.tsx
diff --git a/web/bun.lock b/web/bun.lock
index 65abdf994..b350724aa 100644
--- a/web/bun.lock
+++ b/web/bun.lock
@@ -14,6 +14,7 @@
"@mediapipe/tasks-vision": "^1.0.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
+ "@streamdown/code": "^1.1.1",
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-virtual": "^3.14.8",
"@tiptap/extension-character-count": "^3.28.0",
@@ -46,6 +47,7 @@
"react-router": "^8.2.0",
"recharts": "^3.9.2",
"shadcn": "^4.13.1",
+ "streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.3",
"three": "^0.185.1",
@@ -614,12 +616,28 @@
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
+ "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
+
+ "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
+
+ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
+
+ "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
+
+ "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
+
+ "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
+
+ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
+
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
+ "@streamdown/code": ["@streamdown/code@1.1.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg=="],
+
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
@@ -1186,6 +1204,8 @@
"enhanced-resolve": ["enhanced-resolve@5.24.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw=="],
+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
"env-paths": ["env-paths@2.2.1", "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"error-ex": ["error-ex@1.3.4", "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
@@ -1206,6 +1226,8 @@
"escape-html": ["escape-html@1.0.3", "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
"esprima": ["esprima@4.0.1", "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
@@ -1302,16 +1324,32 @@
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
+ "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
+
+ "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
+
+ "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
+
+ "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
+
+ "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
+
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
+ "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
+
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
+
"hls.js": ["hls.js@1.6.16", "", {}, "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA=="],
"hono": ["hono@4.12.30", "", {}, "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
+ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
+
"http-errors": ["http-errors@2.0.1", "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
@@ -1502,12 +1540,28 @@
"mammoth": ["mammoth@1.12.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w=="],
- "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
+ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
+
+ "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
+
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
+ "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
+
+ "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
+
+ "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
+
+ "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
+
+ "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
+
+ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
+
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
@@ -1542,6 +1596,20 @@
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+ "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
+
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+ "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
+
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
+
+ "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
+
+ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
+
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
@@ -1626,6 +1694,10 @@
"onetime": ["onetime@5.1.2", "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
+ "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
+
+ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
+
"open": ["open@11.0.0", "https://registry.npmmirror.com/open/-/open-11.0.0.tgz", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"open-color": ["open-color@1.9.1", "", {}, "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="],
@@ -1648,6 +1720,8 @@
"parse-ms": ["parse-ms@4.0.0", "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
+ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
"parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-browserify": ["path-browserify@1.0.1", "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
@@ -1782,10 +1856,28 @@
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
+ "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
+
+ "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
+
+ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
+
+ "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="],
+
+ "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
+
+ "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
+
+ "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
+
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
+ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
+
+ "remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="],
+
"require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
@@ -1840,6 +1932,8 @@
"shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+ "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
+
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
"side-channel-list": ["side-channel-list@1.0.1", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
@@ -1870,6 +1964,8 @@
"stdin-discarder": ["stdin-discarder@0.2.2", "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
+ "streamdown": ["streamdown@2.5.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "mermaid": "^11.12.2", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.3.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA=="],
+
"string-convert": ["string-convert@0.2.1", "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
"string-width": ["string-width@7.2.0", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -1994,6 +2090,8 @@
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
+
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
@@ -2014,6 +2112,8 @@
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+
"webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="],
"webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="],
@@ -2664,6 +2764,12 @@
"get-intrinsic/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
+ "hast-util-from-parse5/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "hast-util-raw/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "hast-util-to-html/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
"hast-util-to-jsx-runtime/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"is-core-module/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
@@ -2688,6 +2794,8 @@
"mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="],
+ "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
+
"mermaid/roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
@@ -2770,6 +2878,8 @@
"vfile/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+ "vfile-location/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
"vfile-message/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
diff --git a/web/package.json b/web/package.json
index c71039d1f..e74514128 100644
--- a/web/package.json
+++ b/web/package.json
@@ -22,6 +22,7 @@
"@mediapipe/tasks-vision": "^1.0.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
+ "@streamdown/code": "^1.1.1",
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-virtual": "^3.14.8",
"@tiptap/extension-character-count": "^3.28.0",
@@ -54,6 +55,7 @@
"react-router": "^8.2.0",
"recharts": "^3.9.2",
"shadcn": "^4.13.1",
+ "streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.3",
"three": "^0.185.1",
diff --git a/web/src/components/ai/ai-message-markdown.tsx b/web/src/components/ai/ai-message-markdown.tsx
new file mode 100644
index 000000000..39b9a4f77
--- /dev/null
+++ b/web/src/components/ai/ai-message-markdown.tsx
@@ -0,0 +1,50 @@
+import { Streamdown, type Components } from "streamdown";
+import { code as streamdownCode } from "@streamdown/code";
+
+import "streamdown/styles.css";
+
+type AIMessageMarkdownProps = {
+ children: string;
+ isStreaming?: boolean;
+ className?: string;
+};
+
+const components: Components = {
+ h1: ({ children, ...props }) =>
{children}
,
+ h2: ({ children, ...props }) => {children}
,
+ h3: ({ children, ...props }) => {children}
,
+ h4: ({ children, ...props }) => {children}
,
+ p: ({ children, ...props }) => {children}
,
+ ul: ({ children, ...props }) => ,
+ ol: ({ children, ...props }) => {children}
,
+ li: ({ children, ...props }) => {children},
+ blockquote: ({ children, ...props }) => {children}
,
+ pre: ({ children, ...props }) => {children},
+ code: ({ children, className, ...props }) => {children},
+ a: ({ children, ...props }) => {children},
+ hr: (props) =>
,
+ table: ({ children, ...props }) => ,
+ th: ({ children, ...props }) => {children} | ,
+ td: ({ children, ...props }) => {children} | ,
+ input: ({ ...props }) => ,
+};
+
+export function AIMessageMarkdown({ children, isStreaming = false, className = "" }: AIMessageMarkdownProps) {
+ if (!children.trim()) return null;
+ return (
+
+ {children}
+
+ );
+}
diff --git a/web/src/components/canvas/canvas-agent-chat-ui.tsx b/web/src/components/canvas/canvas-agent-chat-ui.tsx
index 212c77cf3..6b17601a8 100644
--- a/web/src/components/canvas/canvas-agent-chat-ui.tsx
+++ b/web/src/components/canvas/canvas-agent-chat-ui.tsx
@@ -5,6 +5,7 @@ import { ArrowUp, CheckCircle2, CircleAlert, ImagePlus, LoaderCircle, UserRound,
import { canvasThemes } from "@/lib/canvas-theme";
import type { CanvasAgentOperationImpact } from "@/lib/canvas/canvas-agent-ops";
import type { LocalUser } from "@/stores/use-user-store";
+import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
export type CanvasAgentChatAttachment = { id: string; name: string; url: string };
export type CanvasAgentMode = "online" | "local";
@@ -20,7 +21,7 @@ export type CanvasAgentChatMessage = {
const WORKING_TEXT = "正在推演...";
-export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveTool }: { item: CanvasAgentChatMessage; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; user: LocalUser | null; onRejectTool?: (id: string) => void; onApproveTool?: (id: string) => void }) {
+export function AgentChatMessage({ item, theme, user, isStreaming = false, onRejectTool, onApproveTool }: { item: CanvasAgentChatMessage; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; user: LocalUser | null; isStreaming?: boolean; onRejectTool?: (id: string) => void; onApproveTool?: (id: string) => void }) {
const isUser = item.role === "user";
const isSystem = item.role === "system";
const color = item.role === "error" ? "#dc2626" : item.role === "tool" ? "#2563eb" : theme.node.text;
@@ -47,7 +48,7 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
{!isUser ?
: null}
-
{item.text}
+ {item.role === "assistant" ?
{item.text} :
{item.text}
}
{item.attachments?.length ?
: null}
{item.meta ?
{item.meta}
: null}
diff --git a/web/src/components/canvas/canvas-assistant-panel.tsx b/web/src/components/canvas/canvas-assistant-panel.tsx
index 11530fa7e..128f6bd56 100644
--- a/web/src/components/canvas/canvas-assistant-panel.tsx
+++ b/web/src/components/canvas/canvas-assistant-panel.tsx
@@ -743,7 +743,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project
<>
{messages.map((message) => (
-
+
{message.references?.length ?
: null}
))}
diff --git a/web/src/components/canvas/canvas-local-agent-panel.tsx b/web/src/components/canvas/canvas-local-agent-panel.tsx
index 84e36b1da..1fb843825 100644
--- a/web/src/components/canvas/canvas-local-agent-panel.tsx
+++ b/web/src/components/canvas/canvas-local-agent-panel.tsx
@@ -547,7 +547,7 @@ export const CanvasLocalAgentPanel = memo(function CanvasLocalAgentPanel({ snaps
{!messages.length && !pendingTool && !waiting ?
{ setAgentState({ prompt: text }); void sendPrompt(text); }} /> : null}
{messages.map((item) => (
-
+
))}
{pendingTool ? : null}
{waiting && !pendingTool ? : null}
diff --git a/web/src/components/canvas/canvas-node-generation.ts b/web/src/components/canvas/canvas-node-generation.ts
index 62227b24c..0c4abe418 100644
--- a/web/src/components/canvas/canvas-node-generation.ts
+++ b/web/src/components/canvas/canvas-node-generation.ts
@@ -9,6 +9,7 @@ import { resolveCanvasDrawingReference } from "@/lib/canvas/canvas-drawing-refer
import { compileCharacterReferencePrompt } from "@/lib/canvas/canvas-character-reference";
import { nodeReferenceImage } from "@/lib/canvas/canvas-project-generation";
import type { ModelReferenceLimits } from "@/lib/model-selection";
+import type { Asset } from "@/stores/use-asset-store";
export type CharacterGenerationReference = {
nodeId: string;
@@ -56,26 +57,27 @@ export type NodeGenerationInput = {
character?: CharacterGenerationReference;
};
-export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[], prompt: string, promptOnly = false): NodeGenerationContext {
- const inputs = buildNodeGenerationInputs(nodeId, nodes, connections);
+export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[], prompt: string, assets: Asset[], promptOnly = false): NodeGenerationContext {
+ const connectedInputs = buildNodeGenerationInputs(nodeId, nodes, connections);
+ const inputs = [...connectedInputs, ...buildAssetGenerationInputs(assets)];
const sourceNode = nodes.find((node) => node.id === nodeId);
const storyboardInputs = getConnectedStoryboardRows(nodeId, nodes, connections);
- const hasExplicitNodeMention = /@\[node:[^\]]+\]/.test(normalizeLegacyNodeMentions(prompt, inputs));
- if ((sourceNode?.type === CanvasNodeType.Config && Boolean(sourceNode.metadata?.composerContent?.trim())) || hasExplicitNodeMention) {
+ const hasExplicitResourceMention = /@\[(?:node|asset):[^\]]+\]/.test(normalizeLegacyNodeMentions(prompt, inputs));
+ if ((sourceNode?.type === CanvasNodeType.Config && Boolean(sourceNode.metadata?.composerContent?.trim())) || hasExplicitResourceMention) {
return buildComposerGenerationContext(inputs, prompt, [sourceNode?.metadata?.videoStartFrameNodeId, sourceNode?.metadata?.videoEndFrameNodeId].filter((id): id is string => Boolean(id)), promptOnly);
}
const isStoryboardMedia = sourceNode?.type === CanvasNodeType.Image || sourceNode?.type === CanvasNodeType.Video;
const basePrompt = isStoryboardMedia && storyboardInputs.length ? removeTrailingInputBlocks(prompt, storyboardInputs) : prompt;
- const textInputs = inputs.filter((input) => input.type === "text");
- const characterReferences = inputs.map((input) => input.character).filter((item): item is CharacterGenerationReference => Boolean(item));
+ const textInputs = connectedInputs.filter((input) => input.type === "text");
+ const characterReferences = connectedInputs.map((input) => input.character).filter((item): item is CharacterGenerationReference => Boolean(item));
const upstreamText = textInputs
.map((input) => input.text)
.filter(Boolean)
.join("\n\n");
- const referenceImages = inputs.map((input) => input.image).filter((image): image is ReferenceImage => Boolean(image));
- const referenceVideos = inputs.map((input) => input.video).filter((video): video is ReferenceVideo => Boolean(video));
- const referenceAudios = inputs.map((input) => input.audio).filter((audio): audio is ReferenceAudio => Boolean(audio));
+ const referenceImages = connectedInputs.map((input) => input.image).filter((image): image is ReferenceImage => Boolean(image));
+ const referenceVideos = connectedInputs.map((input) => input.video).filter((video): video is ReferenceVideo => Boolean(video));
+ const referenceAudios = connectedInputs.map((input) => input.audio).filter((audio): audio is ReferenceAudio => Boolean(audio));
return {
prompt: promptOnly ? prompt : upstreamText ? `${basePrompt}\n\n${upstreamText}` : basePrompt,
@@ -112,7 +114,8 @@ function removeTrailingInputBlocks(prompt: string, inputs: NodeGenerationInput[]
function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: string, videoFrameNodeIds: string[] = [], promptOnly = false): NodeGenerationContext {
const normalizedPrompt = normalizeLegacyNodeMentions(prompt, inputs);
- const inputByNodeId = new Map(inputs.map((input) => [input.nodeId, input]));
+ const inputByToken = new Map(inputs.map((input) => [generationInputToken(input), input]));
+ const nodeInputById = new Map(inputs.filter((input) => !input.nodeId.startsWith("asset:")).map((input) => [input.nodeId, input]));
const selectedInputs: NodeGenerationInput[] = [];
const labelByNodeId = new Map();
const textBlocks: string[] = [];
@@ -121,11 +124,11 @@ function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: s
let lastIndex = 0;
let nextPrompt = "";
- for (const match of normalizedPrompt.matchAll(/@\[node:([^\]]+)\]/g)) {
+ for (const match of normalizedPrompt.matchAll(/@\[(node|asset):([^\]]+)\]/g)) {
if (match.index === undefined) continue;
hasToken = true;
nextPrompt += normalizedPrompt.slice(lastIndex, match.index);
- const input = inputByNodeId.get(match[1]);
+ const input = inputByToken.get(`${match[1]}:${match[2]}`);
if (input) {
let label = labelByNodeId.get(input.nodeId);
if (!label) {
@@ -145,7 +148,7 @@ function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: s
// 首尾帧是结构化生成参数,不受提示词中的 @ 引用筛选影响。
const selectedNodeIds = new Set(selectedInputs.map((input) => input.nodeId));
videoFrameNodeIds.forEach((nodeId) => {
- const input = inputByNodeId.get(nodeId);
+ const input = nodeInputById.get(nodeId);
if (!input?.image || selectedNodeIds.has(nodeId)) return;
selectedInputs.push(input);
selectedNodeIds.add(nodeId);
@@ -189,7 +192,7 @@ function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: s
// 旧画布保存的是 @角色1 等显示标签;生成时升级为稳定节点 Token,避免标题或排序变化后引用错位。
function normalizeLegacyNodeMentions(prompt: string, inputs: NodeGenerationInput[]) {
const counts = { image: 0, drawing: 0, video: 0, audio: 0, text: 0, character: 0 };
- const labels = inputs.map((input) => {
+ const labels = inputs.filter((input) => !input.nodeId.startsWith("asset:")).map((input) => {
const kind = input.sourceKind === "drawing" ? "drawing" : input.type;
return { label: generationLabel(kind, counts[kind]++), nodeId: input.nodeId };
}).sort((a, b) => b.label.length - a.label.length);
@@ -236,6 +239,22 @@ export function buildNodeGenerationInputs(nodeId: string, nodes: CanvasNodeData[
});
}
+function buildAssetGenerationInputs(assets: Asset[]): NodeGenerationInput[] {
+ return assets.flatMap((asset): NodeGenerationInput[] => {
+ const nodeId = `asset:${asset.id}`;
+ if (asset.kind === "text") return [{ nodeId, type: "text", title: asset.title, text: asset.data.content }];
+ if (asset.kind === "image") return [{ nodeId, type: "image", title: asset.title, image: { id: asset.id, name: asset.title, type: asset.data.mimeType, dataUrl: asset.data.dataUrl, storageKey: asset.data.storageKey, bytes: asset.data.bytes, width: asset.data.width, height: asset.data.height } }];
+ if (asset.kind === "video") return [{ nodeId, type: "video", title: asset.title, video: { id: asset.id, name: asset.title, type: asset.data.mimeType, url: asset.data.url, storageKey: asset.data.storageKey, bytes: asset.data.bytes, width: asset.data.width, height: asset.data.height, durationMs: asset.data.durationMs } }];
+ if (asset.kind === "audio") return [{ nodeId, type: "audio", title: asset.title, audio: { id: asset.id, name: asset.title, type: asset.data.mimeType, url: asset.data.url, storageKey: asset.data.storageKey, bytes: asset.data.bytes, durationMs: asset.data.durationMs } }];
+ if (asset.kind === "entity" && asset.category === "character") return [{ nodeId, type: "character", title: asset.title, character: { nodeId, assetId: asset.id, requestedVersionId: asset.primaryVersionId } }];
+ return [];
+ });
+}
+
+function generationInputToken(input: NodeGenerationInput) {
+ return input.nodeId.startsWith("asset:") ? input.nodeId : `node:${input.nodeId}`;
+}
+
function getConnectedStoryboardRows(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]): NodeGenerationInput[] {
const targetNodeIds = new Set([nodeId]);
connections.forEach((connection) => {
diff --git a/web/src/components/canvas/canvas-node-prompt-panel.tsx b/web/src/components/canvas/canvas-node-prompt-panel.tsx
index 145b23002..7cc448e3e 100644
--- a/web/src/components/canvas/canvas-node-prompt-panel.tsx
+++ b/web/src/components/canvas/canvas-node-prompt-panel.tsx
@@ -244,6 +244,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
showSelectedPrice={false}
variant="creation"
+ showConfiguredModelName
/>
@@ -284,6 +285,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
{
const drag = dragRef.current;
if (!drag || drag.pointerId !== event.pointerId) return;
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
+ dragRef.current = null;
+ return;
+ }
+ if ((event.buttons & 1) === 0) {
+ finishResize(event);
+ return;
+ }
onResize(Math.min(max, Math.max(min, drag.startHeight + event.clientY - drag.startY)));
}}
onPointerUp={finishResize}
onPointerCancel={finishResize}
+ onLostPointerCapture={(event) => {
+ if (dragRef.current?.pointerId === event.pointerId) dragRef.current = null;
+ }}
>
diff --git a/web/src/components/canvas/canvas-resource-mention-textarea.tsx b/web/src/components/canvas/canvas-resource-mention-textarea.tsx
index c97b9e50e..10e8d7106 100644
--- a/web/src/components/canvas/canvas-resource-mention-textarea.tsx
+++ b/web/src/components/canvas/canvas-resource-mention-textarea.tsx
@@ -1,15 +1,17 @@
import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties, ClipboardEvent, KeyboardEvent, MouseEvent, PointerEvent, TextareaHTMLAttributes } from "react";
import { createPortal } from "react-dom";
-import { FileText, Image as ImageIcon, Music2, Pencil, Sparkles, UserRound, Video } from "lucide-react";
+import { ArrowLeft, ChevronRight, FileText, Folder, Image as ImageIcon, Music2, Pencil, Search, Sparkles, UserRound, Video } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
-import { canvasResourceMentionToken, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
+import { buildAssetMentionReferences, canvasResourceMentionToken, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
+import { useAssetStore, type AssetCategory } from "@/stores/use-asset-store";
import { CanvasNodeType } from "@/types/canvas";
type MentionState = {
start: number;
+ end: number;
query: string;
};
@@ -39,13 +41,15 @@ type Props = Omit, "onChange" | "val
mentionMenuWidth?: number;
sendOnEnter?: boolean;
onContentSizeChange?: (height: number) => void;
+ includeAssetLibrary?: boolean;
};
export const CanvasResourceMentionTextarea = forwardRef(function CanvasResourceMentionTextarea(
- { value, references, onChange, onSubmit, onKeyDown, className, containerClassName, style, highlightLabels = true, mentionMenuWidth = 256, sendOnEnter = true, onContentSizeChange, ...props },
+ { value, references, onChange, onSubmit, onKeyDown, className, containerClassName, style, highlightLabels = true, mentionMenuWidth = 320, sendOnEnter = true, onContentSizeChange, includeAssetLibrary = false, ...props },
forwardedRef,
) {
const rawTheme = useThemeStore((state) => state.theme);
+ const assets = useAssetStore((state) => state.assets);
const theme = canvasThemes[rawTheme as keyof typeof canvasThemes] ?? canvasThemes.dark;
const containerRef = useRef(null);
const textareaRef = useRef(null);
@@ -54,22 +58,33 @@ export const CanvasResourceMentionTextarea = forwardRef(null);
const lastRenderedValueRef = useRef("");
const [mention, setMention] = useState(null);
- const [activeIndex, setActiveIndex] = useState(0);
+ const [activeIndex, setActiveIndex] = useState(-1);
+ const assetReferences = useMemo(() => includeAssetLibrary ? buildAssetMentionReferences(assets) : [], [assets, includeAssetLibrary]);
+ const activeCanvasReferences = useMemo(() => references.filter((item) => item.active), [references]);
+ const availableReferences = useMemo(() => [...activeCanvasReferences, ...assetReferences], [activeCanvasReferences, assetReferences]);
const candidates = useMemo(() => {
if (!mention) return [];
const query = mention.query.trim().toLowerCase();
- const activeItems = references.filter((item) => item.active);
- if (!query) return activeItems;
- return activeItems.filter((item) => `${item.label} ${item.title} ${item.kind} ${item.text || ""}`.toLowerCase().includes(query));
- }, [mention, references]);
- const activeReferences = useMemo(() => (highlightLabels ? references.filter((item) => item.active) : []), [highlightLabels, references]);
+ if (!query) return activeCanvasReferences;
+ return availableReferences.filter((item) => `${item.label} ${item.title} ${item.kind} ${item.category || ""} ${item.text || ""}`.toLowerCase().includes(query));
+ }, [activeCanvasReferences, availableReferences, mention]);
+ const activeReferences = useMemo(() => {
+ if (!highlightLabels) return [];
+ return [...activeCanvasReferences, ...assetReferences.filter((item) => value.includes(canvasResourceMentionToken(item)))];
+ }, [activeCanvasReferences, assetReferences, highlightLabels, value]);
const useRichEditor = Boolean(activeReferences.length);
const reportContentSize = useCallback((element: HTMLElement | null) => {
if (!element || !onContentSizeChange) return;
- const previousHeight = element.style.height;
- element.style.height = "0px";
- const height = element.scrollHeight;
- element.style.height = previousHeight;
+ const previous = { height: element.style.height, minHeight: element.style.minHeight, maxHeight: element.style.maxHeight, overflow: element.style.overflow };
+ element.style.height = "1px";
+ element.style.minHeight = "0";
+ element.style.maxHeight = "none";
+ element.style.overflow = "hidden";
+ const height = Math.ceil(element.scrollHeight);
+ element.style.height = previous.height;
+ element.style.minHeight = previous.minHeight;
+ element.style.maxHeight = previous.maxHeight;
+ element.style.overflow = previous.overflow;
onContentSizeChange(height);
}, [onContentSizeChange]);
@@ -96,7 +111,13 @@ export const CanvasResourceMentionTextarea = forwardRef reportContentSize(element));
+ let width = container.clientWidth;
+ const observer = new ResizeObserver(() => {
+ const nextWidth = container.clientWidth;
+ if (nextWidth === width) return;
+ width = nextWidth;
+ reportContentSize(element);
+ });
observer.observe(container);
return () => observer.disconnect();
}, [onContentSizeChange, reportContentSize, useRichEditor]);
@@ -123,30 +144,28 @@ export const CanvasResourceMentionTextarea = forwardRef {
setMention(null);
- setActiveIndex(0);
+ setActiveIndex(-1);
};
const syncMention = (nextValue: string, cursor: number) => {
const prefix = nextValue.slice(0, cursor);
const match = /@([^\s@,.;:!?,。;:!?、)\]}】)]*)$/.exec(prefix);
- if (!match || !references.some((item) => item.active)) {
+ if (!match || !availableReferences.length) {
closeMention();
return;
}
- const nextMention = { start: match.index, query: match[1] };
- const isSameMention = mention?.start === nextMention.start && mention.query === nextMention.query;
+ const nextMention = { start: match.index, end: cursor, query: match[1] };
+ const isSameMention = mention?.start === nextMention.start && mention.end === nextMention.end && mention.query === nextMention.query;
if (!isSameMention) {
setMention(nextMention);
- setActiveIndex(0);
+ setActiveIndex(-1);
}
};
const insertReference = (reference: CanvasResourceReference) => {
if (!mention) return;
- const selection = useRichEditor ? getEditableSelection(editorRef.current) : null;
- const end = selection?.end ?? textareaRef.current?.selectionStart ?? value.length;
const insertText = `${canvasResourceMentionToken(reference)} `;
- const next = `${value.slice(0, mention.start)}${insertText}${value.slice(end)}`;
+ const next = `${value.slice(0, mention.start)}${insertText}${value.slice(mention.end)}`;
closeMention();
updateValue(next, mention.start + insertText.length);
};
@@ -185,7 +204,21 @@ export const CanvasResourceMentionTextarea = forwardRef : null;
+ const menu = mention && availableReferences.length && menuAnchor ? (
+ = 0 ? candidates[Math.min(activeIndex, candidates.length - 1)]?.id : undefined}
+ theme={theme}
+ preferredWidth={mentionMenuWidth}
+ onQueryChange={(query) => setMention((current) => current ? { ...current, query } : current)}
+ onClose={closeMention}
+ onSelect={insertReference}
+ />
+ ) : null;
if (useRichEditor) {
return (
@@ -225,17 +258,17 @@ export const CanvasResourceMentionTextarea = forwardRef (index + 1) % candidates.length);
+ setActiveIndex((index) => index < 0 ? 0 : (index + 1) % candidates.length);
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
- setActiveIndex((index) => (index - 1 + candidates.length) % candidates.length);
+ setActiveIndex((index) => index < 0 ? candidates.length - 1 : (index - 1 + candidates.length) % candidates.length);
return;
}
if (event.key === "Enter" || event.key === "Tab") {
event.preventDefault();
- insertReference(candidates[Math.min(activeIndex, candidates.length - 1)]);
+ insertReference(candidates[activeIndex < 0 ? 0 : Math.min(activeIndex, candidates.length - 1)]);
return;
}
if (event.key === "Escape") {
@@ -274,7 +307,11 @@ export const CanvasResourceMentionTextarea = forwardRef props.onScroll?.(event as unknown as React.UIEvent)}
onFocus={(event) => props.onFocus?.(event as unknown as React.FocusEvent)}
onBlur={(event) => {
- window.setTimeout(closeMention, 120);
+ if (event.relatedTarget instanceof Element && event.relatedTarget.closest("[data-canvas-resource-mention-menu]")) return;
+ window.setTimeout(() => {
+ if (document.activeElement?.closest("[data-canvas-resource-mention-menu]")) return;
+ closeMention();
+ }, 120);
props.onBlur?.(event as unknown as React.FocusEvent);
}}
>
@@ -306,17 +343,17 @@ export const CanvasResourceMentionTextarea = forwardRef (index + 1) % candidates.length);
+ setActiveIndex((index) => index < 0 ? 0 : (index + 1) % candidates.length);
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
- setActiveIndex((index) => (index - 1 + candidates.length) % candidates.length);
+ setActiveIndex((index) => index < 0 ? candidates.length - 1 : (index - 1 + candidates.length) % candidates.length);
return;
}
if (event.key === "Enter") {
event.preventDefault();
- insertReference(candidates[Math.min(activeIndex, candidates.length - 1)]);
+ insertReference(candidates[activeIndex < 0 ? 0 : Math.min(activeIndex, candidates.length - 1)]);
return;
}
if (event.key === "Escape") {
@@ -345,7 +382,11 @@ export const CanvasResourceMentionTextarea = forwardRef {
- window.setTimeout(closeMention, 120);
+ if (event.relatedTarget instanceof Element && event.relatedTarget.closest("[data-canvas-resource-mention-menu]")) return;
+ window.setTimeout(() => {
+ if (document.activeElement?.closest("[data-canvas-resource-mention-menu]")) return;
+ closeMention();
+ }, 120);
props.onBlur?.(event);
}}
/>
@@ -393,12 +434,36 @@ function createInlinePreview(reference: CanvasResourceReference) {
return fallback;
}
-function MentionMenu({ anchor, references, activeIndex, theme, preferredWidth, onSelect }: { anchor: HTMLElement; references: CanvasResourceReference[]; activeIndex: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; preferredWidth: number; onSelect: (reference: CanvasResourceReference) => void }) {
+const ASSET_CATEGORY_LABELS: Record = {
+ character: "角色",
+ environment: "场景",
+ wardrobe: "服饰",
+ prop: "道具",
+ weapon: "武器",
+ style: "画风",
+ other: "其他",
+};
+
+function MentionMenu({ anchor, connectedReferences, assetReferences, filteredReferences, query, activeReferenceId, theme, preferredWidth, onQueryChange, onClose, onSelect }: {
+ anchor: HTMLElement;
+ connectedReferences: CanvasResourceReference[];
+ assetReferences: CanvasResourceReference[];
+ filteredReferences: CanvasResourceReference[];
+ query: string;
+ activeReferenceId?: string;
+ theme: (typeof canvasThemes)[keyof typeof canvasThemes];
+ preferredWidth: number;
+ onQueryChange: (query: string) => void;
+ onClose: () => void;
+ onSelect: (reference: CanvasResourceReference) => void;
+}) {
+ const menuRef = useRef(null);
const selectedRef = useRef(false);
+ const [category, setCategory] = useState(null);
const rect = anchor.getBoundingClientRect();
- const boundary = anchor.closest(".ant-modal-content")?.getBoundingClientRect() || { left: 8, top: 8, right: window.innerWidth - 8, bottom: window.innerHeight - 8 };
+ const boundary = anchor.closest(".ant-modal-container")?.getBoundingClientRect() || { left: 8, top: 8, right: window.innerWidth - 8, bottom: window.innerHeight - 8 };
const menuWidth = Math.min(preferredWidth, Math.max(0, boundary.right - boundary.left - 16));
- const maxMenuHeight = 224;
+ const maxMenuHeight = Math.min(320, Math.max(0, boundary.bottom - boundary.top - 16));
const gap = 6;
const left = clamp(rect.left, boundary.left + 8, boundary.right - menuWidth - 8);
const showAbove = rect.bottom + gap + maxMenuHeight > boundary.bottom && rect.top - gap - maxMenuHeight >= boundary.top;
@@ -412,60 +477,149 @@ function MentionMenu({ anchor, references, activeIndex, theme, preferredWidth, o
selectedRef.current = true;
onSelect(reference);
};
+ const categoryItems = Object.entries(ASSET_CATEGORY_LABELS)
+ .map(([value, label]) => ({ value: value as AssetCategory, label, count: assetReferences.filter((item) => item.category === value).length }))
+ .filter((item) => item.count > 0);
+ const connectedNodes = connectedReferences.filter((item) => item.kind !== "skill");
+ const skillReferences = connectedReferences.filter((item) => item.kind === "skill");
+ const visibleReferences = query
+ ? filteredReferences
+ : category
+ ? assetReferences.filter((item) => item.category === category)
+ : [];
+
+ useLayoutEffect(() => {
+ const closeOnOutsidePointer = (event: globalThis.PointerEvent) => {
+ const target = event.target;
+ if (!(target instanceof Node) || menuRef.current?.contains(target) || anchor.contains(target)) return;
+ onClose();
+ };
+ window.addEventListener("pointerdown", closeOnOutsidePointer, true);
+ return () => window.removeEventListener("pointerdown", closeOnOutsidePointer, true);
+ }, [anchor, onClose]);
return createPortal(
event.stopPropagation()}
>
- {references.map((reference, index) => (
-
,
document.body,
);
}
+function MentionReferenceList({ references, activeReferenceId, theme, onSelect }: { references: CanvasResourceReference[]; activeReferenceId?: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onSelect: (reference: CanvasResourceReference) => void }) {
+ if (!references.length) return 没有匹配的素材
;
+ return references.map((reference) => (
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ onSelect(reference);
+ }}
+ onClick={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ onSelect(reference);
+ }}
+ >
+
+
+ {reference.label}
+ {reference.kind !== "skill" && reference.text && reference.text !== reference.title ? {reference.text} : null}
+
+
+ ));
+}
+
function ReferencePreview({ reference }: { reference: CanvasResourceReference }) {
- if (reference.kind === "image" && reference.previewUrl) return
;
- if (reference.kind === "video" && reference.previewUrl) return ;
- if (reference.kind === "character" && reference.previewUrl) return
;
+ if (reference.kind === "image" && reference.previewUrl) return
;
+ if (reference.kind === "video" && reference.previewUrl) return ;
+ if (reference.kind === "character" && reference.previewUrl) return
;
if (reference.kind === "skill") {
return (
-
-
+
+
);
}
const Icon = reference.sourceType === CanvasNodeType.Drawing ? Pencil : reference.kind === "character" ? UserRound : reference.kind === "audio" ? Music2 : reference.kind === "video" ? Video : reference.kind === "image" ? ImageIcon : FileText;
return (
-
-
+
+
);
}
diff --git a/web/src/components/model-picker.tsx b/web/src/components/model-picker.tsx
index 3505d45e1..fc04f1de2 100644
--- a/web/src/components/model-picker.tsx
+++ b/web/src/components/model-picker.tsx
@@ -4,7 +4,7 @@ import { Popover } from "antd";
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
import { modelCapabilityConfigFor, videoDurationOptions } from "@/lib/model-capabilities";
-import { compatibleModelInGroup, groupModelsByDisplayName, modelCompatibilityError, resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
+import { compatibleModelInGroup, configuredModelDisplayName, groupModelsByDisplayName, modelCompatibilityError, resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
import { cn } from "@/lib/utils";
import { modelDisplayName, modelOptionLabel, modelOptionName, resolveModelChannel, selectableModelsByCapability, type AiConfig, type ModelCapability } from "@/stores/use-config-store";
import { useThemeStore } from "@/stores/use-theme-store";
@@ -22,9 +22,10 @@ type ModelPickerProps = {
showSelectedPrice?: boolean;
variant?: "default" | "creation";
requirements?: ModelRequirements;
+ showConfiguredModelName?: boolean;
};
-export function ModelPicker({ config, value, onChange, capability, className, fullWidth = false, placeholder = "选择模型", onMissingConfig, showSelectedPrice = true, variant = "default", requirements }: ModelPickerProps) {
+export function ModelPicker({ config, value, onChange, capability, className, fullWidth = false, placeholder = "选择模型", onMissingConfig, showSelectedPrice = true, variant = "default", requirements, showConfiguredModelName = false }: ModelPickerProps) {
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
const pickerId = useId();
// 双保险:即使 store merge 写出非法 theme,这里也兜底到 dark,避免 "reading 'node'" 崩溃
@@ -129,7 +130,7 @@ export function ModelPicker({ config, value, onChange, capability, className, fu
{creationVariant ? (
选择模型
- {current ? {modelDisplayName(config, current)} : null}
+ {current ? {pickerModelDisplayName(config, current, showConfiguredModelName)} : null}
) : null}
{optionGroups.length ? (
@@ -155,7 +156,7 @@ export function ModelPicker({ config, value, onChange, capability, className, fu
aria-selected={selected}
aria-disabled={Boolean(disabledReason)}
disabled={Boolean(disabledReason)}
- title={disabledReason || modelOptionLabel(config, displayModel)}
+ title={disabledReason || pickerModelOptionLabel(config, displayModel, showConfiguredModelName)}
className="canvas-model-picker-option disabled:cursor-not-allowed disabled:opacity-45"
style={{ background: "transparent", color: theme.node.text }}
onClick={() => {
@@ -165,7 +166,7 @@ export function ModelPicker({ config, value, onChange, capability, className, fu
window.requestAnimationFrame(() => triggerRef.current?.focus());
}}
>
-
+
{selected ? : null}
);
@@ -203,14 +204,14 @@ export function ModelPicker({ config, value, onChange, capability, className, fu
aria-haspopup="listbox"
aria-expanded={open}
aria-label={placeholder}
- title={current ? modelOptionLabel(config, current) : placeholder}
+ title={current ? pickerModelOptionLabel(config, current, showConfiguredModelName) : placeholder}
onKeyDown={handleTriggerKeyDown}
>
- {current ? (creationVariant ? modelDisplayName(config, current) : modelOptionLabel(config, current)) : placeholder}
+ {current ? (creationVariant ? pickerModelDisplayName(config, current, showConfiguredModelName) : pickerModelOptionLabel(config, current, showConfiguredModelName)) : placeholder}
{showSelectedPrice && creditsEnabled ? : null}
@@ -232,6 +233,7 @@ function ModelLabel({
capability,
theme,
creationVariant,
+ showConfiguredModelName,
showPrice,
disabledReason,
}: {
@@ -240,6 +242,7 @@ function ModelLabel({
capability?: ModelCapability;
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
creationVariant: boolean;
+ showConfiguredModelName: boolean;
showPrice: boolean;
disabledReason?: string;
}) {
@@ -252,7 +255,7 @@ function ModelLabel({
- {modelDisplayName(config, model)}
+ {pickerModelDisplayName(config, model, showConfiguredModelName)}
{capabilitySummary}
@@ -281,6 +284,14 @@ function modelMenuPrice(config: AiConfig, model: string): { value: number; unit:
return { value: cost.unitPriceMicrocredits / 1_000_000, unit: cost.billingMode === "per_second" ? "秒" : "次" };
}
+function pickerModelDisplayName(config: AiConfig, model: string, showConfiguredModelName: boolean) {
+ return showConfiguredModelName ? configuredModelDisplayName(config, model) : modelDisplayName(config, model);
+}
+
+function pickerModelOptionLabel(config: AiConfig, model: string, showConfiguredModelName: boolean) {
+ return showConfiguredModelName ? `${configuredModelDisplayName(config, model)}(${resolveModelChannel(config, model).name})` : modelOptionLabel(config, model);
+}
+
function ModelPrice({ price, compact = false }: { price: { value: number; unit: "次" | "秒" } | null | undefined; compact?: boolean }) {
if (price === undefined) return null;
if (price === null) return compact ? null : 未配置;
diff --git a/web/src/lib/canvas/canvas-resource-references.ts b/web/src/lib/canvas/canvas-resource-references.ts
index 120d5d4a0..69f7ad913 100644
--- a/web/src/lib/canvas/canvas-resource-references.ts
+++ b/web/src/lib/canvas/canvas-resource-references.ts
@@ -1,6 +1,7 @@
import { imageReferenceLabel } from "@/lib/image-reference-prompt";
import { seedanceReferenceLabel } from "@/lib/seedance-video";
import type { Skill } from "@/services/api/skills";
+import type { Asset, AssetCategory } from "@/stores/use-asset-store";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
export type CanvasResourceKind = "image" | "video" | "audio" | "text" | "skill" | "character";
@@ -17,13 +18,38 @@ export type CanvasResourceReference = {
active: boolean;
sourceType?: CanvasNodeType;
skill?: Skill;
+ assetId?: string;
+ category?: AssetCategory;
};
export function canvasResourceMentionToken(reference: CanvasResourceReference) {
if (reference.kind === "skill" && reference.skill?.skill_id) return `@[skill:${reference.skill.skill_id}]`;
+ if (reference.assetId) return `@[asset:${reference.assetId}]`;
return `@[node:${reference.nodeId}]`;
}
+export function buildAssetMentionReferences(assets: Asset[]): CanvasResourceReference[] {
+ return assets.flatMap((asset): CanvasResourceReference[] => {
+ if (asset.kind === "model") return [];
+ const kind: CanvasResourceKind = asset.kind === "entity" ? "character" : asset.kind;
+ const previewUrl = asset.kind === "image" ? asset.data.dataUrl : asset.kind === "video" ? asset.data.url : asset.coverUrl;
+ const text = asset.kind === "text" ? asset.data.content : undefined;
+ return [{
+ id: `asset:${asset.id}`,
+ nodeId: "",
+ assetId: asset.id,
+ kind,
+ label: asset.title,
+ title: asset.title,
+ previewUrl,
+ storageKey: "storageKey" in asset.data ? asset.data.storageKey : undefined,
+ text,
+ active: false,
+ category: asset.category || "other",
+ }];
+ });
+}
+
export function buildCanvasResourceReferences(nodes: CanvasNodeData[], connections: CanvasConnection[], contextNodeId?: string | null) {
const contextNodes = contextNodeId ? getMentionResourceNodes(contextNodeId, nodes, connections) : [];
const globalReferences = labelResourceNodes(nodes.filter(isResourceNode), false);
diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx
index dde12a1a4..148a6a27c 100644
--- a/web/src/pages/canvas/project.tsx
+++ b/web/src/pages/canvas/project.tsx
@@ -1181,6 +1181,7 @@ function InfiniteCanvasPage() {
projectId,
domainProjectId: currentProject?.projectId,
addedSkills,
+ assets,
nodesRef,
connectionsRef,
setNodes,
@@ -1221,6 +1222,7 @@ function InfiniteCanvasPage() {
projectId,
domainProjectId: currentProject?.projectId,
addedSkills,
+ assets,
nodesRef,
connectionsRef,
setNodes,
diff --git a/web/src/pages/canvas/use-canvas-generation-executor.ts b/web/src/pages/canvas/use-canvas-generation-executor.ts
index c2ad92d19..8cbb1834a 100644
--- a/web/src/pages/canvas/use-canvas-generation-executor.ts
+++ b/web/src/pages/canvas/use-canvas-generation-executor.ts
@@ -14,6 +14,7 @@ import { navigateToSettings } from "@/lib/settings-navigation";
import type { Skill } from "@/services/api/skills";
import type { GenerationTask } from "@/services/api/task-center";
import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
+import type { Asset } from "@/stores/use-asset-store";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
import { executeImageGeneration } from "./canvas-image-generation-executor";
@@ -24,6 +25,7 @@ type UseCanvasGenerationExecutorOptions = {
projectId: string;
domainProjectId?: string;
addedSkills: Skill[];
+ assets: Asset[];
nodesRef: { current: CanvasNodeData[] };
connectionsRef: { current: CanvasConnection[] };
setNodes: Dispatch>;
@@ -50,6 +52,7 @@ export function useCanvasGenerationExecutor({
projectId,
domainProjectId,
addedSkills,
+ assets,
nodesRef,
connectionsRef,
setNodes,
@@ -128,7 +131,7 @@ export function useCanvasGenerationExecutor({
// 视频文本只保留输入框内容;连接的媒体仍作为结构化参考传递。
const promptOnly = mode === "video";
try {
- const baseContext = buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt, promptOnly);
+ const baseContext = buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt, assets, promptOnly);
const requirements = generationModelRequirements(mode, baseContext, sourceNode, generationConfig.videoSeconds, true);
generationConfig = buildGenerationConfig(effectiveConfig, sourceNode, mode, requirements);
const compatibilityError = modelCompatibilityError(generationConfig, generationConfig.model, requirements);
@@ -267,7 +270,7 @@ export function useCanvasGenerationExecutor({
setRunningNodeId(null);
}
},
- [addedSkills, bindGenerationTask, domainProjectId, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, nodesRef, connectionsRef, projectId, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest],
+ [addedSkills, assets, bindGenerationTask, domainProjectId, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, nodesRef, connectionsRef, projectId, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest],
);
}
diff --git a/web/src/pages/canvas/use-canvas-generation-retry.ts b/web/src/pages/canvas/use-canvas-generation-retry.ts
index 2fb50e2a5..c54db0eab 100644
--- a/web/src/pages/canvas/use-canvas-generation-retry.ts
+++ b/web/src/pages/canvas/use-canvas-generation-retry.ts
@@ -33,12 +33,14 @@ import type { Skill } from "@/services/api/skills";
import type { GenerationTask } from "@/services/api/task-center";
import { resolveImageUrl, uploadImage } from "@/services/image-storage";
import { resolveModelRequestConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
+import type { Asset } from "@/stores/use-asset-store";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
type UseCanvasGenerationRetryOptions = {
projectId: string;
domainProjectId?: string;
addedSkills: Skill[];
+ assets: Asset[];
nodesRef: { current: CanvasNodeData[] };
connectionsRef: { current: CanvasConnection[] };
setNodes: Dispatch>;
@@ -52,7 +54,7 @@ const NODE_STATUS_LOADING = "loading" as const;
const NODE_STATUS_SUCCESS = "success" as const;
const NODE_STATUS_ERROR = "error" as const;
-export function useCanvasGenerationRetry({ projectId, domainProjectId, addedSkills, nodesRef, connectionsRef, setNodes, setRunningNodeId, startGenerationRequest, finishGenerationRequest, bindGenerationTask }: UseCanvasGenerationRetryOptions) {
+export function useCanvasGenerationRetry({ projectId, domainProjectId, addedSkills, assets, nodesRef, connectionsRef, setNodes, setRunningNodeId, startGenerationRequest, finishGenerationRequest, bindGenerationTask }: UseCanvasGenerationRetryOptions) {
const { message } = App.useApp();
const effectiveConfig = useEffectiveConfig();
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
@@ -88,7 +90,7 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, addedSkil
let rawContext: Awaited> | null;
try {
const promptOnly = retryMode === "video";
- const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryContextPrompt, promptOnly);
+ const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryContextPrompt, assets, promptOnly);
rawContext = hasSavedImageMetadata && !baseContext.characterReferences.length ? null : await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig), !promptOnly);
} catch (error) {
const failure = generationFailureMetadata(error, retryPromptSource);
@@ -227,7 +229,7 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, addedSkil
setRunningNodeId(null);
}
},
- [addedSkills, bindGenerationTask, connectionsRef, domainProjectId, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, nodesRef, projectId, setNodes, setRunningNodeId, startGenerationRequest],
+ [addedSkills, assets, bindGenerationTask, connectionsRef, domainProjectId, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, nodesRef, projectId, setNodes, setRunningNodeId, startGenerationRequest],
);
}
diff --git a/web/src/pages/create/index.tsx b/web/src/pages/create/index.tsx
index d35f922b7..e45fb77c9 100644
--- a/web/src/pages/create/index.tsx
+++ b/web/src/pages/create/index.tsx
@@ -2,9 +2,9 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, ty
import localforage from "localforage";
import { App, Drawer, Modal, Popover, Spin, Tooltip } from "antd";
import { ArrowDown, ArrowUp, Check, ChevronDown, Clapperboard, Clock3, Download, FileText, Film, FolderOpen, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Music2, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Square, X } from "lucide-react";
-import ReactMarkdown from "react-markdown";
import { Link } from "react-router";
+import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
import { AssetLibraryPickerModal, type AssetLibraryPickerItem } from "@/components/assets/asset-library-picker-modal";
import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
import { VoiceRecordingButton } from "@/components/conversation/voice-recording-button";
@@ -1061,7 +1061,7 @@ function StoryboardShotResult({ result, onRetryFailure, onCreateVariant }: { res
;
}
if (status === "error") return {generationErrorMessage(result.error || "")}重新生成
;
- if (mode === "text") return {result.content ? {result.content} : 正在生成…}
;
+ if (mode === "text") return {result.content ?
{result.content} :
正在生成…}
;
if (!resultUrls.length) return 没有返回可预览结果 重试
;
const note = result.settings ? directorNoteFor(mode, result.settings) : "";
return <>
diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css
index fc3f1eb0c..e244a26ff 100644
--- a/web/src/styles/globals.css
+++ b/web/src/styles/globals.css
@@ -1,6 +1,8 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
+@source "../../node_modules/streamdown/dist/*.js";
+@source "../../node_modules/@streamdown/code/dist/*.js";
@custom-variant dark (&:is(.dark *));
@@ -712,6 +714,9 @@
--canvas-composer-control-hover: color-mix(in srgb, var(--foreground) 8%, transparent);
--canvas-composer-shadow: var(--elevation-card);
--canvas-composer-settings-width: 138px;
+ --canvas-mention-menu-max-height: 320px;
+ --canvas-mention-search-height: 40px;
+ --canvas-mention-item-height: 34px;
/* Modal 组件(对话框,对应 #98 面板层级)*/
--modal-radius: var(--r-lg);
@@ -3115,7 +3120,7 @@
.creation-message-model { max-width: min(400px, 48vw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.creation-message-state { border-radius: 6px; background: rgba(255, 255, 255, .08); padding: 3px 6px; color: var(--creation-muted); }
.creation-message-state.is-error { background: rgba(185, 61, 61, .18); color: #ec7474; }
- .creation-message-content { padding: 14px 0 0 42px; color: var(--creation-text); font-size: 15px; line-height: 1.8; white-space: pre-wrap; }
+ .creation-message-content { padding: 14px 0 0 42px; color: var(--creation-text); font-size: 15px; line-height: 1.8; }
.creation-message-content > span { color: var(--creation-muted); }
.creation-message-error { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 15px 0 0 42px; color: #ef5350; font-size: 14px; line-height: 1.6; }
.creation-message-error button { display: inline-flex; align-items: center; gap: 6px; border: 1px solid rgba(255, 255, 255, .13); border-radius: 8px; background: rgba(255, 255, 255, .06); color: var(--creation-text); padding: 6px 9px; font-size: 12px; transition: background-color 140ms ease; }
@@ -5985,6 +5990,134 @@
outline: none;
}
+ .canvas-resource-mention-menu {
+ display: flex;
+ max-height: var(--canvas-mention-menu-max-height);
+ flex-direction: column;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
+ border-radius: var(--r-md);
+ background: var(--canvas-composer-surface);
+ color: var(--cn-text);
+ box-shadow: var(--elevation-overlay);
+ }
+
+ .canvas-resource-mention-search {
+ display: flex;
+ min-height: var(--canvas-mention-search-height);
+ flex: 0 0 auto;
+ align-items: center;
+ gap: var(--space-2);
+ padding-inline: var(--space-2-half);
+ color: color-mix(in srgb, var(--foreground) 44%, transparent);
+ }
+
+ .canvas-resource-mention-search > svg {
+ width: var(--space-4);
+ height: var(--space-4);
+ flex: 0 0 auto;
+ }
+
+ .canvas-resource-mention-search > input {
+ min-width: 0;
+ flex: 1 1 auto;
+ border: 0;
+ outline: 0;
+ background: transparent;
+ color: var(--cn-text);
+ font-size: var(--fs-caption);
+ }
+
+ .canvas-resource-mention-search > input::placeholder {
+ color: color-mix(in srgb, var(--foreground) 36%, transparent);
+ }
+
+ .canvas-resource-mention-scroll {
+ min-height: 0;
+ overflow-y: auto;
+ padding: 0 var(--space-1) var(--space-1);
+ border-top: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
+ }
+
+ .canvas-resource-mention-section {
+ padding-top: var(--space-1);
+ }
+
+ .canvas-resource-mention-section + .canvas-resource-mention-section {
+ margin-top: var(--space-1);
+ }
+
+ .canvas-resource-mention-section > h4 {
+ margin: 0;
+ padding: var(--space-1) var(--space-2);
+ color: color-mix(in srgb, var(--foreground) 48%, transparent);
+ font-size: var(--fs-tiny);
+ font-weight: 500;
+ }
+
+ .canvas-resource-mention-item,
+ .canvas-resource-mention-folder,
+ .canvas-resource-mention-back {
+ display: flex;
+ width: 100%;
+ min-width: 0;
+ min-height: var(--canvas-mention-item-height);
+ align-items: center;
+ gap: var(--space-2);
+ border: 0;
+ border-radius: var(--r-md);
+ background: transparent;
+ padding: calc(var(--space-1) / 2) var(--space-2);
+ color: inherit;
+ text-align: left;
+ transition: background-color var(--motion-instant) ease-out;
+ }
+
+ .canvas-resource-mention-item:hover,
+ .canvas-resource-mention-folder:hover,
+ .canvas-resource-mention-back:hover {
+ background: var(--canvas-composer-control-hover) !important;
+ }
+
+ .canvas-resource-mention-folder > svg:first-child,
+ .canvas-resource-mention-back > svg:first-child {
+ width: var(--space-4);
+ height: var(--space-4);
+ flex: 0 0 auto;
+ color: color-mix(in srgb, var(--foreground) 58%, transparent);
+ }
+
+ .canvas-resource-mention-folder > span,
+ .canvas-resource-mention-back > span {
+ min-width: 0;
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: var(--fs-caption);
+ }
+
+ .canvas-resource-mention-folder > small,
+ .canvas-resource-mention-back > small {
+ color: color-mix(in srgb, var(--foreground) 38%, transparent);
+ font-size: var(--fs-tiny);
+ font-variant-numeric: tabular-nums;
+ }
+
+ .canvas-resource-mention-folder > svg:last-child {
+ width: var(--space-4);
+ height: var(--space-4);
+ flex: 0 0 auto;
+ color: color-mix(in srgb, var(--foreground) 32%, transparent);
+ }
+
+ .canvas-resource-mention-empty {
+ padding: var(--space-8) var(--space-3);
+ color: color-mix(in srgb, var(--foreground) 42%, transparent);
+ font-size: var(--fs-tiny);
+ text-align: center;
+ }
+
[data-canvas-viewport-interacting="true"] .canvas-node-shell,
[data-canvas-viewport-interacting="true"] .canvas-frame-shell,
[data-canvas-viewport-interacting="true"] .aceternity-comet-card {
@@ -7167,19 +7300,6 @@ html:not(.dark) .creation-user-message-reference { background: rgba(28, 32, 37,
.creation-message-state { border: 0; border-radius: 5px; background: rgba(255, 255, 255, .06); padding: 2px 5px; color: var(--creation-muted); font-size: 10px; }
.creation-message-content { max-width: 800px; padding: 14px 0 0 34px; color: var(--creation-text); font-size: 15px; line-height: 1.8; }
-.creation-message-content p { margin: 0 0 12px; }
-.creation-message-content p:last-child { margin-bottom: 0; }
-.creation-message-content h1, .creation-message-content h2, .creation-message-content h3 { margin: 20px 0 8px; color: var(--creation-text); font-weight: 650; line-height: 1.35; }
-.creation-message-content h1 { font-size: 21px; }
-.creation-message-content h2 { font-size: 18px; }
-.creation-message-content h3 { font-size: 16px; }
-.creation-message-content ul, .creation-message-content ol { margin: 10px 0; padding-left: 22px; }
-.creation-message-content li + li { margin-top: 4px; }
-.creation-message-content blockquote { margin: 14px 0; border-left: 2px solid color-mix(in srgb, var(--creation-text) 24%, transparent); padding: 2px 0 2px 12px; color: var(--creation-muted); }
-.creation-message-content code { border-radius: 4px; background: rgba(255, 255, 255, .08); padding: 2px 5px; font-size: .88em; }
-.creation-message-content pre { overflow-x: auto; border-radius: 8px; background: #181b20; padding: 12px 14px; }
-.creation-message-content pre code { background: transparent; padding: 0; }
-.creation-message-content a { color: var(--creation-text); text-decoration: underline; text-underline-offset: 3px; }
.creation-message-error { display: flex; max-width: 760px; align-items: center; gap: 10px; padding: 16px 0 0 34px; color: #ee8585; font-size: 13px; line-height: 1.6; }
.creation-message-error button { display: inline-flex; height: 28px; align-items: center; gap: 5px; border: 0; border-radius: 6px; background: rgba(226, 96, 96, .12); color: #ef8b8b; padding: 0 8px; font-size: 11px; }
@@ -7211,10 +7331,8 @@ html:not(.dark) .creation-user-message-reference { background: rgba(28, 32, 37,
.creation-media-preview-video { width: 100%; background: #050505; }
html:not(.dark) .creation-user-message { background: rgba(28, 32, 37, .07); }
-html:not(.dark) .creation-message-content pre,
html:not(.dark) .creation-video-result,
html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: #1f2227; }
-html:not(.dark) .creation-message-content code { background: rgba(28, 32, 37, .08); }
@media (max-width: 800px) {
.creation-home .creation-chat-writing-surface,
@@ -8451,7 +8569,7 @@ body.app-spatial-overlays :where(.ant-modal-mask, .ant-drawer-mask) {
.app-story-generating p {
margin: 0;
color: var(--foreground);
- font-size: var(--fs-body);
+ font-size: var(--fs-caption);
font-weight: 560;
}
@@ -9938,3 +10056,105 @@ html:not(.dark) .creation-home .storyboard-workbench-bar-action:hover { backgrou
.wallet-library-page .wallet-ledger-table .ant-table-tbody > tr:hover > td {
background: color-mix(in srgb, var(--foreground) 2.5%, var(--background)) !important;
}
+
+/* ===== AI Markdown ===== */
+
+.ai-message-markdown {
+ min-width: 0;
+ color: inherit;
+ font-size: inherit;
+ line-height: 1.7;
+ overflow-wrap: anywhere;
+}
+.ai-message-markdown > :first-child { margin-top: 0; }
+.ai-message-markdown > :last-child { margin-bottom: 0; }
+.ai-message-markdown-heading {
+ margin: 1.15em 0 0.42em;
+ color: inherit;
+ font-weight: 650;
+ line-height: 1.35;
+}
+.ai-message-markdown-heading-1 { font-size: 1.18em; }
+.ai-message-markdown-heading-2 { font-size: 1.08em; }
+.ai-message-markdown-heading-3,
+.ai-message-markdown-heading-4 { font-size: 1em; }
+.ai-message-markdown-paragraph { margin: 0.5em 0; }
+.ai-message-markdown-list {
+ margin: 0.55em 0;
+ padding-left: 1.35em;
+}
+.ai-message-markdown-list-unordered { list-style: disc; }
+.ai-message-markdown-list-ordered { list-style: decimal; }
+.ai-message-markdown-list-item { padding-left: 0.15em; }
+.ai-message-markdown-list-item + .ai-message-markdown-list-item { margin-top: 0.22em; }
+.ai-message-markdown-list-item > .ai-message-markdown-list { margin-top: 0.25em; margin-bottom: 0.25em; }
+.ai-message-markdown-blockquote {
+ margin: 0.75em 0;
+ padding: 0.1em 0 0.1em 0.85em;
+ border-left: 2px solid color-mix(in srgb, currentColor 24%, transparent);
+ color: color-mix(in srgb, currentColor 74%, transparent);
+}
+.ai-message-markdown-code {
+ padding: 0.12em 0.34em;
+ border-radius: var(--r-sm);
+ background: color-mix(in srgb, currentColor 9%, transparent);
+ color: inherit;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.88em;
+}
+.ai-message-markdown-pre {
+ margin: 0.8em 0;
+ padding: 0.8em 0.9em;
+ overflow-x: auto;
+ border-radius: var(--r-sm);
+ background: color-mix(in srgb, var(--foreground) 7%, var(--background));
+ color: color-mix(in srgb, var(--foreground) 86%, transparent);
+ font-size: 0.84em;
+ line-height: 1.55;
+}
+.ai-message-markdown-pre .ai-message-markdown-code {
+ padding: 0;
+ background: transparent;
+ font-size: inherit;
+}
+.ai-message-markdown-link {
+ color: var(--primary);
+ text-decoration: underline;
+ text-decoration-color: color-mix(in srgb, currentColor 38%, transparent);
+ text-underline-offset: 2px;
+}
+.ai-message-markdown-link:hover { text-decoration-color: currentColor; }
+.ai-message-markdown-rule {
+ margin: 1em 0;
+ border: 0;
+ border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
+}
+.ai-message-markdown-table-wrap {
+ max-width: 100%;
+ margin: 0.8em 0;
+ overflow-x: auto;
+}
+.ai-message-markdown-table {
+ width: 100%;
+ min-width: 320px;
+ border-collapse: collapse;
+ font-size: 0.92em;
+}
+.ai-message-markdown-table-cell {
+ padding: 0.48em 0.65em;
+ border-bottom: 1px solid color-mix(in srgb, currentColor 12%, transparent);
+ text-align: left;
+ vertical-align: top;
+}
+.ai-message-markdown-table-header {
+ color: color-mix(in srgb, currentColor 82%, transparent);
+ font-weight: 620;
+ background: color-mix(in srgb, currentColor 5%, transparent);
+}
+.ai-message-markdown-task {
+ width: 0.9em;
+ height: 0.9em;
+ margin: 0 0.35em 0 0;
+ vertical-align: -0.08em;
+ accent-color: var(--primary);
+}