diff --git a/src/pages/TaskAssistant.tsx b/src/pages/TaskAssistant.tsx index 9a00398..2a64b41 100644 --- a/src/pages/TaskAssistant.tsx +++ b/src/pages/TaskAssistant.tsx @@ -1,228 +1,15 @@ -import { useState, useRef, useEffect, useCallback } from 'react' -import { Input, Button, theme, Typography, App } from 'antd' -import { - SendOutlined, RobotOutlined, PlusOutlined, - DeleteOutlined, StopOutlined, MessageOutlined, -} from '@ant-design/icons' -import { streamChat, type StreamEvent } from '@/services/ai-chat' -import { - listConversations, createConversation, deleteConversation, - getMessages, updateConversation, type Conversation, -} from '@/services/conversation-api' -import Markdown from '@/components/Markdown' +import { Typography } from 'antd' const { Text } = Typography -interface Message { - id: string - role: 'user' | 'assistant' - content: string - timestamp: number - streaming?: boolean -} - -function ChatPage() { - const { modal } = 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 [editingId, setEditingId] = useState(null) - const [editTitle, setEditTitle] = useState('') - const [deleting, setDeleting] = useState(false) - const composingRef = useRef(false) - const abortRef = useRef(null) - const messagesEndRef = useRef(null) - const editInputRef = useRef(null) - const { token } = theme.useToken() - - const loadConversations = useCallback(async () => { - try { setConversations(await listConversations()) } catch { /* */ } - }, []) - useEffect(() => { loadConversations() }, [loadConversations]) - - // Auto-select first conversation on load - useEffect(() => { - if (!activeId && conversations.length > 0) { - switchConversation(conversations[0].id) - } - }, [conversations, activeId]) - useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) - - const switchConversation = useCallback(async (id: string) => { - if (streaming) { abortRef.current?.abort(); setStreaming(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]) - - const handleNew = async () => { - if (streaming) { abortRef.current?.abort(); setStreaming(false) } - try { - const conv = await createConversation() - setConversations(prev => [conv, ...prev]) - setActiveId(conv.id); setMessages([]); setInput('') - } catch { /* */ } - } - - const handleDelete = (id: string) => modal.confirm({ - title: '删除对话', content: '确定?', okText: '删除', okType: 'danger', cancelText: '取消', - onOk: async () => { - setDeleting(true) - try { await deleteConversation(id); setConversations(prev => prev.filter(c => c.id !== id)); if (activeId === id) { setActiveId(null); setMessages([]) } } catch { /* */ } - setDeleting(false) - }, - }) - - const startEdit = (conv: Conversation) => { setEditingId(conv.id); setEditTitle(conv.title); setTimeout(() => editInputRef.current?.focus(), 50) } - const saveTitle = async (id: string) => { - const title = editTitle.trim() - if (title && title !== conversations.find(c => c.id === id)?.title) { - await updateConversation(id, title).catch(() => {}) - setConversations(prev => prev.map(c => c.id === id ? { ...c, title } : c)) - } - setEditingId(null) - } - - 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() } - const prevMessages = [...messages, userMsg] - setMessages(prevMessages) - setStreaming(true) - - const controller = new AbortController() - abortRef.current = controller - - const streamMsgId = (Date.now() + 1).toString() - const streamMsg: Message = { id: streamMsgId, role: 'assistant', content: '', timestamp: Date.now(), streaming: true } - setMessages(prev => [...prev, streamMsg]) - - let currentContent = '' - let completedConvId: string | undefined - - const update = (updates: Partial) => - setMessages(prev => prev.map(m => m.id === streamMsgId ? { ...m, ...updates } : m)) - - 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 '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: `❌ ${event.error}`, streaming: false }) - break - } - }, - controller.signal, - ) - if (completedConvId) loadConversations() - } catch (err: any) { - if (err.name !== 'AbortError') update({ content: `❌ ${err.message}`, streaming: false }) - else update({ streaming: false }) - } finally { - setStreaming(false); abortRef.current = null - } - } - - const handleStop = () => { abortRef.current?.abort(); setStreaming(false) } - +export default function TaskAssistant() { return ( -
-
-
- -
-
- {conversations.map(conv => ( -
activeId !== conv.id && switchConversation(conv.id)} - style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 10px', marginBottom: 2, borderRadius: 8, cursor: 'pointer', background: activeId === conv.id ? token.colorFillSecondary : 'transparent' }}> - - {editingId === conv.id ? ( - setEditTitle(e.target.value)} - onBlur={() => saveTitle(conv.id)} onPressEnter={() => saveTitle(conv.id)} - onClick={e => e.stopPropagation()} style={{ flex: 1, fontSize: 13 }} /> - ) : ( - { e.stopPropagation(); startEdit(conv) }}>{conv.title} - )} -
- ))} - {conversations.length === 0 &&
暂无对话
} -
-
- -
-
- {messages.length === 0 && ( -
- - {activeId ? '开始新对话' : '点击「新对话」开始'} - Hermes Agent · 流式响应 · xhigh 推理 -
- )} - - {messages.map(msg => ( -
-
- {msg.role === 'assistant' - ? (msg.content - ? - : 思考中...) - : msg.content} -
-
- ))} -
-
- -
-
- 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 发送' : '请先新建或选择对话'} - autoSize={{ minRows: 1, maxRows: 5 }} disabled={streaming || !activeId} - variant="borderless" style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 14 }} /> - {streaming ? ( - - ) : ( - - )} -
-
-
+
+