diff --git a/src/pages/TaskAssistant.tsx b/src/pages/TaskAssistant.tsx index f71f7dd..98c2cef 100644 --- a/src/pages/TaskAssistant.tsx +++ b/src/pages/TaskAssistant.tsx @@ -3,8 +3,9 @@ import { Input, Button, theme, Typography, App } from 'antd' import { SendOutlined, RobotOutlined, PlusOutlined, ToolOutlined, DeleteOutlined, StopOutlined, MessageOutlined, UserOutlined, + CheckOutlined, CloseOutlined, } from '@ant-design/icons' -import { streamChat, type StreamEvent } from '@/services/ai-chat' +import { streamChat, resolveApproval, type StreamEvent } from '@/services/ai-chat' import { listConversations, createConversation, deleteConversation, getMessages, updateConversation, type Conversation, @@ -17,15 +18,17 @@ interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: number streaming?: boolean toolCalls?: { tool: string; preview?: string; done: boolean; duration?: number; error?: boolean }[] + approval?: { command: string; description: string; choices: string[]; runId: string; resolved?: boolean } } function ChatPage() { - const { modal } = App.useApp() + const { modal, message } = App.useApp() const [conversations, setConversations] = useState([]) const [activeId, setActiveId] = useState(null) const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [streaming, setStreaming] = useState(false) + const [waitingApproval, setWaitingApproval] = useState(false) const [editingId, setEditingId] = useState(null) const [editTitle, setEditTitle] = useState('') const [deleting, setDeleting] = useState(false) @@ -41,7 +44,7 @@ function ChatPage() { useEffect(() => { if (!activeId && conversations.length > 0) switchConversation(conversations[0].id) }, [conversations]) const switchConversation = useCallback(async (id: string) => { - if (streaming) { abortRef.current?.abort(); setStreaming(false) } + if (streaming) { abortRef.current?.abort(); setStreaming(false); setWaitingApproval(false) } setActiveId(id); setMessages([]) try { const records = await getMessages(id); setMessages(records.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: new Date(m.createdAt).getTime() }))) } catch {} }, [streaming]) @@ -67,6 +70,13 @@ function ChatPage() { setEditingId(null) } + const handleApprove = async (approvalMsg: Message, choice: string) => { + 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' ? '已拒绝' : '已批准') + } + 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() } @@ -93,12 +103,13 @@ function ChatPage() { 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] }) - } + 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 }) @@ -120,13 +131,13 @@ function ChatPage() { } catch (err: any) { if (err.name !== 'AbortError') update({ content: 'Error: ' + err.message, streaming: false }) else update({ streaming: false }) - } finally { setStreaming(false); abortRef.current = null } + } finally { setStreaming(false); setWaitingApproval(false); abortRef.current = null } } const handleStop = () => { abortRef.current?.abort(); setStreaming(false) } return ( -
+
{/* Sidebar */}
@@ -152,54 +163,80 @@ function ChatPage() {
- {/* Chat */} + {/* Chat area */}
-
+
{messages.length === 0 ? (
- 有什么可以帮你的? - Hermes Agent · DeepSeek xhigh 推理 · 可执行任务 + 有什么可以帮你的? + Hermes Agent · 可执行任务、操作文件
) : ( -
+
{messages.map(msg => ( -
-
-
- {msg.role === 'user' ? : } +
+ {/* Header */} +
+
+ {msg.role === 'user' ? 'Y' : 'H'}
- {msg.role === 'user' ? '你' : 'Hermes'} - {msg.streaming && 执行中...} + {msg.role === 'user' ? 'You' : 'Hermes'} + {msg.streaming && !msg.content && Thinking...}
+ {/* Approval card */} + {msg.approval && !msg.approval.resolved && ( +
+
+
+ +
+
+ 需要确认操作 +
+ {msg.approval.command} +
+ {msg.approval.description} +
+ {msg.approval.choices.map(c => ( + + ))} +
+
+
+
+ )} + + {/* Tool calls */} {msg.toolCalls && msg.toolCalls.length > 0 && ( -
+
{msg.toolCalls.map((t, i) => ( -
- + border: '1px solid ' + (t.done ? (t.error ? token.colorErrorBorder : token.colorSuccessBorder) : token.colorBorderSecondary) }}> + {t.preview || t.tool} - {t.done - ? {t.error ? '失败' : (t.duration ? t.duration.toFixed(1) + 's' : '')} - : 执行中} + {t.done && {t.error ? 'failed' : t.duration?.toFixed(1) + 's'}}
))}
)} -
- {msg.role === 'assistant' - ? (msg.content ? : 思考中...) - : msg.content} + {/* Content */} +
+
+ {msg.role === 'assistant' + ? (msg.content ? : (!msg.toolCalls?.length && !msg.approval ? Thinking... : null)) + :
{msg.content}
} +
))} @@ -208,20 +245,21 @@ function ChatPage() { )}
-
-
-
+ {/* Input */} +
+
+
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 ? '输入消息,Enter 发送 / Shift+Enter 换行' : '请先新建或选择对话'} - autoSize={{ minRows: 1, maxRows: 5 }} disabled={streaming || !activeId} + placeholder={activeId ? 'Message Hermes...' : 'Select or create a conversation'} + autoSize={{ minRows: 1, maxRows: 6 }} disabled={streaming || !activeId || waitingApproval} variant="borderless" style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 14 }} /> {streaming ? ( - + + disabled={!input.trim() || !activeId || waitingApproval} style={{ borderRadius: 10, height: 36, width: 36 }} /> )}
diff --git a/src/services/ai-chat.ts b/src/services/ai-chat.ts index 95e568a..5fca50e 100644 --- a/src/services/ai-chat.ts +++ b/src/services/ai-chat.ts @@ -7,6 +7,7 @@ export type StreamEvent = | { event: 'message.delta'; delta: string; runId?: string } | { event: 'tool.started'; tool: string; preview?: string; runId?: string } | { event: 'tool.completed'; tool: string; duration?: number; error?: boolean; runId?: string } + | { event: 'approval.request'; command: string; description: string; choices: string[]; runId: string } | { event: 'run.completed'; output?: string; usage?: any; runId?: string } | { event: 'done'; conversationId?: string } | { event: 'stopped' } @@ -91,3 +92,11 @@ export async function sendMessage( if (!json.success) throw new Error(json.message || 'Chat failed') return json.data } + +export async function resolveApproval(runId: string, choice: string): Promise { + await fetch('/admin-api/ai/chat/stop', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + (getAccessToken() || '') }, + body: JSON.stringify({ runId, action: choice }), + }) +}