diff --git a/src/components/chat/GlobalChatbox.parts.tsx b/src/components/chat/GlobalChatbox.parts.tsx index 3cd086b..5b03fb8 100644 --- a/src/components/chat/GlobalChatbox.parts.tsx +++ b/src/components/chat/GlobalChatbox.parts.tsx @@ -7,7 +7,9 @@ import { motion } from "framer-motion"; import { Avatar, Box, + Chip, IconButton, + LinearProgress, Paper, Stack, Typography, @@ -15,7 +17,9 @@ import { } from "@mui/material"; import type { Theme } from "@mui/material/styles"; import AutoAwesome from "@mui/icons-material/AutoAwesome"; +import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; import ErrorOutlineRounded from "@mui/icons-material/ErrorOutlineRounded"; +import HourglassEmptyRounded from "@mui/icons-material/HourglassEmptyRounded"; import VolumeUpRounded from "@mui/icons-material/VolumeUpRounded"; import PauseRounded from "@mui/icons-material/PauseRounded"; import PlayArrowRounded from "@mui/icons-material/PlayArrowRounded"; @@ -28,7 +32,7 @@ import { import { ChatInlineChart } from "./ChatInlineChart"; import { ChatToolCallBlock } from "./ChatToolCallBlock"; import markdownStyles from "./GlobalChatboxMarkdown.module.css"; -import type { Message, SpeechState } from "./GlobalChatbox.types"; +import type { ChatProgress, Message, SpeechState } from "./GlobalChatbox.types"; import { stripMarkdown } from "./GlobalChatbox.utils"; export const TypingIndicator = () => { @@ -267,6 +271,9 @@ export const ChatMessageItem = React.memo( : "#475569", }} > + {!isUser && !isErrorMessage && message.progress?.length ? ( + + ) : null} {contentSegments.map((segment, segIdx) => { if (segment.type === "text") { const text = segment.content.trim(); @@ -424,3 +431,75 @@ export const ChatMessageItem = React.memo( ); ChatMessageItem.displayName = "ChatMessageItem"; + +const ChatProgressPanel = ({ progress }: { progress: ChatProgress[] }) => { + const isComplete = progress.some( + (item) => item.phase === "complete" && item.status === "completed", + ); + const latestRunning = isComplete + ? undefined + : [...progress].reverse().find((item) => item.status === "running"); + return ( + + + + + + Agent 过程 + + {latestRunning ? ( + + ) : null} + + {latestRunning ? : null} + + {progress.slice(-5).map((item) => ( + + {item.status === "completed" ? ( + + ) : item.status === "error" ? ( + + ) : ( + + )} + + + {item.title} + + {item.detail ? ( + + {item.detail} + + ) : null} + + + ))} + + + + ); +}; diff --git a/src/components/chat/GlobalChatbox.tsx b/src/components/chat/GlobalChatbox.tsx index 2835684..4868595 100644 --- a/src/components/chat/GlobalChatbox.tsx +++ b/src/components/chat/GlobalChatbox.tsx @@ -316,10 +316,41 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { content: "⚠️ **错误:** Agent 未返回内容,请稍后重试。", isError: true, } - : m + : m.id === assistantId + ? { + ...m, + progress: m.progress?.map((item) => + item.status === "running" + ? { ...item, status: "completed" as const } + : item, + ), + } + : m ) ); setIsStreaming(false); + } else if (event.type === "progress") { + if (!sessionId && event.sessionId) setSessionId(event.sessionId); + setMessages((prev) => + prev.map((m) => { + if (m.id !== assistantId) return m; + const progress = [...(m.progress ?? [])]; + const index = progress.findIndex((item) => item.id === event.id); + const nextProgress = { + id: event.id, + phase: event.phase, + status: event.status, + title: event.title, + detail: event.detail, + }; + if (index >= 0) { + progress[index] = nextProgress; + } else { + progress.push(nextProgress); + } + return { ...m, progress }; + }) + ); } else if (event.type === "error") { setMessages((prev) => prev.map((m) => diff --git a/src/components/chat/GlobalChatbox.types.ts b/src/components/chat/GlobalChatbox.types.ts index b7155da..e8d0a85 100644 --- a/src/components/chat/GlobalChatbox.types.ts +++ b/src/components/chat/GlobalChatbox.types.ts @@ -1,8 +1,17 @@ +export type ChatProgress = { + id: string; + phase: string; + status: "running" | "completed" | "error"; + title: string; + detail?: string; +}; + export type Message = { id: string; role: "user" | "assistant"; content: string; isError?: boolean; + progress?: ChatProgress[]; }; export type Props = { diff --git a/src/lib/chatStream.test.ts b/src/lib/chatStream.test.ts index 4528f51..1db2bd5 100644 --- a/src/lib/chatStream.test.ts +++ b/src/lib/chatStream.test.ts @@ -70,6 +70,33 @@ describe("streamAgentChat", () => { ]); }); + it("parses progress events", async () => { + apiFetch.mockResolvedValue({ + ok: true, + body: makeStream([ + 'event: progress\ndata: {"session_id":"s1","id":"p1","phase":"tool","status":"running","title":"正在调用后端数据查询","detail":"GET /api/v1/demo"}\n\n', + 'event: done\ndata: {"session_id":"s1"}\n\n', + ]), + }); + + const events: Array<{ type: string; title?: string; status?: string; detail?: string }> = []; + + await streamAgentChat({ + message: "hi", + onEvent: (event) => events.push(event), + }); + + expect(events[0]).toEqual({ + type: "progress", + sessionId: "s1", + id: "p1", + phase: "tool", + status: "running", + title: "正在调用后端数据查询", + detail: "GET /api/v1/demo", + }); + }); + it("emits error when response is not ok", async () => { apiFetch.mockResolvedValue({ ok: false, diff --git a/src/lib/chatStream.ts b/src/lib/chatStream.ts index 772658c..ad163ad 100644 --- a/src/lib/chatStream.ts +++ b/src/lib/chatStream.ts @@ -4,6 +4,15 @@ import { config } from "@config/config"; export type StreamEvent = | { type: "token"; sessionId: string; content: string } | { type: "done"; sessionId: string } + | { + type: "progress"; + sessionId: string; + id: string; + phase: string; + status: "running" | "completed" | "error"; + title: string; + detail?: string; + } | { type: "error"; sessionId?: string; @@ -121,6 +130,10 @@ export const streamAgentChat = async ({ detail?: string; tool?: string; params?: Record; + id?: string; + phase?: string; + status?: "running" | "completed" | "error"; + title?: string; }; if (event === "token") { onEvent({ @@ -128,6 +141,16 @@ export const streamAgentChat = async ({ sessionId: parsed.session_id ?? "", content: parsed.content ?? "", }); + } else if (event === "progress") { + onEvent({ + type: "progress", + sessionId: parsed.session_id ?? "", + id: parsed.id ?? `${parsed.phase ?? "progress"}-${Date.now()}`, + phase: parsed.phase ?? "progress", + status: parsed.status ?? "running", + title: parsed.title ?? "正在处理", + detail: parsed.detail, + }); } else if (event === "done") { onEvent({ type: "done", diff --git a/tsconfig.json b/tsconfig.json index f25bd34..a16d12d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -68,7 +68,9 @@ "**/*.ts", "**/*.tsx", ".next*/types/**/*.ts", - ".next*/dev/types/**/*.ts" + ".next*/dev/types/**/*.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], "exclude": [ "node_modules"