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 ? (
) : 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) => (
{item.name} {onRemoveAttachment ? ( ) : null}
))}
) : null}