import { useEffect, useState, type ReactNode } from "react";
import { Button, Image } from "antd";
import { Brain, CheckCircle2, ChevronDown, ChevronRight, Circle, CircleAlert, FilePenLine, FileText, ListChecks, LoaderCircle, Search, TerminalSquare, Wrench, XCircle } from "lucide-react";
import { Streamdown } from "streamdown";
import { canvasThemes } from "@/lib/canvas-theme";
export type AgentChatAttachment = { id: string; name: string; url: string };
export type AgentChatMessageItem = {
id: string;
role: "user" | "assistant" | "system" | "tool" | "error";
title?: string;
text: string;
meta?: string;
detail?: unknown;
attachments?: AgentChatAttachment[];
/** Present while the message is actively streaming; cleared on completion. */
streamId?: string;
};
export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: { item: AgentChatMessageItem; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onRejectTool?: (id: string) => void; onApproveTool?: (id: string) => void }) {
const isUser = item.role === "user";
const isSystem = item.role === "system";
const color = item.role === "error" ? "#dc2626" : item.role === "tool" ? "#2563eb" : theme.node.text;
if (isSystem) {
return (
{item.text}
{item.meta ? {item.meta} : null}
);
}
if (item.role === "tool") {
if (objectField(item.detail, "status") === "pending") return onRejectTool?.(item.id)} onApprove={() => onApproveTool?.(item.id)} />;
return ;
}
return (
{isUser ? (
{item.text}
) : (
{item.text}
)}
{item.attachments?.length ?
: null}
{item.meta ?
{item.meta}
: null}
);
}
export function AgentPendingToolCard({ summary, detail, theme, onReject, onApprove }: { summary: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onReject?: () => void; onApprove?: () => void }) {
const view = userDetail(detail);
return (
{ if (!view) event.preventDefault(); }}>
等待确认
{view ? : null}
{summary}
{view ? : null}
{onReject || onApprove ? (
} onClick={() => onReject?.()}>
拒绝执行
} style={{ color: "#16a34a" }} onClick={() => onApprove?.()}>
批准执行
) : null}
);
}
export function AgentToolCard({ title, text, detail, theme }: { title: string; text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const plan = planDetail(detail);
if (plan) return ;
const kind = String(objectField(detail, "kind") || "");
if (kind === "reasoning") return ;
if (kind === "command") return ;
const state = toolCardState(title, text, detail);
const view = userDetail(detail);
return (
{ if (!view) event.preventDefault(); }}>
{toolIcon(kind, state.icon)}
{title}
{state.label}
{view ? : null}
{text}
{view ? : null}
);
}
function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const status = String(objectField(detail, "status") || "");
const running = ["inProgress", "in_progress", "running", "started", "pending"].includes(status);
return (
{running ? : }
{running ? "正在思考" : "思考摘要"}
{text}
);
}
function AgentCommandSummary({ text, detail, theme }: { text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const view = userDetail(detail);
const status = String(objectField(detail, "status") || "");
const running = ["inProgress", "in_progress", "running", "started", "pending"].includes(status);
const failed = ["failed", "error"].includes(status);
const color = failed ? "#dc2626" : running ? "#d97706" : theme.node.muted;
return (
{ if (!view) event.preventDefault(); }}>
{running ? : }
{failed ? "命令执行失败" : running ? "正在执行命令" : "已执行 1 条命令"}
{view ? : null}
{text}
{view ? : null}
);
}
function AgentPlanCard({ title, plan, theme }: { title: string; plan: PlanDetail; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const [open, setOpen] = useState(true);
const completed = plan.tasks.filter((item) => item.status === "completed").length;
const state = planCardState(plan, completed);
return (
setOpen(event.currentTarget.open)} className="group min-w-0 flex-1 rounded-xl border px-3 py-2.5 text-left" style={{ borderColor: theme.node.stroke, background: "transparent", color: theme.node.text }}>
{title}
{state.label}
{completed}/{plan.tasks.length}
{plan.explanation ? {plan.explanation}
: null}
{plan.tasks.map((item, index) => {
const task = planTaskState(item.status, theme.node.muted);
return (
{task.icon}
{item.step}
{task.label}
);
})}
);
}
export function AgentWorkingMessage({ text, activityKey, theme }: { text: string; activityKey: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
const startedAt = Date.now();
setElapsed(0);
const timer = window.setInterval(() => setElapsed(Math.floor((Date.now() - startedAt) / 1000)), 1000);
return () => window.clearInterval(timer);
}, [activityKey]);
return (
{text}
{elapsed >= 5 ? {waitingTime(elapsed)} : null}
{elapsed >= 30 ?
响应时间较长,但任务仍在运行。可以继续等待,或点击输入框右侧的停止按钮结束本轮。
: null}
);
}
function waitingTime(seconds: number) {
if (seconds < 60) return `已等待 ${seconds} 秒`;
const minutes = Math.floor(seconds / 60);
return `已等待 ${minutes} 分 ${seconds % 60} 秒`;
}
type PlanTask = { step: string; status: string };
type PlanDetail = { status: string; tasks: PlanTask[]; explanation?: string };
type UserDetail = { kind?: string; status?: string; rows?: Array<{ label: string; value: string }>; output?: string; files?: Array<{ path: string; action?: string }> };
function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
return (
{detail.rows?.length ? (
{detail.rows.map((row) => (
- {row.label}
- {row.value}
))}
) : null}
{detail.files?.length ? (
涉及文件
{detail.files.map((file) => (
{file.path}
{file.action ? {file.action} : null}
))}
) : null}
{detail.output ? (
{detail.status === "failed" || detail.status === "error" ? "错误信息" : "运行输出"}
{detail.output}
) : null}
);
}
function AgentMessageAttachments({ attachments, alignRight }: { attachments: AgentChatAttachment[]; alignRight?: boolean }) {
const [previewUrl, setPreviewUrl] = useState(null);
return (
<>
{attachments.map((item) => (

setPreviewUrl(item.url)}
/>
))}
{previewUrl ? (
!visible && setPreviewUrl(null) }} />
) : null}
>
);
}
function toolCardState(title: string, text: string, detail?: unknown) {
const raw = `${title} ${text} ${normalizeText(objectField(detail, "error"))}`;
const lower = raw.toLowerCase();
const status = String(objectField(detail, "status") || "").toLowerCase();
if (status === "noop" || /未生效|无需|没有找到|没有.*可|已存在/.test(raw)) return { label: "未生效", color: "#d97706", icon: , isError: false };
if (["declined", "rejected", "cancelled", "canceled"].includes(status) || /拒绝|取消/.test(raw)) return { label: "已取消", color: "#dc2626", icon: , isError: true };
if (["failed", "error"].includes(status) || /失败|错误/.test(raw) || lower.includes("failed") || lower.includes("error")) return { label: "执行失败", color: "#dc2626", icon: , isError: true };
if (["inprogress", "in_progress", "running", "started", "pending"].includes(status)) return { label: "进行中", color: "#d97706", icon: , isError: false };
if (["completed", "succeeded", "success"].includes(status) || /完成|成功/.test(raw)) return { label: "已完成", color: "#16a34a", icon: , isError: false };
return { label: "已记录", color: "#2563eb", icon: , isError: false };
}
function toolIcon(kind: string | undefined, fallback: ReactNode) {
if (kind === "search") return ;
if (kind === "file") return ;
if (kind === "plan") return ;
return fallback;
}
function planCardState(plan: PlanDetail, completed: number) {
if (plan.status === "failed") return { label: "执行失败", color: "#dc2626" };
if (["interrupted", "cancelled", "canceled"].includes(plan.status)) return { label: "已停止", color: "#d97706" };
if (completed === plan.tasks.length) return { label: "已完成", color: "#16a34a" };
if (plan.status === "finished") return { label: "已结束", color: "#2563eb" };
return { label: "进行中", color: "#d97706" };
}
function planTaskState(status: string, muted: string) {
if (status === "completed") return { label: "已完成", color: "#16a34a", icon: };
if (status === "inProgress") return { label: "进行中", color: "#d97706", icon: };
return { label: "待处理", color: muted, icon: };
}
function planDetail(value: unknown): PlanDetail | null {
if (!value || typeof value !== "object" || objectField(value, "kind") !== "todo") return null;
const tasks = Array.isArray(objectField(value, "tasks"))
? (objectField(value, "tasks") as unknown[]).flatMap((item) => {
const step = String(objectField(item, "step") || "").trim();
return step ? [{ step, status: String(objectField(item, "status") || "pending") }] : [];
})
: [];
if (!tasks.length) return null;
const explanation = String(objectField(value, "explanation") || "").trim();
return { status: String(objectField(value, "status") || "inProgress"), tasks, ...(explanation ? { explanation } : {}) };
}
function userDetail(value: unknown): UserDetail | null {
if (!value || typeof value !== "object") return null;
const detail = value as Record;
const rows = Array.isArray(detail.rows)
? detail.rows.flatMap((row) => {
if (!row || typeof row !== "object") return [];
const label = String((row as Record).label || "");
const value = String((row as Record).value || "");
return label && value ? [{ label, value }] : [];
})
: [];
const files = Array.isArray(detail.files)
? detail.files.flatMap((file) => {
if (!file || typeof file !== "object") return [];
const path = String((file as Record).path || "");
return path ? [{ path, action: String((file as Record).action || "") || undefined }] : [];
})
: [];
const error = objectField(detail.error, "message");
const output = typeof detail.output === "string" ? detail.output.trim() : typeof error === "string" ? error : "";
if (!rows.length && !files.length && !output) return null;
return { kind: typeof detail.kind === "string" ? detail.kind : undefined, status: typeof detail.status === "string" ? detail.status : undefined, rows, files, output };
}
function normalizeText(value: unknown) {
if (typeof value === "string") return value.trim();
if (value instanceof Error) return value.message;
if (value == null) return "";
return String(objectField(value, "message") || "");
}
function objectField(value: unknown, key: string) {
return value && typeof value === "object" ? (value as Record)[key] : undefined;
}