Files
ttstd-infinite-canvas/web/src/components/canvas/canvas-agent-panel-chrome.tsx

148 lines
9.4 KiB
TypeScript

import { Button, Switch, Tooltip } from "antd";
import { BookOpenCheck, BookOpenText, Bot, Clapperboard, Focus, Globe2, History, LayoutTemplate, Laptop, PanelRightClose, PanelsTopLeft, Plus, RotateCcw, Workflow } from "lucide-react";
import { useNavigate } from "react-router";
import type { CanvasContextSummary } from "@/lib/canvas/canvas-context-summary";
import type { CanvasTheme } from "@/lib/canvas-theme";
import { useUserStore } from "@/stores/use-user-store";
import type { CanvasAgentMode } from "./canvas-agent-chat-ui";
export function AgentPanelChrome({
theme,
mode,
context,
referenceCount,
confirmTools,
canUndo,
undoCount,
onModeChange,
onConfirmToolsChange,
onUndo,
onCollapse,
historyCount = 0,
historyActive = false,
onOpenHistory,
onNewChat,
newChatDisabled = false,
}: {
theme: CanvasTheme;
mode: CanvasAgentMode;
context: CanvasContextSummary;
referenceCount: number;
confirmTools: boolean;
canUndo: boolean;
undoCount: number;
onModeChange: (mode: CanvasAgentMode) => void;
onConfirmToolsChange: (confirm: boolean) => void;
onUndo: () => void;
onCollapse: () => void;
historyCount?: number;
historyActive?: boolean;
onOpenHistory?: () => void;
onNewChat?: () => void;
newChatDisabled?: boolean;
}) {
const navigate = useNavigate();
return (
<header className="shrink-0 px-3 pb-1.5 pt-2.5">
<div className="flex min-w-0 items-center gap-2">
<span className="grid size-8 shrink-0 place-items-center rounded-lg" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }}>
<Bot className="size-4" />
</span>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-baseline gap-1.5">
<div className="truncate text-sm font-semibold leading-5">Agent</div>
<span className="truncate text-[var(--fs-label)]" style={{ color: theme.node.muted }}>画布协作</span>
</div>
</div>
<div className="ml-auto flex shrink-0 items-center gap-0.5">
<Tooltip title="技能库">
<Button type="text" shape="circle" className="!h-7 !w-7 !min-w-7" style={{ color: theme.node.muted }} icon={<BookOpenCheck className="size-3.5" />} onClick={() => navigate("/skills")} aria-label="打开技能库" />
</Tooltip>
{onOpenHistory ? (
<Tooltip title={historyCount ? `历史会话 · ${historyCount}` : "历史会话"}>
<Button type="text" className={`!h-7 !min-w-7 !px-1.5 ${historyActive ? "font-medium" : ""}`} style={{ color: historyActive ? theme.node.text : theme.node.muted, background: historyActive ? theme.spatial.surface : "transparent" }} icon={<History className="size-3.5" />} onClick={onOpenHistory} aria-label="打开历史会话">
{historyCount ? <span className="text-[var(--fs-tiny)] tabular-nums">{historyCount}</span> : null}
</Button>
</Tooltip>
) : null}
{onNewChat ? (
<Tooltip title="新对话">
<Button type="text" shape="circle" className="!h-7 !w-7 !min-w-7" disabled={newChatDisabled} style={{ color: theme.node.muted }} icon={<Plus className="size-3.5" />} onClick={onNewChat} aria-label="新建对话" />
</Tooltip>
) : null}
<Tooltip title="收起 Agent">
<Button type="text" shape="circle" className="!h-7 !w-7 !min-w-7" style={{ color: theme.node.muted }} icon={<PanelRightClose className="size-3.5" />} onClick={onCollapse} aria-label="收起 Agent" />
</Tooltip>
</div>
</div>
<div className="mt-1 flex min-h-7 flex-wrap items-center gap-x-2 gap-y-0.5 px-0.5 text-[var(--fs-label)]" style={{ color: theme.node.muted }}>
<span className="font-medium" style={{ color: theme.node.text }}>{context.nodeCount} 个节点</span>
{context.selectedCount ? <span className="inline-flex items-center gap-1"><Focus className="size-3" />选中 {context.selectedCount}</span> : <span>未选择节点</span>}
{context.chapterLabel ? <span className="inline-flex min-w-0 items-center gap-1"><BookOpenText className="size-3 shrink-0" /><span className="max-w-32 truncate">{context.chapterLabel}{context.shotLabel ? ` · ${context.shotLabel}` : ""}</span></span> : null}
{referenceCount ? <span>{referenceCount} 个参考</span> : null}
<div className="ml-auto flex shrink-0 items-center gap-1.5">
<AgentModeSwitch value={mode} theme={theme} onChange={onModeChange} />
<Tooltip title={undoCount ? `撤销最近一批 Agent 写回,可撤销 ${undoCount} 批` : "没有可撤销的 Agent 写回"}>
<Button type="text" shape="circle" className="!h-6 !w-6 !min-w-6" disabled={!canUndo} style={{ color: theme.node.muted }} icon={<RotateCcw className="size-3" />} onClick={onUndo} aria-label="撤销最近一批 Agent 写回" />
</Tooltip>
<label className="flex h-6 cursor-pointer items-center gap-1 rounded-md px-1" style={{ color: theme.node.muted }}>
<Switch size="small" checked={confirmTools} onChange={onConfirmToolsChange} />
<span className="whitespace-nowrap">确认</span>
</label>
</div>
</div>
</header>
);
}
function AgentModeSwitch({ value, theme, onChange }: { value: CanvasAgentMode; theme: CanvasTheme; onChange: (value: CanvasAgentMode) => void }) {
return (
<div className="inline-flex h-7 shrink-0 items-center rounded-full p-0.5 text-[var(--fs-label)]" style={{ background: `${theme.node.text}08`, boxShadow: `inset 0 1px 0 ${theme.node.text}08` }} role="group" aria-label="Agent 运行位置">
{(["online", "local"] as const).map((item) => {
const active = value === item;
const Icon = item === "online" ? Globe2 : Laptop;
return (
<button key={item} type="button" className="inline-flex h-6 items-center gap-1 rounded-full px-2 transition-[background-color,color,transform] duration-200 active:scale-95" style={{ background: active ? theme.node.fill : "transparent", color: active ? theme.node.text : theme.node.muted, boxShadow: active ? `0 4px 12px ${theme.spatial.shadow}` : "none" }} onClick={() => onChange(item)} aria-pressed={active}>
<Icon className="size-3" />
{item === "online" ? "网站" : "本机"}
</button>
);
})}
</div>
);
}
const starterActions = [
{ label: "搭建短剧工作流", icon: Clapperboard },
{ label: "整理当前画布", icon: LayoutTemplate },
{ label: "生成镜头分镜", icon: PanelsTopLeft },
{ label: "检查节点连线", icon: Workflow },
];
export function AgentChatEmptyState({ theme, nodeCount, onSelect }: { theme: CanvasTheme; nodeCount: number; onSelect: (value: string) => void }) {
const shortDramaEnabled = useUserStore((state) => state.features.shortDramaEnabled);
const visibleStarterActions = shortDramaEnabled ? starterActions : starterActions.filter((item) => item.label !== "搭建短剧工作流");
return (
<div className="flex h-full items-center px-5 py-8">
<div className="mx-auto w-full max-w-[380px]">
<div className="flex items-center gap-2">
<span className="grid size-7 place-items-center rounded-md" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }}><Bot className="size-3.5" /></span>
<span className="text-[var(--fs-label)] font-medium" style={{ color: theme.node.muted }}>{nodeCount} 个节点已就绪</span>
</div>
<h2 className="mt-3 text-[var(--fs-heading-lg)] font-semibold leading-6" style={{ color: theme.node.text }}>从当前画布开始</h2>
<div className="mt-4 grid grid-cols-1 gap-1">
{visibleStarterActions.map(({ label, icon: Icon }) => (
<button key={label} type="button" className="group flex min-h-11 min-w-0 items-center gap-2.5 rounded-md px-2.5 text-left text-xs font-medium transition-colors" style={{ color: theme.node.text }} onMouseEnter={(event) => { event.currentTarget.style.background = theme.spatial.surface; }} onMouseLeave={(event) => { event.currentTarget.style.background = "transparent"; }} onFocus={(event) => { event.currentTarget.style.background = theme.spatial.surface; }} onBlur={(event) => { event.currentTarget.style.background = "transparent"; }} onClick={() => onSelect(label)}>
<span className="grid size-7 shrink-0 place-items-center rounded-md" style={{ background: theme.spatial.surface, color: theme.node.muted }}><Icon className="size-3.5" /></span>
<span className="min-w-0 truncate">{label}</span>
</button>
))}
</div>
</div>
</div>
);
}