From d5fdbb0cb38fa193227b2bb4d1babfb434184939 Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Thu, 26 Mar 2026 14:43:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BB=A3=E7=A0=81=E7=B2=BE?= =?UTF-8?q?=E7=AE=80=E4=B8=8EUI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/services/chat.py | 10 + docs/Design.md | 9 +- frontend/package-lock.json | 11 + frontend/package.json | 1 + frontend/src/App.vue | 163 ++---- frontend/src/api/index.js | 16 - frontend/src/components/ChatView.vue | 114 ++-- frontend/src/components/MessageBubble.vue | 83 ++- frontend/src/components/MessageInput.vue | 2 +- frontend/src/components/ProcessBlock.vue | 601 +++++++++------------ frontend/src/components/ProjectManager.vue | 63 ++- frontend/src/components/SettingsPanel.vue | 68 +-- frontend/src/components/Sidebar.vue | 15 +- frontend/src/components/StatsPanel.vue | 564 ++++++++++++++----- frontend/src/composables/useTheme.js | 17 +- frontend/src/styles/global.css | 330 ++++++++--- frontend/src/styles/highlight.css | 72 ++- frontend/src/utils/markdown.js | 88 ++- 18 files changed, 1345 insertions(+), 882 deletions(-) diff --git a/backend/services/chat.py b/backend/services/chat.py index 7ce3943..efd8c13 100644 --- a/backend/services/chat.py +++ b/backend/services/chat.py @@ -127,6 +127,11 @@ class ChatService: yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'thinking', 'content': full_thinking}, ensure_ascii=False)}\n\n" step_index += 1 + # Send text as a step if exists (text before tool calls) + if full_content: + yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'text', 'content': full_content}, ensure_ascii=False)}\n\n" + step_index += 1 + # Also send legacy tool_calls event for backward compatibility yield f"event: tool_calls\ndata: {json.dumps({'calls': tool_calls_list}, ensure_ascii=False)}\n\n" @@ -169,6 +174,11 @@ class ChatService: yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'thinking', 'content': full_thinking}, ensure_ascii=False)}\n\n" step_index += 1 + # Send text as a step if exists + if full_content: + yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'text', 'content': full_content}, ensure_ascii=False)}\n\n" + step_index += 1 + suggested_title = None with app.app_context(): # Build content JSON diff --git a/docs/Design.md b/docs/Design.md index afe7f93..2289f0c 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -402,7 +402,7 @@ def process_tool_calls(self, tool_calls, context=None): | `message` | 回复内容的增量片段 | | `tool_calls` | 工具调用信息 | | `tool_result` | 工具执行结果 | -| `process_step` | 处理步骤(按顺序:thinking/tool_call/tool_result),支持交替显示 | +| `process_step` | 处理步骤(按顺序:thinking/text/tool_call/tool_result),支持穿插显示 | | `error` | 错误信息 | | `done` | 回复结束,携带 message_id 和 token_count | @@ -412,11 +412,14 @@ def process_tool_calls(self, tool_calls, context=None): // 思考过程 {"index": 0, "type": "thinking", "content": "完整思考内容..."} +// 回复文本(可穿插在任意步骤之间) +{"index": 1, "type": "text", "content": "回复文本内容..."} + // 工具调用 -{"index": 1, "type": "tool_call", "id": "call_abc123", "name": "web_search", "arguments": "{\"query\": \"...\"}"} +{"index": 2, "type": "tool_call", "id": "call_abc123", "name": "web_search", "arguments": "{\"query\": \"...\"}"} // 工具返回 -{"index": 2, "type": "tool_result", "id": "call_abc123", "name": "web_search", "content": "{\"success\": true, ...}", "skipped": false} +{"index": 3, "type": "tool_result", "id": "call_abc123", "name": "web_search", "content": "{\"success\": true, ...}", "skipped": false} ``` 字段说明: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ceef20e..2b6fc93 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "highlight.js": "^11.10.0", "katex": "^0.16.40", "marked": "^15.0.0", + "marked-highlight": "^2.2.3", "vue": "^3.4.0" }, "devDependencies": { @@ -1151,6 +1152,7 @@ "resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.12.tgz", "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -1158,6 +1160,15 @@ "node": ">= 18" } }, + "node_modules/marked-highlight": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/marked-highlight/-/marked-highlight-2.2.3.tgz", + "integrity": "sha512-FCfZRxW/msZAiasCML4isYpxyQWKEEx44vOgdn5Kloae+Qc3q4XR7WjpKKf8oMLk7JP9ZCRd2vhtclJFdwxlWQ==", + "license": "MIT", + "peerDependencies": { + "marked": ">=4 <18" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index 176b2ef..e368977 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "highlight.js": "^11.10.0", "katex": "^0.16.40", "marked": "^15.0.0", + "marked-highlight": "^2.2.3", "vue": "^3.4.0" }, "devDependencies": { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5e74dd1..41d151a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,7 +14,6 @@ /> + + + + @@ -47,10 +56,9 @@ import { ref, computed, onMounted } from 'vue' import Sidebar from './components/Sidebar.vue' import ChatView from './components/ChatView.vue' import SettingsPanel from './components/SettingsPanel.vue' +import StatsPanel from './components/StatsPanel.vue' import { conversationApi, messageApi } from './api' -const chatViewRef = ref(null) - // -- Conversations state -- const conversations = ref([]) const currentConvId = ref(null) @@ -71,6 +79,15 @@ const streamThinking = ref('') const streamToolCalls = ref([]) const streamProcessSteps = ref([]) +function resetStreamState() { + streaming.value = false + streamContent.value = '' + streamThinking.value = '' + streamToolCalls.value = [] + streamProcessSteps.value = [] + currentStreamPromise = null +} + // 保存每个对话的流式状态 const streamStates = new Map() @@ -79,6 +96,7 @@ let currentStreamPromise = null // -- UI state -- const showSettings = ref(false) +const showStats = ref(false) const toolsEnabled = ref(localStorage.getItem('tools_enabled') !== 'false') // 默认开启 const currentProject = ref(null) // Current selected project @@ -267,21 +285,15 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) { token_count: data.token_count, created_at: new Date().toISOString(), }) - streamContent.value = '' - streamThinking.value = '' - streamToolCalls.value = [] - streamProcessSteps.value = [] + resetStreamState() if (updateConvList) { const idx = conversations.value.findIndex(c => c.id === convId) - if (idx > 0) { - const [conv] = conversations.value.splice(idx, 1) + if (idx >= 0) { + const conv = idx > 0 ? conversations.value.splice(idx, 1)[0] : conversations.value[0] conv.message_count = (conv.message_count || 0) + 2 if (data.suggested_title) conv.title = data.suggested_title - conversations.value.unshift(conv) - } else if (idx === 0) { - conversations.value[0].message_count = (conversations.value[0].message_count || 0) + 2 - if (data.suggested_title) conversations.value[0].title = data.suggested_title + if (idx > 0) conversations.value.unshift(conv) } } } else { @@ -301,12 +313,7 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) { onError(msg) { streamStates.delete(convId) if (currentConvId.value === convId) { - streaming.value = false - currentStreamPromise = null - streamContent.value = '' - streamThinking.value = '' - streamToolCalls.value = [] - streamProcessSteps.value = [] + resetStreamState() console.error('Stream error:', msg) } }, @@ -428,105 +435,29 @@ onMounted(() => { diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 815faff..b94c393 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -202,17 +202,6 @@ export const projectApi = { }) }, - get(projectId) { - return request(`/projects/${projectId}`) - }, - - update(projectId, data) { - return request(`/projects/${projectId}`, { - method: 'PUT', - body: data, - }) - }, - delete(projectId) { return request(`/projects/${projectId}`, { method: 'DELETE' }) }, @@ -223,9 +212,4 @@ export const projectApi = { body: data, }) }, - - listFiles(projectId, path = '') { - const params = path ? `?path=${encodeURIComponent(path)}` : '' - return request(`/projects/${projectId}/files${params}`) - }, } diff --git a/frontend/src/components/ChatView.vue b/frontend/src/components/ChatView.vue index 29b1fa1..eef1daf 100644 --- a/frontend/src/components/ChatView.vue +++ b/frontend/src/components/ChatView.vue @@ -10,10 +10,17 @@

