diff --git a/src/api.ts b/src/api.ts index 68fc734..10ec045 100644 --- a/src/api.ts +++ b/src/api.ts @@ -95,12 +95,14 @@ export interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; + reasoning?: string | null; parentId?: string | null; createdAt: number; meta?: { retrieved?: RetrievedSnippet[]; toolCalls?: ToolCallTrace[]; aborted?: boolean; + reasoning?: string; } | null; } @@ -536,8 +538,9 @@ export const LLMProviderAPI = { export interface StreamEvents { - onMeta?: (data: { userMsgId: string; retrieved: RetrievedSnippet[]; toolsAvailable: number; mcpToolsCount: number }) => void; - onDelta?: (text: string) => void; + onMeta?: (data: any) => void; + onReasoningDelta?: (text: string) => void; + onDelta?: (text: string) => void; onToolCall?: (data: { id: string; name: string; args: any }) => void; onToolResult?: (data: { id: string; name: string; result: any }) => void; onDone?: (data: { user: ChatMessage; assistant: ChatMessage }) => void; @@ -563,7 +566,7 @@ export async function streamChat( ) { const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/stream`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: JSON.stringify({ content, sessionId, @@ -587,7 +590,7 @@ export async function regenerateMessage( ) { const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/${messageId}/regenerate`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: JSON.stringify({ overrides, attachmentsText }), signal, credentials: 'include' @@ -633,6 +636,9 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) case 'meta': h.onMeta?.(data); break; + case 'reasoning_delta': + h.onReasoningDelta?.(data.content || ''); + break; case 'delta': h.onDelta?.(data.content || ''); break; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index f7a7053..8fea917 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -25,7 +25,9 @@ import PromptLibraryPage from './PromptLibraryPage'; interface StreamingState { active: boolean; - text: string; + reasoningText: string; + answerText: string; + errorMessage: string | null; retrieved: RetrievedSnippet[]; toolCalls: ToolCallTrace[]; } @@ -61,7 +63,9 @@ export default function ChatPage() { const [uploadingAtt, setUploadingAtt] = useState(false); const [streaming, setStreaming] = useState({ active: false, - text: '', + reasoningText: '', + answerText: '', + errorMessage: null, retrieved: [], toolCalls: [] }); @@ -186,7 +190,14 @@ export default function ChatPage() { createdAt: Date.now() }; setMessages((m) => [...(m || []), tempUser]); - setStreaming({ active: true, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ + active: true, + reasoningText: '', + answerText: '', + errorMessage: null, + retrieved: [], + toolCalls: [] + }); scrollBottom(); abortRef.current?.abort(); @@ -203,9 +214,15 @@ export default function ChatPage() { content, { onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), + onReasoningDelta: (chunk) => + setStreaming((s) => { + const next = { ...s, reasoningText: s.reasoningText + chunk }; + scrollBottom(); + return next; + }), onDelta: (chunk) => setStreaming((s) => { - const next = { ...s, text: s.text + chunk }; + const next = { ...s, answerText: s.answerText + chunk }; scrollBottom(); return next; }), @@ -226,8 +243,25 @@ export default function ChatPage() { 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: [] }); + setStreaming((s) => { + const assistant: any = data.assistant; + const reasoningText = + assistant?.reasoning || + assistant?.meta?.reasoning || + assistant?.meta?.reasoningText || + s.reasoningText; + const nextAssistant: ChatMessage = { + ...data.assistant, + meta: { + ...(data.assistant.meta || {}), + retrieved: data.assistant.meta?.retrieved || s.retrieved, + toolCalls: data.assistant.meta?.toolCalls || s.toolCalls, + reasoning: reasoningText || undefined + } + }; + setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), data.user, nextAssistant]); + return { active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }; + }); setSessionRefresh((t) => t + 1); setAttachments([]); // 用完即清 setImageUrls([]); @@ -235,12 +269,19 @@ export default function ChatPage() { }, onAborted: (data) => { // 已停止:保留已生成内容;user 消息也留下(用 tempUser 占位) + const assistant: ChatMessage = { + ...data.assistant, + meta: { + ...(data.assistant.meta || {}), + aborted: true + } + }; setMessages((m) => [ ...(m || []).filter((x) => x.id !== tempUser.id), { ...tempUser, id: 'u-' + data.assistant.id }, // 占位 - data.assistant + assistant ]); - setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }); setSessionRefresh((t) => t + 1); // 重新拉一次确保和后端一致(user 真实 id 在那边) loadMessages(); @@ -248,7 +289,7 @@ export default function ChatPage() { onError: (errMsg) => { msg.error('流式失败:' + errMsg); setMessages((m) => (m || []).filter((x) => x.id !== tempUser.id)); - setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: errMsg, retrieved: [], toolCalls: [] }); } }, ctrl.signal, @@ -261,7 +302,14 @@ export default function ChatPage() { msg.error('请求失败:' + (e?.message ?? e)); } setMessages((m) => (m || []).filter((x) => x.id !== tempUser.id)); - setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ + active: false, + reasoningText: '', + answerText: '', + errorMessage: e?.message ?? String(e), + retrieved: [], + toolCalls: [] + }); } }; @@ -326,7 +374,14 @@ export default function ChatPage() { const handleRegenerate = async (assistantId: string) => { if (!id || sending) return; setSending(true); - setStreaming({ active: true, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ + active: true, + reasoningText: '', + answerText: '', + errorMessage: null, + retrieved: [], + toolCalls: [] + }); abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; @@ -336,9 +391,15 @@ export default function ChatPage() { assistantId, { onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), + onReasoningDelta: (chunk) => + setStreaming((s) => { + const next = { ...s, reasoningText: s.reasoningText + chunk }; + scrollBottom(); + return next; + }), onDelta: (chunk) => setStreaming((s) => { - const next = { ...s, text: s.text + chunk }; + const next = { ...s, answerText: s.answerText + chunk }; scrollBottom(); return next; }), @@ -359,16 +420,16 @@ export default function ChatPage() { return { ...s, toolCalls: list }; }), onDone: () => { - setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }); loadMessages(); }, onAborted: () => { - setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retrieved: [], toolCalls: [] }); loadMessages(); }, onError: (errMsg) => { msg.error('重新生成失败:' + errMsg); - setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); + setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: errMsg, retrieved: [], toolCalls: [] }); loadMessages(); } }, @@ -579,11 +640,29 @@ export default function ChatPage() { {streaming.active && (
- {streaming.text ? ( - {streaming.text + '▍'} - ) : ( - 思考中… - )} +
+
+
+ 推理过程 +
+ {streaming.reasoningText ? ( + {streaming.reasoningText + '▍'} + ) : ( + 等待推理… + )} +
+ +
+
+ 正式回答 +
+ {streaming.answerText ? ( + {streaming.answerText + '▍'} + ) : ( + 等待输出… + )} +
+
{(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (
@@ -1069,6 +1148,7 @@ function MessageItem({ )} {message.role === 'assistant' && message.meta && (
+ {!!message.meta.reasoning && } {!!message.meta.retrieved?.length && } {!!message.meta.toolCalls?.length && }
@@ -1077,6 +1157,30 @@ function MessageItem({ ); } +function ReasoningView({ reasoning }: { reasoning: string }) { + return ( + + 🧠 推理过程 + + ), + children: ( +
+ {reasoning} +
+ ) + } + ]} + /> + ); +} + function RetrievedView({ retrieved, liveStyle }: { retrieved: RetrievedSnippet[]; liveStyle?: boolean }) { return (