fix: clean TaskAssistant rewrite
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 8s

This commit is contained in:
WangDL 2026-05-22 17:26:09 +08:00
parent b2c2fd3805
commit 3b7a765c40

View File

@ -38,7 +38,6 @@ function ChatPage() {
const loadConversations = useCallback(async () => { try { setConversations(await listConversations()) } catch {} }, []) const loadConversations = useCallback(async () => { try { setConversations(await listConversations()) } catch {} }, [])
useEffect(() => { loadConversations() }, [loadConversations]) useEffect(() => { loadConversations() }, [loadConversations])
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages])
useEffect(() => { if (!activeId && conversations.length > 0) switchConversation(conversations[0].id) }, [conversations]) useEffect(() => { if (!activeId && conversations.length > 0) switchConversation(conversations[0].id) }, [conversations])
const switchConversation = useCallback(async (id: string) => { const switchConversation = useCallback(async (id: string) => {
@ -54,7 +53,11 @@ function ChatPage() {
const handleDelete = (id: string) => modal.confirm({ const handleDelete = (id: string) => modal.confirm({
title: '删除对话', okText: '删除', okType: 'danger', cancelText: '取消', title: '删除对话', 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) }, 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 startEdit = (conv: Conversation) => { setEditingId(conv.id); setEditTitle(conv.title); setTimeout(() => editInputRef.current?.focus(), 50) }
@ -80,18 +83,43 @@ function ChatPage() {
try { try {
await streamChat(prevMessages.map(m => ({ role: m.role, content: m.content })), activeId, (event: StreamEvent) => { await streamChat(prevMessages.map(m => ({ role: m.role, content: m.content })), activeId, (event: StreamEvent) => {
switch (event.event) { switch (event.event) {
case 'meta': completedConvId = event.conversationId; if (event.conversationId && event.conversationId !== activeId) { setActiveId(event.conversationId); loadConversations() } break case 'meta':
case 'tool.started': currentTools.push({ tool: event.tool, preview: event.preview, done: false }); update({ toolCalls: [...currentTools] }) break completedConvId = event.conversationId
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 if (event.conversationId && event.conversationId !== activeId) { setActiveId(event.conversationId); loadConversations() }
case 'message.delta': currentContent += event.delta || ''; update({ content: currentContent, streaming: true }) break break
case 'run.completed': if (event.output) currentContent = event.output; update({ content: currentContent, streaming: false }) break case 'tool.started':
case 'done': completedConvId = event.conversationId || completedConvId; update({ streaming: false }) break currentTools.push({ tool: event.tool, preview: event.preview, done: false })
case 'error': update({ content: `${event.error}`, streaming: false }) break 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 '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) }, controller.signal)
if (completedConvId) loadConversations() if (completedConvId) loadConversations()
} catch (err: any) { } catch (err: any) {
if (err.name !== 'AbortError') update({ content: `${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); abortRef.current = null } } finally { setStreaming(false); abortRef.current = null }
} }
@ -100,8 +128,8 @@ function ChatPage() {
return ( return (
<div style={{ display: 'flex', height: 'calc(100vh - 112px)', gap: 0 }}> <div style={{ display: 'flex', height: 'calc(100vh - 112px)', gap: 0 }}>
{/* Sidebar */} {/* Sidebar */}
<div style={{ width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column', background: token.colorBgContainer, borderRight: `1px solid ${token.colorBorderSecondary}` }}> <div style={{ width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column', background: token.colorBgContainer, borderRight: '1px solid ' + token.colorBorderSecondary }}>
<div style={{ padding: '12px 16px', borderBottom: `1px solid ${token.colorBorderSecondary}` }}> <div style={{ padding: '12px 16px', borderBottom: '1px solid ' + token.colorBorderSecondary }}>
<Button type="primary" icon={<PlusOutlined />} block onClick={handleNew}></Button> <Button type="primary" icon={<PlusOutlined />} block onClick={handleNew}></Button>
</div> </div>
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}> <div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
@ -126,7 +154,6 @@ function ChatPage() {
{/* Chat */} {/* Chat */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', background: token.colorBgLayout }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', background: token.colorBgLayout }}>
{/* Messages */}
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}> <div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
{messages.length === 0 ? ( {messages.length === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12 }}>
@ -140,7 +167,6 @@ function ChatPage() {
<div style={{ maxWidth: 800, margin: '0 auto' }}> <div style={{ maxWidth: 800, margin: '0 auto' }}>
{messages.map(msg => ( {messages.map(msg => (
<div key={msg.id} style={{ marginBottom: 24 }}> <div key={msg.id} style={{ marginBottom: 24 }}>
{/* Role label */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<div style={{ width: 28, height: 28, borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', <div style={{ width: 28, height: 28, borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center',
background: msg.role === 'user' ? token.colorPrimary : token.colorSuccess }}> background: msg.role === 'user' ? token.colorPrimary : token.colorSuccess }}>
@ -150,30 +176,27 @@ function ChatPage() {
{msg.streaming && <Text type="secondary" style={{ fontSize: 11 }}>...</Text>} {msg.streaming && <Text type="secondary" style={{ fontSize: 11 }}>...</Text>}
</div> </div>
{/* Tool calls */}
{msg.toolCalls && msg.toolCalls.length > 0 && ( {msg.toolCalls && msg.toolCalls.length > 0 && (
<div style={{ marginBottom: 8, marginLeft: 36, display: 'flex', flexDirection: 'column', gap: 4 }}> <div style={{ marginBottom: 8, marginLeft: 36, display: 'flex', flexDirection: 'column', gap: 4 }}>
{msg.toolCalls.map((t, i) => ( {msg.toolCalls.map((t, i) => (
<div key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 12px', borderRadius: 8, <div key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 12px', borderRadius: 8,
background: t.done ? (t.error ? token.colorErrorBg : token.colorSuccessBg) : token.colorFillSecondary, background: t.done ? (t.error ? token.colorErrorBg : token.colorSuccessBg) : token.colorFillSecondary,
border: `1px solid ${t.done ? (t.error ? token.colorErrorBorder : token.colorSuccessBorder) : token.colorBorderSecondary}`, border: '1px solid ' + (t.done ? (t.error ? token.colorErrorBorder : token.colorSuccessBorder) : token.colorBorderSecondary),
fontSize: 12, maxWidth: 'fit-content' }}> fontSize: 12, maxWidth: 'fit-content' }}>
<ToolOutlined style={{ color: t.done ? (t.error ? token.colorError : token.colorSuccess) : token.colorTextSecondary }} /> <ToolOutlined style={{ color: t.done ? (t.error ? token.colorError : token.colorSuccess) : token.colorTextSecondary }} />
<Text style={{ fontSize: 12 }}>{t.preview || t.tool}</Text> <Text style={{ fontSize: 12 }}>{t.preview || t.tool}</Text>
{t.done {t.done
? <Text style={{ fontSize: 11, color: t.error ? token.colorError : token.colorSuccess }}>{t.error ? '失败' : `${t.duration?.toFixed(1)}s`}</Text> ? <Text style={{ fontSize: 11, color: t.error ? token.colorError : token.colorSuccess }}>{t.error ? '失败' : (t.duration ? t.duration.toFixed(1) + 's' : '')}</Text>
: <Text type="secondary" style={{ fontSize: 11 }}></Text>} : <Text type="secondary" style={{ fontSize: 11 }}></Text>}
</div> </div>
))} ))}
</div> </div>
)} )}
{/* Content */}
<div style={{ marginLeft: 36, padding: '12px 16px', borderRadius: 12, lineHeight: 1.85, fontSize: 14, <div style={{ marginLeft: 36, padding: '12px 16px', borderRadius: 12, lineHeight: 1.85, fontSize: 14,
background: msg.role === 'user' ? token.colorPrimary : token.colorBgContainer, background: msg.role === 'user' ? token.colorPrimary : token.colorBgContainer,
color: msg.role === 'user' ? '#fff' : token.colorText, color: msg.role === 'user' ? '#fff' : token.colorText,
border: msg.role === 'assistant' ? `1px solid ${token.colorBorderSecondary}` : 'none', border: msg.role === 'assistant' ? '1px solid ' + token.colorBorderSecondary : 'none' }}>
boxShadow: msg.role === 'assistant' ? 'none' : undefined }}>
{msg.role === 'assistant' {msg.role === 'assistant'
? (msg.content ? <Markdown content={msg.content + (msg.streaming ? '▊' : '')} /> : <Text type="secondary">...</Text>) ? (msg.content ? <Markdown content={msg.content + (msg.streaming ? '▊' : '')} /> : <Text type="secondary">...</Text>)
: msg.content} : msg.content}
@ -185,10 +208,9 @@ function ChatPage() {
)} )}
</div> </div>
{/* Input */} <div style={{ padding: '16px 32px 20px', background: token.colorBgContainer, borderTop: '1px solid ' + token.colorBorderSecondary }}>
<div style={{ padding: '16px 32px 20px', background: token.colorBgContainer, borderTop: `1px solid ${token.colorBorderSecondary}` }}>
<div style={{ maxWidth: 800, margin: '0 auto' }}> <div style={{ maxWidth: 800, margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 10, background: token.colorFillTertiary, borderRadius: 16, padding: '10px 10px 10px 18px', border: `1px solid ${token.colorBorderSecondary}` }}> <div style={{ display: 'flex', alignItems: 'flex-end', gap: 10, background: token.colorFillTertiary, borderRadius: 16, padding: '10px 10px 10px 18px', border: '1px solid ' + token.colorBorderSecondary }}>
<Input.TextArea value={input} onChange={e => setInput(e.target.value)} <Input.TextArea value={input} onChange={e => setInput(e.target.value)}
onCompositionStart={() => { composingRef.current = true }} onCompositionEnd={() => { composingRef.current = false }} onCompositionStart={() => { composingRef.current = true }} onCompositionEnd={() => { composingRef.current = false }}
onKeyDown={e => { if (composingRef.current) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }} onKeyDown={e => { if (composingRef.current) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }}