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"