import { useEffect, useRef, useState } from 'react'; import { Button, Input, Space, App as AntApp, Empty, Spin } from 'antd'; import ReactMarkdown from 'react-markdown'; import { Agent, ChatMessage, streamChat, RetrievedSnippet, ToolCallTrace } from '../api'; interface Props { agent: Agent; agentId?: string; } interface StreamingState { active: boolean; text: string; retrieved: RetrievedSnippet[]; toolCalls: ToolCallTrace[]; } export default function ChatPreview({ agent, agentId }: Props) { const { message: msg } = AntApp.useApp(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); const [streaming, setStreaming] = useState({ active: false, text: '', retrieved: [], toolCalls: [] }); const bodyRef = useRef(null); const abortRef = useRef(null); const scrollBottom = () => { requestAnimationFrame(() => { bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight, behavior: 'smooth' }); }); }; useEffect(() => { return () => abortRef.current?.abort(); }, []); const handleSend = async () => { const text = input.trim(); if (!text || !agentId || sending) return; setInput(''); setSending(true); const tempUser: ChatMessage = { id: 'tmp-' + Date.now(), role: 'user', content: text, createdAt: Date.now() }; setMessages((m) => [...m, tempUser]); setStreaming({ active: true, text: '', retrieved: [], toolCalls: [] }); scrollBottom(); abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; try { await streamChat( agentId, text, { onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), onDelta: (chunk) => setStreaming((s) => { const next = { ...s, text: s.text + chunk }; scrollBottom(); return next; }), onToolCall: (data) => setStreaming((s) => ({ ...s, toolCalls: [...s.toolCalls, { name: data.name, args: data.args, result: { pending: true } }] })), onToolResult: (data) => setStreaming((s) => { const list = [...s.toolCalls]; for (let i = list.length - 1; i >= 0; i--) { if (list[i].name === data.name && (list[i].result as any)?.pending) { list[i] = { ...list[i], result: data.result }; break; } } return { ...s, toolCalls: list }; }), onDone: (data) => { setMessages((m) => [...m.filter((x) => x.id !== tempUser.id), data.user, data.assistant]); setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); scrollBottom(); }, onError: (errMsg) => { msg.error('预览失败:' + errMsg); setMessages((m) => m.filter((x) => x.id !== tempUser.id)); setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); } }, ctrl.signal ); } catch (e: any) { if (e?.name !== 'AbortError') { msg.error('请求失败:' + (e?.message ?? e)); } setMessages((m) => m.filter((x) => x.id !== tempUser.id)); setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); } finally { setSending(false); } }; const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('/'); return (
{isImageUrl(agent.avatar) ? ( avatar ) : ( (agent?.name?.charAt(0) || '?').toUpperCase() )}
预览
{messages.length === 0 && !streaming.active ? ( ) : (
{messages.map((m) => (
{m.content}
))} {streaming.active && (
{streaming.text ? ( {streaming.text + '▍'} ) : ( 思考中... )}
)}
)}
setInput(e.target.value)} placeholder="输入消息测试..." autoSize={{ minRows: 1, maxRows: 4 }} onPressEnter={(e) => { if (!e.shiftKey) { e.preventDefault(); handleSend(); } }} disabled={sending || !agentId} className="rounded-lg" />
{!agentId && (
请先保存智能体以启用预览
)}
); }