{{ conversation.title || '新对话' }}

- {{ conversation.model }} + {{ formatModelName(conversation.model) }} 思考
+
-
+
@@ -80,11 +81,11 @@ @@ -91,7 +106,6 @@ function copyContent() { .message-bubble { display: flex; gap: 12px; - padding: 0; margin-bottom: 16px; width: 100%; } @@ -195,39 +209,6 @@ function copyContent() { transition: background 0.2s, border-color 0.2s; } -.message-content { -} - -.tool-result-content { - background: var(--bg-code); - border: 1px solid var(--border-light); - border-radius: 8px; - padding: 12px; - overflow: hidden; -} - -.tool-badge { - font-size: 11px; - color: var(--success-color); - font-weight: 600; - margin-bottom: 8px; - padding: 2px 8px; - background: var(--success-bg); - border-radius: 4px; - display: inline-block; -} - -.tool-result-content pre { - font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 12px; - line-height: 1.5; - color: var(--text-secondary); - margin: 0; - overflow-x: auto; - white-space: pre-wrap; - word-break: break-word; -} - .message-footer { display: flex; align-items: center; diff --git a/frontend/src/components/MessageInput.vue b/frontend/src/components/MessageInput.vue index a96d4af..c76d07f 100644 --- a/frontend/src/components/MessageInput.vue +++ b/frontend/src/components/MessageInput.vue @@ -27,7 +27,7 @@ diff --git a/frontend/src/components/ProcessBlock.vue b/frontend/src/components/ProcessBlock.vue index 31d0d74..eb6d00a 100644 --- a/frontend/src/components/ProcessBlock.vue +++ b/frontend/src/components/ProcessBlock.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/ProjectManager.vue b/frontend/src/components/ProjectManager.vue index bba4bc9..688348e 100644 --- a/frontend/src/components/ProjectManager.vue +++ b/frontend/src/components/ProjectManager.vue @@ -88,7 +88,15 @@
- +
+ + +
@@ -157,6 +165,26 @@ const uploadData = ref({ description: '', }) +async function selectFolder() { + try { + if ('showDirectoryPicker' in window) { + const dirHandle = await window.showDirectoryPicker() + // 将文件夹名称自动填入项目名(如未填写) + if (!uploadData.value.name.trim()) { + uploadData.value.name = dirHandle.name + } + // 提示用户手动确认服务器路径 + if (!uploadData.value.folderPath.trim()) { + uploadData.value.folderPath = dirHandle.name + } + } + } catch (e) { + if (e.name !== 'AbortError') { + console.error('Failed to select folder:', e) + } + } +} + // 固定用户ID(实际应用中应从登录状态获取) const userId = 1 @@ -439,6 +467,39 @@ defineExpose({ transition: border-color 0.2s; } +.input-with-action { + display: flex; + gap: 8px; + align-items: center; +} + +.input-with-action input { + flex: 1; + min-width: 0; +} + +.btn-browse { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border: 1px solid var(--border-input); + border-radius: 8px; + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.btn-browse:hover { + background: var(--bg-hover); + color: var(--accent-primary); + border-color: var(--accent-primary); +} + .form-group input:focus, .form-group textarea:focus { outline: none; diff --git a/frontend/src/components/SettingsPanel.vue b/frontend/src/components/SettingsPanel.vue index 00365e7..bda9762 100644 --- a/frontend/src/components/SettingsPanel.vue +++ b/frontend/src/components/SettingsPanel.vue @@ -1,5 +1,5 @@