@@ -39,3 +39,4 @@ __screenshots__/
|
||||
*.timestamp-*-*.mjs
|
||||
|
||||
AGENTS.md
|
||||
.agents
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"axios": "^1.19.0",
|
||||
"echarts": "^6.1.0",
|
||||
"element-plus": "^2.14.4",
|
||||
"markstream-vue": "^2.0.6",
|
||||
"mockjs": "^1.1.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"pinia": "^4.0.2",
|
||||
|
||||
Generated
+145
-7
@@ -13,13 +13,16 @@ importers:
|
||||
version: 4.3.3
|
||||
axios:
|
||||
specifier: ^1.19.0
|
||||
version: 1.19.0
|
||||
version: 1.19.0(debug@4.4.3)
|
||||
echarts:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
element-plus:
|
||||
specifier: ^2.14.4
|
||||
version: 2.14.4(vue@3.5.41(typescript@6.0.3))
|
||||
markstream-vue:
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6(vue@3.5.41(typescript@6.0.3))
|
||||
mockjs:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
@@ -291,6 +294,9 @@ packages:
|
||||
'@cfcs/core@0.0.6':
|
||||
resolution: {integrity: sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==}
|
||||
|
||||
'@chenglou/pretext@0.0.8':
|
||||
resolution: {integrity: sha512-yqm2GMxnPI7VHcHwe84P8ZF0JK/2d2DMKPqMN+s95jQhwDMYYXKVFVJUMEaVWckQStdsjdLav/0Vu+d9YbtGxA==}
|
||||
|
||||
'@ctrl/tinycolor@4.2.0':
|
||||
resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -845,6 +851,11 @@ packages:
|
||||
|
||||
'@volar/typescript@2.4.28':
|
||||
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@vue-macros/common@3.1.4':
|
||||
resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==}
|
||||
@@ -1149,6 +1160,10 @@ packages:
|
||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@8.0.0:
|
||||
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
error-stack-parser-es@1.0.5:
|
||||
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
||||
|
||||
@@ -1622,6 +1637,9 @@ packages:
|
||||
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
linkify-it@6.1.0:
|
||||
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||
|
||||
local-pkg@1.2.1:
|
||||
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -1656,15 +1674,67 @@ packages:
|
||||
magic-string@1.2.0:
|
||||
resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==}
|
||||
|
||||
markdown-it-container@4.0.0:
|
||||
resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==}
|
||||
|
||||
markdown-it-footnote@4.0.0:
|
||||
resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==}
|
||||
|
||||
markdown-it-ins@4.0.0:
|
||||
resolution: {integrity: sha512-sWbjK2DprrkINE4oYDhHdCijGT+MIDhEupjSHLXe5UXeVr5qmVxs/nTUVtgi0Oh/qtF+QKV0tNWDhQBEPxiMew==}
|
||||
|
||||
markdown-it-mark@4.0.0:
|
||||
resolution: {integrity: sha512-YLhzaOsU9THO/cal0lUjfMjrqSMPjjyjChYM7oyj4DnyaXEzA8gnW6cVJeyCrCVeyesrY2PlEdUYJSPFYL4Nkg==}
|
||||
|
||||
markdown-it-sup@2.0.0:
|
||||
resolution: {integrity: sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA==}
|
||||
|
||||
markdown-it-task-checkbox@1.0.6:
|
||||
resolution: {integrity: sha512-7pxkHuvqTOu3iwVGmDPeYjQg+AIS9VQxzyLP9JCg9lBjgPAJXGEkChK6A2iFuj3tS0GV3HG2u5AMNhcQqwxpJw==}
|
||||
|
||||
markdown-it-ts@1.1.0:
|
||||
resolution: {integrity: sha512-ycYoj3jeP0zHW9u4vxtSuuq9JaitIRxxYbRtns9D4fDR5mzSpFnb855KclFBGiKFJxVi+8EkMB938oZpdDVc0A==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
marked@14.0.0:
|
||||
resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
markstream-core@2.0.6:
|
||||
resolution: {integrity: sha512-kSGNAoTArTu9LxwyfZI+fuxanjhxwPtlW+yJM+5IJhW2jtsH9p7mSpApHC6YtJLnzeXCGS2XlqIoivzIdzy6iA==}
|
||||
|
||||
markstream-vue@2.0.6:
|
||||
resolution: {integrity: sha512-87skNadimBbQFZbb6kNgIrCpXr5x63YW4MYkRbWHh/g/1+BHJ2t3iIzEsSIIES1yn29STf4ZLw1ruepZqXZeXg==}
|
||||
peerDependencies:
|
||||
'@antv/infographic': ^0.2.3
|
||||
'@terrastruct/d2': '>=0.1.33'
|
||||
katex: '>=0.16.22'
|
||||
mermaid: '>=11'
|
||||
stream-diffs: '>=0.0.2'
|
||||
vue: '>=3.0.0'
|
||||
vue-i18n: '>=9'
|
||||
peerDependenciesMeta:
|
||||
'@antv/infographic':
|
||||
optional: true
|
||||
'@terrastruct/d2':
|
||||
optional: true
|
||||
katex:
|
||||
optional: true
|
||||
mermaid:
|
||||
optional: true
|
||||
stream-diffs:
|
||||
optional: true
|
||||
vue-i18n:
|
||||
optional: true
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mdurl@2.1.0:
|
||||
resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==}
|
||||
|
||||
memoize-one@6.0.0:
|
||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||
|
||||
@@ -1862,6 +1932,10 @@ packages:
|
||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
punycode.js@2.3.1:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1940,6 +2014,9 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
stream-markdown-parser@1.2.12:
|
||||
resolution: {integrity: sha512-gT6GWlOfNTVGBEM66Me1DZ1cZZ/Mm4tcpJ4br8Zvz2CQhiWueEv+xFBBHCwls/XG4E63DygTd890rDdV+D6JxA==}
|
||||
|
||||
strip-literal@4.0.0:
|
||||
resolution: {integrity: sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==}
|
||||
|
||||
@@ -1987,6 +2064,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uc.micro@3.0.0:
|
||||
resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==}
|
||||
|
||||
ufo@1.6.4:
|
||||
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
||||
|
||||
@@ -2101,6 +2181,7 @@ packages:
|
||||
engines: {node: '>=v14.21.3'}
|
||||
peerDependencies:
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
vue: '*'
|
||||
|
||||
vite-plugin-vue-inspector@6.0.0:
|
||||
resolution: {integrity: sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==}
|
||||
@@ -2473,6 +2554,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@egjs/component': 3.0.5
|
||||
|
||||
'@chenglou/pretext@0.0.8': {}
|
||||
|
||||
'@ctrl/tinycolor@4.2.0': {}
|
||||
|
||||
'@daybrush/utils@1.13.0': {}
|
||||
@@ -2907,11 +2990,13 @@ snapshots:
|
||||
|
||||
'@volar/source-map@2.4.28': {}
|
||||
|
||||
'@volar/typescript@2.4.28':
|
||||
'@volar/typescript@2.4.28(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.28
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.1.0
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@vue-macros/common@3.1.4(vue@3.5.41(typescript@6.0.3))':
|
||||
dependencies:
|
||||
@@ -3106,9 +3191,9 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.19.0:
|
||||
axios@1.19.0(debug@4.4.3):
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0
|
||||
follow-redirects: 1.16.0(debug@4.4.3)
|
||||
form-data: 4.0.6
|
||||
https-proxy-agent: 5.0.1
|
||||
proxy-from-env: 2.1.0
|
||||
@@ -3281,6 +3366,8 @@ snapshots:
|
||||
|
||||
entities@7.0.1: {}
|
||||
|
||||
entities@8.0.0: {}
|
||||
|
||||
error-stack-parser-es@1.0.5: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
@@ -3442,7 +3529,9 @@ snapshots:
|
||||
|
||||
flatted@3.4.4: {}
|
||||
|
||||
follow-redirects@1.16.0: {}
|
||||
follow-redirects@1.16.0(debug@4.4.3):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3
|
||||
|
||||
form-data@4.0.6:
|
||||
dependencies:
|
||||
@@ -3683,6 +3772,10 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.33.0
|
||||
lightningcss-win32-x64-msvc: 1.33.0
|
||||
|
||||
linkify-it@6.1.0:
|
||||
dependencies:
|
||||
uc.micro: 3.0.0
|
||||
|
||||
local-pkg@1.2.1:
|
||||
dependencies:
|
||||
mlly: 1.8.2
|
||||
@@ -3719,10 +3812,41 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
markdown-it-container@4.0.0: {}
|
||||
|
||||
markdown-it-footnote@4.0.0: {}
|
||||
|
||||
markdown-it-ins@4.0.0: {}
|
||||
|
||||
markdown-it-mark@4.0.0: {}
|
||||
|
||||
markdown-it-sup@2.0.0: {}
|
||||
|
||||
markdown-it-task-checkbox@1.0.6: {}
|
||||
|
||||
markdown-it-ts@1.1.0:
|
||||
dependencies:
|
||||
entities: 8.0.0
|
||||
linkify-it: 6.1.0
|
||||
mdurl: 2.1.0
|
||||
punycode.js: 2.3.1
|
||||
|
||||
marked@14.0.0: {}
|
||||
|
||||
markstream-core@2.0.6: {}
|
||||
|
||||
markstream-vue@2.0.6(vue@3.5.41(typescript@6.0.3)):
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.8
|
||||
'@floating-ui/dom': 1.8.0
|
||||
markstream-core: 2.0.6
|
||||
stream-markdown-parser: 1.2.12
|
||||
vue: 3.5.41(typescript@6.0.3)
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdurl@2.1.0: {}
|
||||
|
||||
memoize-one@6.0.0: {}
|
||||
|
||||
memorystream@0.3.1: {}
|
||||
@@ -3916,6 +4040,8 @@ snapshots:
|
||||
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
quansync@0.2.11: {}
|
||||
@@ -4017,6 +4143,16 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stream-markdown-parser@1.2.12:
|
||||
dependencies:
|
||||
markdown-it-container: 4.0.0
|
||||
markdown-it-footnote: 4.0.0
|
||||
markdown-it-ins: 4.0.0
|
||||
markdown-it-mark: 4.0.0
|
||||
markdown-it-sup: 2.0.0
|
||||
markdown-it-task-checkbox: 1.0.6
|
||||
markdown-it-ts: 1.1.0
|
||||
|
||||
strip-literal@4.0.0:
|
||||
dependencies:
|
||||
js-tokens: 10.0.0
|
||||
@@ -4059,6 +4195,8 @@ snapshots:
|
||||
|
||||
typescript@6.0.3: {}
|
||||
|
||||
uc.micro@3.0.0: {}
|
||||
|
||||
ufo@1.6.4: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
@@ -4194,10 +4332,10 @@ snapshots:
|
||||
vite: 8.2.1(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)
|
||||
vite-plugin-inspect: 11.4.1(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 6.0.0(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))
|
||||
vue: 3.5.41(typescript@6.0.3)
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@6.0.0(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
@@ -4284,7 +4422,7 @@ snapshots:
|
||||
|
||||
vue-tsc@3.3.9(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.28
|
||||
'@volar/typescript': 2.4.28(typescript@6.0.3)
|
||||
'@vue/language-core': 3.3.9
|
||||
typescript: 6.0.3
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import MaterialPanel from '@/editor/panels/material/index.vue'
|
||||
import LayerPanel from '@/editor/panels/layer/index.vue'
|
||||
import CanvasRoot from '@/editor/canvas/index.vue'
|
||||
import PropertyPanel from '@/editor/panels/property/index.vue'
|
||||
import ChatPanel from '@/editor/panels/chat/index.vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { getPublishedPage } from '@/utils/publish'
|
||||
@@ -32,6 +33,7 @@ provide('dataSource', dataSource)
|
||||
const materialWidth = computed(() => (panelVisible.value.material ? '260px' : '0'))
|
||||
const propertyWidth = computed(() => (panelVisible.value.property ? '160px' : '0'))
|
||||
const layerWidth = computed(() => (panelVisible.value.layer ? '260px' : '0'))
|
||||
const chatWidth = computed(() => (panelVisible.value.chat ? '360px' : '0'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,7 +41,7 @@ const layerWidth = computed(() => (panelVisible.value.layer ? '260px' : '0'))
|
||||
<header class="flex items-center justify-between h-14 border-b border-white/20 px-5">
|
||||
<div class="w-75"><ToolbarLeft /></div>
|
||||
<div class="">工作区</div>
|
||||
<div class="w-75"><ToolbarRight /></div>
|
||||
<div class="w-80"><ToolbarRight /></div>
|
||||
</header>
|
||||
<main class="h-[calc(100%-56px)] flex">
|
||||
<!-- 物料 -->
|
||||
@@ -64,6 +66,11 @@ const layerWidth = computed(() => (panelVisible.value.layer ? '260px' : '0'))
|
||||
>
|
||||
属性
|
||||
</PropertyPanel>
|
||||
<!-- AI 对话 -->
|
||||
<ChatPanel
|
||||
class="w-[360px] min-h-0 border-l border-white/20 overflow-hidden transition-all"
|
||||
:style="{ width: chatWidth }"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { PageSchema } from '@/schema/page'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
export type AgentPatch = {
|
||||
op: string
|
||||
node_id?: string
|
||||
payload?: object
|
||||
}
|
||||
|
||||
export type AgentChatMessage = {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
final: boolean
|
||||
error?: boolean
|
||||
patches: AgentPatch[]
|
||||
}
|
||||
|
||||
type InstructPlan = {
|
||||
summary?: string
|
||||
patches?: AgentPatch[]
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
type StreamFrame = {
|
||||
choices?: Array<{
|
||||
delta?: { content?: string }
|
||||
finish_reason?: string | null
|
||||
}>
|
||||
progress?: {
|
||||
type?: string
|
||||
step?: string
|
||||
status?: string
|
||||
patch_count?: number
|
||||
}
|
||||
schema?: PageSchema
|
||||
plan?: InstructPlan
|
||||
warnings?: string[]
|
||||
ok?: boolean
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
/** 拆一帧 SSE:`data: ...` 或 `[DONE]` */
|
||||
const parseSseBlock = (block: string) => {
|
||||
const line = block.split('\n').find((item) => item.startsWith('data:'))
|
||||
if (!line) return null
|
||||
const raw = line.slice('data:'.length).trim()
|
||||
if (raw === '[DONE]') return '[DONE]' as const
|
||||
try {
|
||||
return JSON.parse(raw) as StreamFrame
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isAbortError = (err: unknown) =>
|
||||
(err instanceof DOMException && err.name === 'AbortError') ||
|
||||
(err instanceof Error && err.name === 'AbortError')
|
||||
|
||||
export const useAgentChat = () => {
|
||||
const editor = useEditorStore()
|
||||
|
||||
// 对话
|
||||
const messages = ref<AgentChatMessage[]>([])
|
||||
const running = ref(false)
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
const failAssistant = (assistant: AgentChatMessage, text: string) => {
|
||||
assistant.error = true
|
||||
assistant.final = true
|
||||
if (!assistant.content) assistant.content = text
|
||||
}
|
||||
|
||||
/** token 拼进 content;有 progress 才记;stop 帧根上才 setPage / 收 patches */
|
||||
const handleFrame = (assistant: AgentChatMessage, frame: StreamFrame) => {
|
||||
const choice = frame.choices?.[0]
|
||||
const token = choice?.delta?.content
|
||||
if (token) assistant.content += token
|
||||
if (choice?.finish_reason !== 'stop') return
|
||||
if (frame.ok === false) {
|
||||
const text = frame.errors?.join(';') || '指令失败'
|
||||
failAssistant(assistant, text)
|
||||
ElMessage.error(text)
|
||||
return
|
||||
}
|
||||
assistant.patches = frame.plan?.patches ?? []
|
||||
assistant.final = true
|
||||
if (frame.schema) editor.setPage(frame.schema)
|
||||
const warnings = frame.warnings?.length ? frame.warnings : frame.plan?.warnings
|
||||
if (warnings?.length) ElMessage.warning(warnings.join(';'))
|
||||
}
|
||||
|
||||
const consumeStream = async (
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
assistant: AgentChatMessage,
|
||||
) => {
|
||||
const decoder = new TextDecoder()
|
||||
let buf = ''
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buf += decoder.decode(value, { stream: true })
|
||||
const parts = buf.split('\n\n')
|
||||
buf = parts.pop() || ''
|
||||
for (const block of parts) {
|
||||
const frame = parseSseBlock(block)
|
||||
if (frame == null || frame === '[DONE]') continue
|
||||
handleFrame(assistant, frame)
|
||||
}
|
||||
}
|
||||
if (buf.trim()) {
|
||||
const frame = parseSseBlock(buf)
|
||||
if (frame != null && frame !== '[DONE]') handleFrame(assistant, frame)
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /agent/instruct:只渲染 content token 和 stop 里的 patches */
|
||||
const send = async (instruction: string) => {
|
||||
const text = instruction.trim()
|
||||
if (!text || running.value) return
|
||||
|
||||
abortController?.abort()
|
||||
abortController = new AbortController()
|
||||
|
||||
const userMsg: AgentChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
final: true,
|
||||
patches: [],
|
||||
}
|
||||
const live = reactive<AgentChatMessage>({
|
||||
id: crypto.randomUUID(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
final: false,
|
||||
patches: [],
|
||||
})
|
||||
messages.value.push(userMsg, live)
|
||||
running.value = true
|
||||
|
||||
try {
|
||||
const res = await fetch('/agent/instruct', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: abortController.signal,
|
||||
body: JSON.stringify({
|
||||
instruction: text,
|
||||
schema: editor.page,
|
||||
selected_node_ids: editor.selectedNodeIds,
|
||||
stream: true,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
let detail = `指令失败 (${res.status})`
|
||||
try {
|
||||
const body = await res.json()
|
||||
if (body.errors?.length) detail = body.errors.join(';')
|
||||
} catch {
|
||||
/* 非 JSON 错误体 */
|
||||
}
|
||||
throw new Error(detail)
|
||||
}
|
||||
if (!res.body) throw new Error('没有响应体')
|
||||
await consumeStream(res.body.getReader(), live)
|
||||
if (!live.final) failAssistant(live, '没有收到结果')
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) return
|
||||
const text = err instanceof Error ? err.message : '指令失败'
|
||||
failAssistant(live, text)
|
||||
ElMessage.error(text)
|
||||
} finally {
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
abortController?.abort()
|
||||
abortController = null
|
||||
messages.value = []
|
||||
running.value = false
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
abortController?.abort()
|
||||
})
|
||||
|
||||
return { messages, running, send, clear }
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<!-- AI 对话面板 -->
|
||||
<script setup lang="ts">
|
||||
import MarkdownRender from 'markstream-vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useStickToBottom } from 'markstream-vue/utils'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useAgentChat } from '@/editor/panels/chat/composables/useAgentChat'
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatPanel',
|
||||
})
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { selectedNodeIds } = storeToRefs(editorStore)
|
||||
const { messages, running, send, clear } = useAgentChat()
|
||||
|
||||
// 输入
|
||||
const draft = ref('')
|
||||
const scrollRootRef = useTemplateRef<HTMLElement>('scrollRootRef')
|
||||
const contentRootRef = useTemplateRef<HTMLElement>('contentRootRef')
|
||||
const { scheduleScrollToBottom } = useStickToBottom(scrollRootRef, contentRootRef)
|
||||
|
||||
const selectedHint = computed(() => {
|
||||
const count = selectedNodeIds.value.length
|
||||
return count ? `已选 ${count} 个节点` : '未选中节点'
|
||||
})
|
||||
|
||||
const canSend = computed(() => Boolean(draft.value.trim()) && !running.value)
|
||||
|
||||
watch(
|
||||
messages,
|
||||
async () => {
|
||||
await nextTick()
|
||||
scheduleScrollToBottom()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
/** Enter 发送,Shift+Enter 换行 */
|
||||
const onKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
onSend()
|
||||
}
|
||||
}
|
||||
|
||||
const onSend = () => {
|
||||
if (!canSend.value) return
|
||||
const text = draft.value
|
||||
draft.value = ''
|
||||
send(text)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full min-h-0 min-w-0 flex flex-col select-text">
|
||||
<div class="h-10 shrink-0 flex items-center justify-between px-3 border-b border-white/20">
|
||||
<span class="text-sm text-(--el-text-color-regular)">AI 助手</span>
|
||||
<span
|
||||
v-if="messages.length"
|
||||
class="border p-1 rounded-lg cursor-pointer"
|
||||
title="清空对话"
|
||||
@click="clear"
|
||||
>
|
||||
<Icon icon="solar:trash-bin-minimalistic-linear" width="14" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ref="scrollRootRef" class="flex-1 min-h-0">
|
||||
<div
|
||||
ref="contentRootRef"
|
||||
class="h-full flex flex-col gap-3 p-3 overflow-auto"
|
||||
:class="{ 'items-center justify-center': !messages.length }"
|
||||
>
|
||||
<div
|
||||
v-if="!messages.length"
|
||||
class="flex flex-col items-center justify-center gap-2 py-10 text-sm text-white/50"
|
||||
>
|
||||
<Icon icon="solar:chat-round-dots-bold" width="28" />
|
||||
<p>用自然语言改画布</p>
|
||||
<p class="text-xs text-white/35">试试:加一张柱状图</p>
|
||||
</div>
|
||||
|
||||
<template v-for="msg in messages" :key="msg.id">
|
||||
<div v-if="msg.role === 'user'" class="flex justify-end">
|
||||
<div
|
||||
class="max-w-[90%] rounded-lg bg-white/10 px-2.5 py-2 text-sm text-(--el-text-color-regular) whitespace-pre-wrap break-words"
|
||||
>
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex justify-start">
|
||||
<div
|
||||
class="max-w-[90%] min-w-0 rounded-lg bg-white/5 px-2.5 py-2 text-sm text-(--el-text-color-regular)"
|
||||
>
|
||||
<Icon
|
||||
v-if="!msg.content && !msg.patches.length"
|
||||
icon="svg-spinners:3-dots-fade"
|
||||
width="20"
|
||||
class="text-white/50"
|
||||
/>
|
||||
<div v-if="msg.content" class="markdownRender">
|
||||
<MarkdownRender
|
||||
mode="chat"
|
||||
:content="msg.content"
|
||||
:final="msg.final"
|
||||
:is-dark="true"
|
||||
:fade="false"
|
||||
:typewriter="false"
|
||||
:smooth-streaming="msg.final ? false : 'auto'"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.patches.length"
|
||||
class="mt-2 flex flex-col gap-1 font-mono text-xs text-white/45"
|
||||
>
|
||||
<div v-for="(patch, index) in msg.patches" :key="index">
|
||||
{{ patch.op }}
|
||||
<span v-if="patch.node_id" class="text-white/25"> {{ patch.node_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 border-t border-white/20 p-2.5">
|
||||
<div class="rounded-lg border border-white/20 px-2 py-1.5">
|
||||
<textarea
|
||||
v-model="draft"
|
||||
rows="3"
|
||||
class="w-full resize-none bg-transparent text-sm text-(--el-text-color-regular) outline-none placeholder:text-white/35"
|
||||
placeholder="描述你想改的画布,Enter 发送"
|
||||
:disabled="running"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
<div class="flex items-center justify-between pt-1">
|
||||
<span class="text-xs text-white/35">{{ selectedHint }}</span>
|
||||
<span
|
||||
class="border p-1 rounded-lg cursor-pointer"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !canSend }"
|
||||
title="发送"
|
||||
@click="onSend"
|
||||
>
|
||||
<Icon icon="solar:plain-2-bold" width="14" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.markdownRender .paragraph-node) {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- 右侧工具栏 -->
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -16,7 +17,7 @@ const MonacoEditor = defineAsyncComponent({
|
||||
|
||||
const router = useRouter()
|
||||
const editorStore = useEditorStore()
|
||||
const { page } = storeToRefs(editorStore)
|
||||
const { page, panelVisible } = storeToRefs(editorStore)
|
||||
// 全局数据源
|
||||
const dataSourceVisible = ref(false)
|
||||
const visible = ref(false)
|
||||
@@ -125,6 +126,14 @@ const onPublish = () => {
|
||||
<span class="border p-1 rounded-lg cursor-pointer" @click="onExport" title="导出">
|
||||
<Icon icon="lets-icons:export"> </Icon>
|
||||
</span>
|
||||
<span
|
||||
class="border p-1 rounded-lg cursor-pointer"
|
||||
:class="{ 'bg-white/20': panelVisible.chat }"
|
||||
title="AI 对话"
|
||||
@click="panelVisible.chat = !panelVisible.chat"
|
||||
>
|
||||
<Icon icon="solar:chat-round-dots-bold" />
|
||||
</span>
|
||||
|
||||
<input ref="inputRef" type="file" class="hidden" @change="onFileChange" />
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
material: true,
|
||||
layer: true,
|
||||
property: true,
|
||||
chat: true,
|
||||
})
|
||||
|
||||
// 页面数据
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'markstream-vue/index.css' layer(components);
|
||||
|
||||
@supports not selector(::-webkit-scrollbar) {
|
||||
* {
|
||||
|
||||
@@ -44,5 +44,11 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
allowedHosts: ['www.qflink.xyz'],
|
||||
proxy: {
|
||||
'/agent': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user