From 6ffbb29ec7b4aa22f1167592edcb3add1f660f79 Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Thu, 26 Mar 2026 21:17:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BC=A0=E9=80=92=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/models.py | 11 +- backend/routes/projects.py | 2 +- backend/services/chat.py | 143 ++++++++++++++-------- backend/utils/helpers.py | 14 ++- docs/Design.md | 96 ++++++++++++--- frontend/src/App.vue | 77 +++++++++--- frontend/src/api/index.js | 5 +- frontend/src/components/ChatView.vue | 5 +- frontend/src/components/MessageBubble.vue | 25 ++-- frontend/src/components/ProcessBlock.vue | 88 ++++++------- frontend/src/components/Sidebar.vue | 34 ++++- 11 files changed, 349 insertions(+), 151 deletions(-) diff --git a/backend/models.py b/backend/models.py index e7d4e39..052bea5 100644 --- a/backend/models.py +++ b/backend/models.py @@ -96,7 +96,16 @@ class Message(db.Model): role = db.Column(db.String(16), nullable=False) # user, assistant, system, tool # Unified JSON structure: # User: {"text": "...", "attachments": [{"name": "a.py", "extension": "py", "content": "..."}]} - # Assistant: {"text": "...", "thinking": "...", "tool_calls": [{"id": "...", "name": "...", "arguments": "...", "result": "..."}]} + # Assistant: { + # "text": "...", + # "tool_calls": [...], // legacy flat structure + # "steps": [ // ordered steps for rendering (primary source of truth) + # {"id": "step-0", "index": 0, "type": "thinking", "content": "..."}, + # {"id": "step-1", "index": 1, "type": "text", "content": "..."}, + # {"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_xxx", "name": "...", "arguments": "..."}, + # {"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_xxx", "name": "...", "content": "..."}, + # ] + # } content = db.Column(LongText, default="") token_count = db.Column(db.Integer, default=0) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), index=True) diff --git a/backend/routes/projects.py b/backend/routes/projects.py index acfa0fe..e70785c 100644 --- a/backend/routes/projects.py +++ b/backend/routes/projects.py @@ -224,7 +224,7 @@ def upload_project_folder(): # Create project record project = Project( id=str(uuid.uuid4()), - user_id=user_id, + user_id=user.id, name=project_name, path=relative_path, description=description diff --git a/backend/services/chat.py b/backend/services/chat.py index 9858793..6061469 100644 --- a/backend/services/chat.py +++ b/backend/services/chat.py @@ -53,8 +53,9 @@ class ChatService: messages = list(initial_messages) all_tool_calls = [] all_tool_results = [] + all_steps = [] # Collect all ordered steps for DB storage (thinking/text/tool_call/tool_result) step_index = 0 # Track global step index for ordering - + for iteration in range(self.MAX_ITERATIONS): full_content = "" full_thinking = "" @@ -62,10 +63,10 @@ class ChatService: prompt_tokens = 0 msg_id = str(uuid.uuid4()) tool_calls_list = [] - + # Send thinking_start event to clear previous thinking in frontend yield f"event: thinking_start\ndata: {{}}\n\n" - + try: with app.app_context(): active_conv = db.session.get(Conversation, conv_id) @@ -79,7 +80,8 @@ class ChatService: stream=True, ) resp.raise_for_status() - + + # Stream LLM response chunk by chunk for line in resp.iter_lines(): if not line: continue @@ -93,76 +95,109 @@ class ChatService: chunk = json.loads(data_str) except json.JSONDecodeError: continue - + delta = chunk["choices"][0].get("delta", {}) - - # Process thinking - send as process_step + + # Accumulate thinking content for this iteration reasoning = delta.get("reasoning_content", "") if reasoning: full_thinking += reasoning - # Still send thinking event for backward compatibility yield f"event: thinking\ndata: {json.dumps({'content': reasoning}, ensure_ascii=False)}\n\n" - - # Process text + + # Accumulate text content for this iteration text = delta.get("content", "") if text: full_content += text yield f"event: message\ndata: {json.dumps({'content': text}, ensure_ascii=False)}\n\n" - - # Process tool calls + + # Accumulate tool calls from streaming deltas tool_calls_list = self._process_tool_calls_delta(delta, tool_calls_list) - + usage = chunk.get("usage", {}) if usage: token_count = usage.get("completion_tokens", 0) prompt_tokens = usage.get("prompt_tokens", 0) - + except Exception as e: yield f"event: error\ndata: {json.dumps({'content': str(e)}, ensure_ascii=False)}\n\n" return - - # Tool calls exist - execute and continue + + # --- Tool calls exist: emit finalized steps, execute tools, continue loop --- if tool_calls_list: all_tool_calls.extend(tool_calls_list) - - # Send thinking as a complete step if exists + + # Record thinking as a finalized step (preserves order) if full_thinking: - yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'thinking', 'content': full_thinking}, ensure_ascii=False)}\n\n" + step_data = { + 'id': f'step-{step_index}', + 'index': step_index, + 'type': 'thinking', + 'content': full_thinking, + } + all_steps.append(step_data) + yield f"event: process_step\ndata: {json.dumps(step_data, ensure_ascii=False)}\n\n" step_index += 1 - - # Send text as a step if exists (text before tool calls) + + # Record text as a finalized step (text that preceded 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_data = { + 'id': f'step-{step_index}', + 'index': step_index, + 'type': 'text', + 'content': full_content, + } + all_steps.append(step_data) + yield f"event: process_step\ndata: {json.dumps(step_data, ensure_ascii=False)}\n\n" step_index += 1 - - # Also send legacy tool_calls event for backward compatibility + + # Legacy tool_calls event for backward compatibility yield f"event: tool_calls\ndata: {json.dumps({'calls': tool_calls_list}, ensure_ascii=False)}\n\n" - - # Process each tool call one by one, send result immediately + + # Execute each tool call, emit tool_call + tool_result as paired steps tool_results = [] for tc in tool_calls_list: - # Send tool call step - yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'tool_call', 'id': tc['id'], 'name': tc['function']['name'], 'arguments': tc['function']['arguments']}, ensure_ascii=False)}\n\n" + # Emit tool_call step (before execution) + call_step = { + 'id': f'step-{step_index}', + 'index': step_index, + 'type': 'tool_call', + 'id_ref': tc['id'], + 'name': tc['function']['name'], + 'arguments': tc['function']['arguments'], + } + all_steps.append(call_step) + yield f"event: process_step\ndata: {json.dumps(call_step, ensure_ascii=False)}\n\n" step_index += 1 - - # Execute this single tool call (needs app context for db access) + + # Execute the tool with app.app_context(): single_result = self.executor.process_tool_calls([tc], context) tool_results.extend(single_result) - - # Send tool result step immediately + + # Emit tool_result step (after execution) tr = single_result[0] try: result_content = json.loads(tr["content"]) skipped = result_content.get("skipped", False) except: skipped = False - yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'tool_result', 'id': tr['tool_call_id'], 'name': tr['name'], 'content': tr['content'], 'skipped': skipped}, ensure_ascii=False)}\n\n" + result_step = { + 'id': f'step-{step_index}', + 'index': step_index, + 'type': 'tool_result', + 'id_ref': tr['tool_call_id'], + 'name': tr['name'], + 'content': tr['content'], + 'skipped': skipped, + } + all_steps.append(result_step) + yield f"event: process_step\ndata: {json.dumps(result_step, ensure_ascii=False)}\n\n" step_index += 1 - - # Also send legacy tool_result event + + # Legacy tool_result event for backward compatibility yield f"event: tool_result\ndata: {json.dumps({'id': tr['tool_call_id'], 'name': tr['name'], 'content': tr['content'], 'skipped': skipped}, ensure_ascii=False)}\n\n" - + + # Append assistant message + tool results for the next iteration messages.append({ "role": "assistant", "content": full_content or None, @@ -171,28 +206,41 @@ class ChatService: messages.extend(tool_results) all_tool_results.extend(tool_results) continue - - # No tool calls - finish - # Send thinking as a step if exists + + # --- No tool calls: final iteration — emit remaining steps and save --- if full_thinking: - yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'thinking', 'content': full_thinking}, ensure_ascii=False)}\n\n" + step_data = { + 'id': f'step-{step_index}', + 'index': step_index, + 'type': 'thinking', + 'content': full_thinking, + } + all_steps.append(step_data) + yield f"event: process_step\ndata: {json.dumps(step_data, 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_data = { + 'id': f'step-{step_index}', + 'index': step_index, + 'type': 'text', + 'content': full_content, + } + all_steps.append(step_data) + yield f"event: process_step\ndata: {json.dumps(step_data, ensure_ascii=False)}\n\n" step_index += 1 suggested_title = None with app.app_context(): - # Build content JSON + # Build content JSON with ordered steps array for DB storage. + # 'steps' is the single source of truth for rendering order. content_json = { "text": full_content, } - if full_thinking: - content_json["thinking"] = full_thinking if all_tool_calls: content_json["tool_calls"] = self._build_tool_calls_json(all_tool_calls, all_tool_results) + # Store ordered steps — the single source of truth for rendering order + content_json["steps"] = all_steps msg = Message( id=msg_id, @@ -208,15 +256,13 @@ class ChatService: if user: record_token_usage(user.id, conv_model, prompt_tokens, token_count) - # Check if we need to set title (first message in conversation) + # Auto-generate title from first user message if needed conv = db.session.get(Conversation, conv_id) if conv and (not conv.title or conv.title == "新对话"): - # Get user message content user_msg = Message.query.filter_by( conversation_id=conv_id, role="user" ).order_by(Message.created_at.asc()).first() if user_msg and user_msg.content: - # Parse content JSON to get text try: content_data = json.loads(user_msg.content) title_text = content_data.get("text", "")[:30] @@ -226,7 +272,6 @@ class ChatService: suggested_title = title_text else: suggested_title = "新对话" - # Refresh conv to avoid stale state db.session.refresh(conv) conv.title = suggested_title db.session.commit() diff --git a/backend/utils/helpers.py b/backend/utils/helpers.py index f523d7a..a9761b4 100644 --- a/backend/utils/helpers.py +++ b/backend/utils/helpers.py @@ -63,7 +63,12 @@ def to_dict(inst, **extra): def message_to_dict(msg: Message) -> dict: - """Convert message to dict, parsing JSON content""" + """Convert message to dict, parsing JSON content. + + For assistant messages, extracts the 'steps' array which preserves the + ordered sequence of thinking/text/tool_call/tool_result steps, so the + frontend can render them in the correct interleaved order. + """ result = to_dict(msg) # Parse content JSON @@ -71,16 +76,15 @@ def message_to_dict(msg: Message) -> dict: try: content_data = json.loads(msg.content) if isinstance(content_data, dict): - # Extract all fields from JSON result["text"] = content_data.get("text", "") if content_data.get("attachments"): result["attachments"] = content_data["attachments"] - if content_data.get("thinking"): - result["thinking"] = content_data["thinking"] if content_data.get("tool_calls"): result["tool_calls"] = content_data["tool_calls"] + # Extract ordered steps array for correct rendering order + if content_data.get("steps"): + result["process_steps"] = content_data["steps"] else: - # Fallback: plain text result["text"] = msg.content except (json.JSONDecodeError, TypeError): result["text"] = msg.content diff --git a/docs/Design.md b/docs/Design.md index 5ca0f80..6518862 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -175,7 +175,6 @@ classDiagram ```json { "text": "AI 回复的文本内容", - "thinking": "思考过程(可选)", "tool_calls": [ { "id": "call_xxx", @@ -189,10 +188,55 @@ classDiagram "skipped": false, "execution_time": 0.5 } + ], + "steps": [ + { + "id": "step-0", + "index": 0, + "type": "thinking", + "content": "第一轮思考过程..." + }, + { + "id": "step-1", + "index": 1, + "type": "text", + "content": "工具调用前的文本..." + }, + { + "id": "step-2", + "index": 2, + "type": "tool_call", + "id_ref": "call_abc123", + "name": "web_search", + "arguments": "{\"query\": \"...\"}" + }, + { + "id": "step-3", + "index": 3, + "type": "tool_result", + "id_ref": "call_abc123", + "name": "web_search", + "content": "{\"success\": true, ...}", + "skipped": false + }, + { + "id": "step-4", + "index": 4, + "type": "thinking", + "content": "第二轮思考过程..." + }, + { + "id": "step-5", + "index": 5, + "type": "text", + "content": "最终回复文本..." + } ] } ``` +`steps` 字段是**渲染顺序的唯一数据源**,按 `index` 顺序排列。thinking、text、tool_call、tool_result 可以在多轮迭代中穿插出现。`id_ref` 用于 tool_call 和 tool_result 步骤之间的匹配(对应 LLM 返回的工具调用 ID)。`tool_calls` 字段保留用于向后兼容旧版前端。 + ### 服务层 ```mermaid @@ -426,33 +470,53 @@ def process_tool_calls(self, tool_calls, context=None): | `message` | 回复内容的增量片段 | | `tool_calls` | 工具调用信息 | | `tool_result` | 工具执行结果 | -| `process_step` | 处理步骤(按顺序:thinking/text/tool_call/tool_result),支持穿插显示 | +| `process_step` | 有序处理步骤(thinking/text/tool_call/tool_result),支持穿插显示。携带 `id`、`index` 确保渲染顺序 | | `error` | 错误信息 | | `done` | 回复结束,携带 message_id 和 token_count | ### process_step 事件格式 +每个 `process_step` 事件携带一个带 `id`、`index` 和 `type` 的步骤对象。步骤按 `index` 顺序排列,确保前端可以正确渲染穿插的思考、文本和工具调用。 + ```json // 思考过程 -{"index": 0, "type": "thinking", "content": "完整思考内容..."} +{"id": "step-0", "index": 0, "type": "thinking", "content": "完整思考内容..."} // 回复文本(可穿插在任意步骤之间) -{"index": 1, "type": "text", "content": "回复文本内容..."} +{"id": "step-1", "index": 1, "type": "text", "content": "回复文本内容..."} -// 工具调用 -{"index": 2, "type": "tool_call", "id": "call_abc123", "name": "web_search", "arguments": "{\"query\": \"...\"}"} +// 工具调用(id_ref 存储工具调用 ID,用于与 tool_result 匹配) +{"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_abc123", "name": "web_search", "arguments": "{\"query\": \"...\"}"} -// 工具返回 -{"index": 3, "type": "tool_result", "id": "call_abc123", "name": "web_search", "content": "{\"success\": true, ...}", "skipped": false} +// 工具返回(id_ref 与 tool_call 的 id_ref 匹配) +{"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_abc123", "name": "web_search", "content": "{\"success\": true, ...}", "skipped": false} ``` 字段说明: -- `index`: 步骤序号,确保按正确顺序显示 -- `type`: 步骤类型(thinking/tool_call/tool_result) -- `id`: 工具调用唯一标识,用于匹配工具调用和返回结果 -- `name`: 工具名称 -- `content`: 内容或结果 -- `skipped`: 工具是否被跳过(失败后跳过) + +| 字段 | 说明 | +|------|------| +| `id` | 步骤唯一标识(格式 `step-{index}`),用于前端 key | +| `index` | 步骤序号,确保按正确顺序显示 | +| `type` | 步骤类型:`thinking` / `text` / `tool_call` / `tool_result` | +| `id_ref` | 工具调用引用 ID(仅 tool_call/tool_result),用于匹配调用与结果 | +| `name` | 工具名称(仅 tool_call/tool_result) | +| `arguments` | 工具调用参数 JSON 字符串(仅 tool_call) | +| `content` | 内容(thinking 的思考内容、text 的文本、tool_result 的返回结果) | +| `skipped` | 工具是否被跳过(仅 tool_result) | + +### 多轮迭代中的步骤顺序 + +一次完整的 LLM 交互可能经历多轮工具调用循环,每轮产生的步骤按以下顺序追加: + +``` +迭代 1: thinking → text → tool_call → tool_result +迭代 2: thinking → text → tool_call → tool_result +... +最终轮: thinking → text(无工具调用,结束) +``` + +所有步骤通过全局递增的 `index` 保证顺序。后端在完成所有迭代后,将这些步骤存入 `content_json["steps"]` 数组写入数据库。前端页面刷新时从 API 加载消息,`message_to_dict` 提取 `steps` 字段映射为 `process_steps` 返回,ProcessBlock 组件按 `index` 顺序渲染。 --- @@ -509,10 +573,12 @@ def process_tool_calls(self, tool_calls, context=None): | `id` | String(64) | UUID 主键 | | `conversation_id` | String(64) | 外键关联 Conversation | | `role` | String(16) | user/assistant/system/tool | -| `content` | LongText | JSON 格式内容(见上方结构说明) | +| `content` | LongText | JSON 格式内容(见上方结构说明),assistant 消息包含 `steps` 有序步骤数组 | | `token_count` | Integer | Token 数量 | | `created_at` | DateTime | 创建时间 | +`message_to_dict()` 辅助函数负责解析 `content` JSON,并提取 `steps` 字段映射为 `process_steps` 返回给前端,确保页面刷新后仍能按正确顺序渲染穿插的思考、文本和工具调用。 + ### TokenUsage(Token 使用统计) | 字段 | 类型 | 说明 | diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 42e2a1f..e3ce5ed 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -2,6 +2,7 @@
@@ -42,7 +44,6 @@ :messages="messages" :streaming="streaming" :streaming-content="streamContent" - :streaming-thinking="streamThinking" :streaming-tool-calls="streamToolCalls" :streaming-process-steps="streamProcessSteps" :has-more-messages="hasMoreMessages" @@ -128,6 +129,9 @@ const loadingConvs = ref(false) const hasMoreConvs = ref(false) const nextConvCursor = ref(null) +// -- Projects state -- +const projects = ref([]) + // -- Messages state -- const messages = shallowRef([]) const hasMoreMessages = ref(false) @@ -135,11 +139,14 @@ const loadingMessages = ref(false) const nextMsgCursor = ref(null) // -- Streaming state -- +// These refs hold the real-time streaming data for the current conversation. +// When switching conversations, the current state is saved to streamStates Map +// and restored when switching back. On stream completion (onDone), the finalized +// processSteps are stored in the message object and later persisted to DB. const streaming = ref(false) -const streamContent = ref('') -const streamThinking = ref('') -const streamToolCalls = shallowRef([]) -const streamProcessSteps = shallowRef([]) +const streamContent = ref('') // Accumulated text content during current iteration +const streamToolCalls = shallowRef([]) // All tool calls across iterations (legacy compat) +const streamProcessSteps = shallowRef([]) // Ordered steps: thinking/text/tool_call/tool_result // 保存每个对话的流式状态 const streamStates = new Map() @@ -147,7 +154,6 @@ const streamStates = new Map() function setStreamState(isActive) { streaming.value = isActive streamContent.value = '' - streamThinking.value = '' streamToolCalls.value = [] streamProcessSteps.value = [] } @@ -253,7 +259,6 @@ async function selectConversation(id) { streamStates.set(currentConvId.value, { streaming: true, streamContent: streamContent.value, - streamThinking: streamThinking.value, streamToolCalls: [...streamToolCalls.value], streamProcessSteps: [...streamProcessSteps.value], messages: [...messages.value], @@ -269,7 +274,6 @@ async function selectConversation(id) { if (savedState && savedState.streaming) { streaming.value = true streamContent.value = savedState.streamContent - streamThinking.value = savedState.streamThinking streamToolCalls.value = savedState.streamToolCalls streamProcessSteps.value = savedState.streamProcessSteps messages.value = savedState.messages || [] @@ -310,13 +314,6 @@ function loadMoreMessages() { // -- Helpers: create stream callbacks for a conversation -- function createStreamCallbacks(convId, { updateConvList = true } = {}) { return { - onThinkingStart() { - updateStreamField(convId, 'streamThinking', streamThinking, '') - updateStreamField(convId, 'streamContent', streamContent, '') - }, - onThinking(text) { - updateStreamField(convId, 'streamThinking', streamThinking, prev => (prev || '') + text) - }, onMessage(text) { updateStreamField(convId, 'streamContent', streamContent, prev => (prev || '') + text) }, @@ -335,6 +332,10 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) { }) }, onProcessStep(step) { + // Insert step at its index position to preserve ordering. + // Uses sparse array strategy: fills gaps with null. + // Each step carries { id, index, type, content, ... } — + // these are the same steps that get stored to DB as the 'steps' array. updateStreamField(convId, 'streamProcessSteps', streamProcessSteps, prev => { const steps = prev ? [...prev] : [] while (steps.length <= step.index) steps.push(null) @@ -347,12 +348,15 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) { if (currentConvId.value === convId) { streaming.value = false + + // Build the final message object. + // process_steps is the primary ordered data for rendering (thinking/text/tool_call/tool_result). + // When page reloads, these steps are loaded from DB via the 'steps' field in content JSON. messages.value = [...messages.value, { id: data.message_id, conversation_id: convId, role: 'assistant', text: streamContent.value, - thinking: streamThinking.value || null, tool_calls: streamToolCalls.value.length > 0 ? streamToolCalls.value : null, process_steps: streamProcessSteps.value.filter(Boolean), token_count: data.token_count, @@ -507,13 +511,13 @@ async function createProject() { creatingProject.value = true try { await projectApi.create({ - user_id: 1, name: newProjectName.value.trim(), description: newProjectDesc.value.trim(), }) showCreateModal.value = false newProjectName.value = '' newProjectDesc.value = '' + await loadProjects() } catch (e) { console.error('Failed to create project:', e) } finally { @@ -521,8 +525,47 @@ async function createProject() { } } +// -- Load projects -- +async function loadProjects() { + try { + const res = await projectApi.list() + projects.value = res.data.projects || [] + } catch (e) { + console.error('Failed to load projects:', e) + } +} + +// -- Delete project -- +async function deleteProject(project) { + if (!confirm(`确定删除项目「${project.name}」及其所有对话?`)) return + try { + await projectApi.delete(project.id) + // Remove conversations belonging to this project + conversations.value = conversations.value.filter(c => c.project_id !== project.id) + // If current conversation was in this project, switch away + if (currentConvId.value && conversations.value.length > 0) { + const currentConv = conversations.value.find(c => c.id === currentConvId.value) + if (!currentConv || currentConv.project_id === project.id) { + await selectConversation(conversations.value[0].id) + } + } else if (conversations.value.length === 0) { + currentConvId.value = null + messages.value = [] + currentProject.value = null + } + if (currentProject.value?.id === project.id) { + currentProject.value = null + showFileExplorer.value = false + } + await loadProjects() + } catch (e) { + console.error('Failed to delete project:', e) + } +} + // -- Init -- onMounted(() => { + loadProjects() loadConversations() }) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 266152b..8c960b0 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -194,8 +194,8 @@ export const messageApi = { } export const projectApi = { - list(userId) { - return request(`/projects${buildQueryParams({ user_id: userId })}`) + list() { + return request('/projects') }, create(data) { @@ -211,7 +211,6 @@ export const projectApi = { uploadFolder(data) { const formData = new FormData() - formData.append('user_id', String(data.user_id)) formData.append('name', data.name || '') formData.append('description', data.description || '') for (const file of data.files) { diff --git a/frontend/src/components/ChatView.vue b/frontend/src/components/ChatView.vue index db651c0..624dc18 100644 --- a/frontend/src/components/ChatView.vue +++ b/frontend/src/components/ChatView.vue @@ -27,12 +27,11 @@ v-for="msg in messages" :key="msg.id" :data-msg-id="msg.id" - v-memo="[msg.text, msg.thinking, msg.tool_calls, msg.process_steps, msg.attachments]" + v-memo="[msg.text, msg.tool_calls, msg.process_steps, msg.attachments]" > claw
[] }, streamingProcessSteps: { type: Array, default: () => [] }, hasMoreMessages: { type: Boolean, default: false }, diff --git a/frontend/src/components/MessageBubble.vue b/frontend/src/components/MessageBubble.vue index 9b5ba41..796e8e1 100644 --- a/frontend/src/components/MessageBubble.vue +++ b/frontend/src/components/MessageBubble.vue @@ -3,7 +3,7 @@
user
claw
- +
{{ file.extension }} @@ -11,18 +11,18 @@
- + + - + +