From 0efc78c656f1d0b5dbd57ebd51dd905244b5f9c9 Mon Sep 17 00:00:00 2001 From: WangDL Date: Fri, 22 May 2026 19:13:06 +0800 Subject: [PATCH] fix: right-aligned user bubbles + no labels + Chinese + copy btn --- src/pages/TaskAssistant.tsx | 236 ++++++++++++++---------------------- 1 file changed, 89 insertions(+), 147 deletions(-) diff --git a/src/pages/TaskAssistant.tsx b/src/pages/TaskAssistant.tsx index 994868b..81f7b6e 100644 --- a/src/pages/TaskAssistant.tsx +++ b/src/pages/TaskAssistant.tsx @@ -1,13 +1,8 @@ import { useState, useRef, useEffect, useCallback } from 'react' -import { Input, Button, Typography, App } from 'antd' -import { - PlusOutlined, ToolOutlined, DeleteOutlined, MessageOutlined, ArrowUpOutlined, -} from '@ant-design/icons' +import { Input, Button, Typography, App, message as antMsg } from 'antd' +import { PlusOutlined, ToolOutlined, DeleteOutlined, MessageOutlined, ArrowUpOutlined, CopyOutlined } from '@ant-design/icons' import { streamChat, resolveApproval, type StreamEvent } from '@/services/ai-chat' -import { - listConversations, createConversation, deleteConversation, - getMessages, updateConversation, type Conversation, -} from '@/services/conversation-api' +import { listConversations, createConversation, deleteConversation, getMessages, updateConversation, type Conversation } from '@/services/conversation-api' import Markdown from '@/components/Markdown' const { Text } = Typography @@ -20,7 +15,7 @@ interface Message { } function ChatPage() { - const { modal, message } = App.useApp() + const { modal } = App.useApp() const [conversations, setConversations] = useState([]) const [activeId, setActiveId] = useState(null) const [messages, setMessages] = useState([]) @@ -51,10 +46,8 @@ function ChatPage() { } const handleDelete = (id: string) => modal.confirm({ - title: 'Delete conversation?', okText: 'Delete', okType: 'danger', cancelText: 'Cancel', - onOk: async () => { - try { await deleteConversation(id); setConversations(prev => prev.filter(c => c.id !== id)); if (activeId === id) { setActiveId(null); setMessages([]) } } catch {} - }, + title: '删除对话', okText: '删除', okType: 'danger', cancelText: '取消', + onOk: async () => { try { await deleteConversation(id); setConversations(prev => prev.filter(c => c.id !== id)); if (activeId === id) { setActiveId(null); setMessages([]) } } catch {} }, }) const startEdit = (conv: Conversation) => { setEditingId(conv.id); setEditTitle(conv.title); setTimeout(() => editInputRef.current?.focus(), 50) } @@ -68,9 +61,11 @@ function ChatPage() { setMessages(prev => prev.map(m => m.id === approvalMsg.id ? { ...m, approval: { ...m.approval!, resolved: true } } : m)) setWaitingApproval(false) await resolveApproval(approvalMsg.approval!.runId, choice) - message.success(choice === 'deny' ? 'Denied' : 'Approved') + antMsg.success(choice === 'deny' ? '已拒绝' : '已批准') } + const copyText = (text: string) => { navigator.clipboard.writeText(text); antMsg.success('已复制') } + const handleSend = async () => { const text = input.trim(); if (!text || streaming) return; setInput('') const userMsg: Message = { id: Date.now().toString(), role: 'user', content: text, timestamp: Date.now() } @@ -87,44 +82,19 @@ function ChatPage() { try { await streamChat(prevMessages.map(m => ({ role: m.role, content: m.content })), activeId, (event: StreamEvent) => { switch (event.event) { - case 'meta': - completedConvId = event.conversationId - if (event.conversationId && event.conversationId !== activeId) { setActiveId(event.conversationId); loadConversations() } - break - case 'tool.started': - currentTools.push({ tool: event.tool, preview: event.preview, done: false }) - update({ toolCalls: [...currentTools] }) - break - case 'tool.completed': { - const i = currentTools.findIndex((t: any) => t.tool === event.tool && !t.done) - if (i >= 0) { currentTools[i] = { ...currentTools[i], done: true, duration: event.duration, error: event.error }; update({ toolCalls: [...currentTools] }) } - break - } - case 'approval.request': - setWaitingApproval(true) - update({ approval: { command: event.command, description: event.description, choices: event.choices, runId: event.runId } }) - break - case 'message.delta': - currentContent += event.delta || '' - update({ content: currentContent, streaming: true }) - break - case 'run.completed': - if (event.output) currentContent = event.output - update({ content: currentContent, streaming: false }) - break - case 'done': - completedConvId = event.conversationId || completedConvId - update({ streaming: false }) - break - case 'error': - update({ content: 'Error: ' + event.error, streaming: false }) - break + case 'meta': completedConvId = event.conversationId; if (event.conversationId && event.conversationId !== activeId) { setActiveId(event.conversationId); loadConversations() } break + case 'tool.started': currentTools.push({ tool: event.tool, preview: event.preview, done: false }); update({ toolCalls: [...currentTools] }) break + case 'tool.completed': { const i = currentTools.findIndex((t: any) => t.tool === event.tool && !t.done); if (i >= 0) { currentTools[i] = { ...currentTools[i], done: true, duration: event.duration, error: event.error }; update({ toolCalls: [...currentTools] }) } } break + case 'approval.request': setWaitingApproval(true); update({ approval: { command: event.command, description: event.description, choices: event.choices, runId: event.runId } }) break + case 'message.delta': currentContent += event.delta || ''; update({ content: currentContent, streaming: true }) break + case 'run.completed': if (event.output) currentContent = event.output; update({ content: currentContent, streaming: false }) break + case 'done': completedConvId = event.conversationId || completedConvId; update({ streaming: false }) break + case 'error': update({ content: 'Error: ' + event.error, streaming: false }) break } }, controller.signal) if (completedConvId) loadConversations() } catch (err: any) { - if (err.name !== 'AbortError') update({ content: 'Error: ' + err.message, streaming: false }) - else update({ streaming: false }) + if (err.name !== 'AbortError') update({ content: 'Error: ' + err.message, streaming: false }); else update({ streaming: false }) } finally { setStreaming(false); setWaitingApproval(false); abortRef.current = null } } @@ -132,21 +102,15 @@ function ChatPage() { return (
- {/* Sidebar — DeepSeek style: no border, subtle bg */}
- +
{conversations.map(conv => ( -
activeId !== conv.id && switchConversation(conv.id)} - style={{ - display: 'flex', alignItems: 'center', padding: '10px 12px', borderRadius: 8, cursor: 'pointer', marginBottom: 2, - background: activeId === conv.id ? '#e8e8ed' : 'transparent', - transition: 'background 0.15s', - }} +
activeId !== conv.id && switchConversation(conv.id)} + style={{ display: 'flex', alignItems: 'center', padding: '10px 12px', borderRadius: 8, cursor: 'pointer', marginBottom: 2, + background: activeId === conv.id ? '#e8e8ed' : 'transparent' }} onMouseEnter={e => { if (activeId !== conv.id) e.currentTarget.style.background = '#ececf0' }} onMouseLeave={e => { if (activeId !== conv.id) e.currentTarget.style.background = 'transparent' }}> @@ -155,81 +119,79 @@ function ChatPage() { onBlur={() => saveTitle(conv.id)} onPressEnter={() => saveTitle(conv.id)} onClick={e => e.stopPropagation()} style={{ flex: 1, fontSize: 13 }} bordered={false} /> ) : ( - { e.stopPropagation(); startEdit(conv) }}>{conv.title} + { e.stopPropagation(); startEdit(conv) }}>{conv.title} )} { e.stopPropagation(); handleDelete(conv.id) }} - style={{ fontSize: 12, color: '#bbb', cursor: 'pointer', opacity: 0, transition: 'opacity 0.15s' }} - className="delete-icon" /> + style={{ fontSize: 12, color: '#bbb', cursor: 'pointer', opacity: 0, transition: 'opacity 0.15s' }} className="dl-icon" />
))}
- +
- {/* Chat */}
- {/* Messages */}
{messages.length === 0 ? (
- What can I help with? + 有什么可以帮助你的? + Hermes Agent · DeepSeek · xhigh 推理
) : (
{messages.map(msg => (
- {/* Role label — DeepSeek style: small bold text, no avatar */} -
- - {msg.role === 'user' ? 'You' : 'Hermes'} - - {msg.streaming && !msg.content && !msg.toolCalls?.length && !msg.approval && ( - - )} -
- - {/* Approval */} - {msg.approval && !msg.approval.resolved && ( -
- Approval Required -
- $ {msg.approval.command} -
- {msg.approval.description} -
- {msg.approval.choices.map(c => ( - - ))} + {/* User: right-aligned bubble / Assistant: left-aligned plain */} + {msg.role === 'user' ? ( +
+
+ {msg.content}
- )} - - {/* Tool calls — subtle pills */} - {msg.toolCalls && msg.toolCalls.length > 0 && ( -
- {msg.toolCalls.map((t, i) => ( -
- - {t.preview || t.tool} - {t.done && {t.error ? 'failed' : 'done'}} + ) : ( + <> + {/* Approval */} + {msg.approval && !msg.approval.resolved && ( +
+ 需要确认操作 +
$ {msg.approval.command}
+ {msg.approval.description} +
+ {msg.approval.choices.map(c => ( + + ))} +
- ))} -
- )} + )} - {/* Content — DeepSeek style: no bubble, just clean text */} -
- {msg.role === 'assistant' - ? (msg.content ? : null) - : {msg.content}} -
+ {/* Tool calls */} + {msg.toolCalls && msg.toolCalls.length > 0 && ( +
+ {msg.toolCalls.map((t, i) => ( + + {t.preview || t.tool} + {t.done && {t.error ? '失败' : '完成'}} + + ))} +
+ )} + + {/* Content */} +
+ {msg.content ? : (msg.streaming ? 思考中... : null)} +
+ + {/* Copy button after response completes */} + {msg.content && !msg.streaming && msg.role === 'assistant' && ( +
+ +
+ )} + + )}
))}
@@ -237,55 +199,35 @@ function ChatPage() { )}
- {/* Input — DeepSeek style: minimal, centered */}
-
- setInput(e.target.value)} - onCompositionStart={() => { composingRef.current = true }} - onCompositionEnd={() => { composingRef.current = false }} - onKeyDown={e => { - if (composingRef.current) return - if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } - }} - placeholder={activeId ? 'Ask anything' : 'Select a conversation'} - autoSize={{ minRows: 1, maxRows: 6 }} - disabled={streaming || !activeId || waitingApproval} - variant="borderless" - style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 15, lineHeight: 1.5 }} - /> +
+ setInput(e.target.value)} + onCompositionStart={() => { composingRef.current = true }} onCompositionEnd={() => { composingRef.current = false }} + onKeyDown={e => { if (composingRef.current) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }} + placeholder={activeId ? '发送消息...' : '请先选择或新建对话'} + autoSize={{ minRows: 1, maxRows: 6 }} disabled={streaming || !activeId || waitingApproval} + variant="borderless" style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 15, lineHeight: 1.5 }} /> {streaming ? ( -
+
) : ( -
+
)}
- Hermes Agent · DeepSeek · xhigh reasoning + Hermes Agent · DeepSeek · xhigh 推理
- -
) }