import { useState, useRef, useEffect, useCallback } from 'react' import { Typography, Input, Button, Space, Avatar, Spin, theme, Modal, Tooltip } from 'antd' import { SendOutlined, RobotOutlined, UserOutlined, PlusOutlined, DeleteOutlined, StopOutlined, MessageOutlined, } from '@ant-design/icons' import { sendMessage } from '@/services/ai-chat' import { listConversations, createConversation, deleteConversation } from '@/services/conversation-api' import Markdown from '@/components/Markdown' const { Text } = Typography const { TextArea } = Input interface Message { id: string role: 'user' | 'assistant' content: string timestamp: number } interface Conversation { id: string title: string updatedAt: string } export default function TaskAssistant() { const [conversations, setConversations] = useState([]) const [activeId, setActiveId] = useState(null) const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const abortRef = useRef(null) const messagesEndRef = useRef(null) const { token } = theme.useToken() // Load conversations useEffect(() => { listConversations().then(setConversations).catch(() => {}) }, []) // Scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) // Switch conversation const switchConversation = useCallback((id: string) => { if (loading) { abortRef.current?.abort() setLoading(false) } setActiveId(id) setMessages([]) setInput('') }, [loading]) // New conversation const handleNew = async () => { if (loading) { abortRef.current?.abort() setLoading(false) } try { const conv = await createConversation() setConversations(prev => [conv, ...prev]) setActiveId(conv.id) setMessages([]) setInput('') } catch { /* ignore */ } } // Delete conversation const handleDelete = async (id: string, e: React.MouseEvent) => { e.stopPropagation() Modal.confirm({ title: '删除对话', content: '确定要删除这个对话吗?', okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { await deleteConversation(id).catch(() => {}) setConversations(prev => prev.filter(c => c.id !== id)) if (activeId === id) { setActiveId(null) setMessages([]) } }, }) } // Send message const handleSend = async () => { const text = input.trim() if (!text || loading) return const userMsg: Message = { id: Date.now().toString(), role: 'user', content: text, timestamp: Date.now(), } const newMessages = [...messages, userMsg] setMessages(newMessages) setInput('') setLoading(true) const controller = new AbortController() abortRef.current = controller try { const result = await sendMessage( newMessages.map(m => ({ role: m.role, content: m.content })), activeId ?? undefined, controller.signal, ) // Use returned conversationId if this was auto-created if (result.conversationId && !activeId) { setActiveId(result.conversationId) const conv = await createConversation( newMessages[newMessages.length - 1]?.content?.slice(0, 30) ).catch(() => null) if (conv) setConversations(prev => [conv, ...prev]) } const assistantMsg: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: result.content, timestamp: Date.now(), } setMessages(prev => [...prev, assistantMsg]) // Refresh conversation list for updated timestamps listConversations().then(setConversations).catch(() => {}) } catch (err: any) { if (err.name === 'AbortError') return const errorMsg: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: '请求失败:' + (err instanceof Error ? err.message : '未知错误'), timestamp: Date.now(), } setMessages(prev => [...prev, errorMsg]) } finally { setLoading(false) abortRef.current = null } } // Stop generation const handleStop = () => { abortRef.current?.abort() setLoading(false) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSend() } } return (
{/* Sidebar */}
{conversations.map(conv => (
switchConversation(conv.id)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', margin: '2px 0', borderRadius: 8, cursor: 'pointer', background: activeId === conv.id ? token.colorFillSecondary : 'transparent', transition: 'background 0.2s', }} onMouseEnter={e => (e.currentTarget.style.background = token.colorFillSecondary)} onMouseLeave={e => { if (activeId !== conv.id) e.currentTarget.style.background = 'transparent' }} > {conv.title} handleDelete(conv.id, e)} style={{ fontSize: 12, color: token.colorTextQuaternary, opacity: 0, transition: 'opacity 0.2s' }} className="conv-delete-icon" />
))} {conversations.length === 0 && (
暂无对话
)}
{/* Chat area */}
{messages.length === 0 && (
{activeId ? '开始新对话' : '点击左侧「新对话」开始'} Hermes Agent 通过 DeepSeek 提供回答
)} {messages.map(msg => (
: } style={{ backgroundColor: msg.role === 'user' ? token.colorPrimary : token.colorSuccess, flexShrink: 0, }} />
{msg.role === 'assistant' ? : msg.content }
))} {loading && (
} style={{ backgroundColor: token.colorSuccess, flexShrink: 0 }} />
Hermes 思考中...
)}
{/* Input area */}