import { useEffect, useRef, useState, type ReactNode } from "react";
import { Button, Tooltip } from "antd";
import { ArrowUp, CheckCircle2, CircleAlert, ImagePlus, LoaderCircle, Square, UserRound, Wrench, X, XCircle } from "lucide-react";
import { Streamdown } from "streamdown";
import { isPlainEnterKey } from "@/lib/keyboard-event";
import { canvasThemes } from "@/lib/canvas-theme";
import type { LocalUser } from "@/stores/use-user-store";
export type CanvasAgentChatAttachment = { id: string; name: string; url: string };
export type CanvasAgentChatMessage = {
id: string;
role: "user" | "assistant" | "system" | "tool" | "error";
title?: string;
text: string;
meta?: string;
detail?: unknown;
attachments?: CanvasAgentChatAttachment[];
/** Present while the message is actively streaming; cleared on completion. */
streamId?: string;
};
const WORKING_TEXT = "working...";
export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveTool }: { item: CanvasAgentChatMessage; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; user: LocalUser | null; 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 ?
: null}
{isUser ? (
{item.text}
) : (
{item.text}
)}
{item.attachments?.length ?
: null}
{item.meta ?
{item.meta}
: null}
{isUser ?
: null}
);
}
export function AgentPendingToolCard({ summary, detail, theme, onReject, onApprove }: { summary: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onReject?: () => void; onApprove?: () => void }) {
return (
确认工具调用
等待确认
{detail ? 详情 : null}
{summary}
{detail ? : null}
{onReject || onApprove ? (
} onClick={() => onReject?.()}>
拒绝执行
} style={{ borderColor: "rgba(22,163,74,.42)", color: "#16a34a", background: "transparent" }} onClick={() => onApprove?.()}>
批准执行
) : null}
);
}
export function AgentToolCard({ title, text, detail, theme }: { title: string; text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const state = toolCardState(title, text, detail);
return (
{state.icon}
{title}
{state.label}
{detail ? 详情 : null}
{text}
{detail ? : null}
);
}
export function AgentWorkingMessage({ theme }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const [length, setLength] = useState(1);
useEffect(() => {
const timer = window.setInterval(() => setLength((value) => (value >= WORKING_TEXT.length + 4 ? 1 : value + 1)), 120);
return () => window.clearInterval(timer);
}, [setLength]);
return (
{WORKING_TEXT.slice(0, Math.min(length, WORKING_TEXT.length))}
);
}
export function AgentChatComposer({
prompt,
attachments = [],
disabled,
sending,
placeholder,
theme,
onPromptChange,
onSubmit,
onStop,
onAddFiles,
onRemoveAttachment,
left,
}: {
prompt: string;
attachments?: CanvasAgentChatAttachment[];
disabled?: boolean;
sending?: boolean;
placeholder: string;
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
onPromptChange: (value: string) => void;
onSubmit: () => void;
onStop?: () => void;
onAddFiles?: (files: FileList | File[] | null) => void | Promise;
onRemoveAttachment?: (id: string) => void;
left?: ReactNode;
}) {
const fileInputRef = useRef(null);
const canSubmit = !disabled && !sending && Boolean(prompt.trim() || attachments.length);
return (
event.stopPropagation()}>
{attachments.length ? (
{attachments.map((item) => (

{onRemoveAttachment ? (
) : null}
))}
) : null}
);
}
export function AgentPanelTabs({ value, items, theme, right, onChange }: { value: T; items: { value: T; label: string; icon?: ReactNode; count?: number }[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; right?: ReactNode; onChange: (value: T) => void }) {
return (
{right ?
{right}
: null}
);
}
function AgentDetailBlock({ detail, theme }: { detail: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
return (
{JSON.stringify(detail, null, 2)}
);
}
function AgentAvatar({ theme }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
return (
);
}
function AgentUserAvatar({ user, theme }: { user: LocalUser | null; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const avatarUrl = user?.avatarUrl?.trim();
return (
{avatarUrl ?
: }
);
}
function AgentMessageAttachments({ attachments }: { attachments: CanvasAgentChatAttachment[] }) {
return (
{attachments.map((item) => (

))}
);
}
function toolCardState(title: string, text: string, detail?: unknown) {
const raw = `${title} ${text} ${normalizeText(objectField(detail, "error"))}`;
const lower = raw.toLowerCase();
const tool = String(objectField(detail, "name") || objectField(detail, "tool") || "");
if (objectField(detail, "status") === "noop" || /未生效|无需|没有找到|没有.*可|已存在/.test(raw)) return { label: "未生效", color: "#d97706", softBorder: "rgba(217,119,6,.22)", softBg: "rgba(217,119,6,.04)", icon: , isError: false };
if (/拒绝|取消/.test(raw) || lower.includes("rejected")) return { label: "拒绝执行", color: "#dc2626", softBorder: "rgba(220,38,38,.20)", softBg: "rgba(220,38,38,.04)", icon: , isError: true };
if (/失败|错误/.test(raw) || lower.includes("failed") || lower.includes("error")) return { label: "执行失败", color: "#dc2626", softBorder: "rgba(220,38,38,.20)", softBg: "rgba(220,38,38,.04)", icon: , isError: true };
if (/完成|成功/.test(raw) || lower.includes("completed") || lower.includes("succeeded")) return { label: tool === "canvas_apply_ops" || /画布操作/.test(title) ? "已批准执行" : "执行完成", color: "#16a34a", softBorder: "rgba(22,163,74,.20)", softBg: "rgba(22,163,74,.04)", icon: , isError: false };
return { label: "工具调用", color: "#2563eb", softBorder: "rgba(37,99,235,.20)", softBg: "rgba(37,99,235,.04)", icon: , isError: false };
}
function normalizeText(value: unknown) {
if (typeof value === "string") return value.trim();
if (value instanceof Error) return value.message;
if (value == null) return "";
return JSON.stringify(value, null, 2);
}
function objectField(value: unknown, key: string) {
return value && typeof value === "object" ? (value as Record)[key] : undefined;
}