diff --git a/frontend/package.json b/frontend/package.json index e02a8eb..8fa7393 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@milkdown/vue": "^7.22.0", "@vueuse/core": "^14.0.0", "codemirror": "^6.0.0", + "dompurify": "^3.4.14", "marked": "^15.0.0", "pinia": "^4.0.0", "vue": "^3.5.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fa6908b..87bc7a4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: codemirror: specifier: ^6.0.0 version: 6.0.2 + dompurify: + specifier: ^3.4.14 + version: 3.4.14 marked: specifier: ^15.0.0 version: 15.0.12 @@ -829,11 +832,6 @@ packages: '@volar/typescript@2.4.15': resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true '@vue-macros/common@3.1.4': resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} @@ -2608,13 +2606,11 @@ snapshots: '@volar/source-map@2.4.15': {} - '@volar/typescript@2.4.15(typescript@5.9.3)': + '@volar/typescript@2.4.15': dependencies: '@volar/language-core': 2.4.15 path-browserify: 1.0.1 vscode-uri: 3.2.0 - optionalDependencies: - typescript: 5.9.3 '@vue-macros/common@3.1.4(vue@3.5.41(typescript@5.9.3))': dependencies: @@ -3652,7 +3648,7 @@ snapshots: vue-tsc@2.2.12(typescript@5.9.3): dependencies: - '@volar/typescript': 2.4.15(typescript@5.9.3) + '@volar/typescript': 2.4.15 '@vue/language-core': 2.2.12(typescript@5.9.3) typescript: 5.9.3 diff --git a/frontend/src/contracts/index.ts b/frontend/src/contracts/index.ts index 23c9008..aaba2a7 100644 --- a/frontend/src/contracts/index.ts +++ b/frontend/src/contracts/index.ts @@ -75,6 +75,8 @@ export interface ChatMessage { created_at: string citations?: Citation[] tool_calls?: ToolCall[] + thinking?: string + usage?: TokenUsage } export interface Citation { @@ -99,6 +101,7 @@ export type ModelEventType = | 'ToolCallDelta' | 'ToolCallEnd' | 'Usage' + | 'Citation' | 'Error' | 'Done' diff --git a/frontend/src/features/chat/ChatView.vue b/frontend/src/features/chat/ChatView.vue index 833f641..2846e60 100644 --- a/frontend/src/features/chat/ChatView.vue +++ b/frontend/src/features/chat/ChatView.vue @@ -7,6 +7,7 @@ import { useEditorStore } from '@/stores/editor' import { useProviderStore } from '@/stores/provider' import { useSkillStore } from '@/stores/skill' import { useWorkspaceStore } from '@/stores/workspace' +import { renderMarkdown } from '@/utils/markdown' const chatStore = useChatStore() const providerStore = useProviderStore() @@ -67,13 +68,17 @@ async function openCitation(citation: Citation) {
{{ message.role === 'user' ? '你' : 'AI' }}
-
{{ message.content || (chatStore.isStreaming ? '正在思考…' : '') }}
+
思考过程

{{ message.thinking }}

+
+
正在思考…
+
{{ call.status }}{{ call.name }}
{{ JSON.stringify(call.parameters, null, 2) }}
+ Token {{ message.usage.total_tokens }}(输入 {{ message.usage.input_tokens }} / 输出 {{ message.usage.output_tokens }})
@@ -99,6 +104,18 @@ async function openCitation(citation: Citation) { .avatar { display: grid; place-items: center; width: 34px; height: 34px; border-radius: var(--radius-full); background: var(--color-background-tertiary); font-weight: 700; } .assistant .avatar { background: var(--color-accent-soft); color: var(--color-accent-primary); } .message-content { white-space: pre-wrap; line-height: var(--line-height-relaxed); } +.markdown-content { white-space: normal; user-select: text; } +.markdown-content :deep(p), .markdown-content :deep(ul), .markdown-content :deep(ol), .markdown-content :deep(pre), .markdown-content :deep(blockquote) { margin: .65em 0; } +.markdown-content :deep(h1), .markdown-content :deep(h2), .markdown-content :deep(h3) { margin: 1em 0 .5em; line-height: var(--line-height-tight); } +.markdown-content :deep(ul) { padding-left: 1.5em; list-style: disc; }.markdown-content :deep(ol) { padding-left: 1.5em; list-style: decimal; } +.markdown-content :deep(pre) { overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-background-secondary); } +.markdown-content :deep(code) { padding: .1em .3em; border-radius: var(--radius-sm); background: var(--color-background-tertiary); font-family: var(--font-ui-mono); }.markdown-content :deep(pre code) { padding: 0; background: transparent; } +.markdown-content :deep(blockquote) { padding-left: 1em; border-left: 3px solid var(--color-accent-primary); color: var(--color-text-secondary); } +.markdown-content :deep(table) { width: 100%; margin: .65em 0; border-collapse: collapse; }.markdown-content :deep(th), .markdown-content :deep(td) { padding: .45em .65em; border: 1px solid var(--color-border-default); text-align: left; } +.markdown-content :deep(img) { max-width: 100%; }.markdown-content :deep(hr) { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border-default); } +.thinking { margin-bottom: var(--space-sm); color: var(--color-text-secondary); }.thinking p { margin-top: var(--space-sm); white-space: pre-wrap; } +.tool-calls { display: grid; gap: var(--space-sm); margin-top: var(--space-md); }.tool-calls .item-card { display: grid; gap: var(--space-xs); }.tool-calls pre { overflow: auto; font-size: var(--font-size-xs); } +.usage { display: block; margin-top: var(--space-xs); color: var(--color-text-tertiary); } .message time { display: block; margin-top: var(--space-sm); color: var(--color-text-tertiary); font-size: var(--font-size-xs); } .citations { display: grid; gap: var(--space-sm); margin-top: var(--space-md); } .citation-card { display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-sm); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); text-align: left; } diff --git a/frontend/src/features/editor/EditorPane.vue b/frontend/src/features/editor/EditorPane.vue index f575ee5..6da29c9 100644 --- a/frontend/src/features/editor/EditorPane.vue +++ b/frontend/src/features/editor/EditorPane.vue @@ -1,18 +1,27 @@