feat(canvas): 完善画布 Agent 交互能力

This commit is contained in:
ddcat666 committed 2026-09-27 18:31:37 +08:00
1 parent 367145010f
commit 05f0c7a64b
12 files changed
+1567 -488

No files matched your search

@@ -1,10 +1,53 @@
import { agentCanvasActions, agentCanvasActionLabel } from "@/lib/canvas/agent-canvas-actions";
import { Button } from "antd";
import { Tooltip } from "@/components/ui/base/tooltip";
import { useCallback, useEffect, useId, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
type ClipboardEvent as ReactClipboardEvent,
type CSSProperties,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
type ReactNode,
type SyntheticEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { ArrowLeft, ArrowUp, AtSign, Bookmark, BrainCircuit, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Clapperboard, Eye, HelpCircle, ImagePlus, Layers3, List, ListChecks, LoaderCircle, MessageCircle, Palette, Pencil, Plus, RotateCcw, Shapes, Share2, ShoppingBag, Sparkles, Square, UserRound, Wrench, X, XCircle } from "lucide-react";
import {
ArrowLeft,
ArrowUp,
AtSign,
Bookmark,
CheckCircle2,
ChevronDown,
ChevronUp,
CircleAlert,
CircleDot,
Clapperboard,
Eye,
HelpCircle,
ImagePlus,
Layers3,
List,
ListChecks,
LoaderCircle,
Palette,
Pencil,
Plus,
RotateCcw,
Shapes,
Share2,
ShoppingBag,
Sparkles,
Square,
Wrench,
X,
XCircle,
} from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
@@ -28,13 +71,36 @@ type CloudAgentOperationImpact = {
warning?: string;
};
export type CloudAgentPlanItem = { id: string; title: string; status: "pending" | "doing" | "done" };
export type CloudAgentFormOption = { id?: string; label: string; detail?: string; recommended?: boolean };
export type CloudAgentFormField = {
id: string;
title: string;
type: "single_select" | "segmented" | "text" | "textarea" | "model_picker";
options?: CloudAgentFormOption[];
defaultValue?: string;
required?: boolean;
allowCustom?: boolean;
placeholder?: string;
};
export type CloudAgentUserQuestion = {
question: string;
options: Array<{ label: string; detail?: string }>;
fields?: CloudAgentFormField[];
kind?: "choice" | "form";
questionId?: string;
allowFreeform?: boolean;
round?: number;
maxRounds?: number;
};
export type CloudAgentFormAnswer = {
type: "form_answer";
questionId?: string;
answers: Record<string, string>;
displayAnswers?: Record<string, string>;
fieldTitles?: Record<string, string>;
skippedFields?: string[];
useRecommendedDefaults?: boolean;
};
export type CloudAgentChatMessage = {
id: string;
role: "user" | "assistant" | "system" | "tool" | "error";
@@ -53,6 +119,7 @@ export type CloudAgentChatMessage = {
/** 运行已进入终态,但计划项仍未全部完成;用于历史恢复时停止显示 loading。 */
planTerminal?: boolean;
question?: CloudAgentUserQuestion;
formAnswer?: CloudAgentFormAnswer;
meta?: string;
detail?: unknown;
attachments?: CloudAgentChatAttachment[];
@@ -75,6 +142,81 @@ export function agentControlMessage(id: string, text: string, meta?: string): Cl
return { id, role: "system", text, meta };
}
const AGENT_FORM_FIELD_LABELS: Record<string, string> = {
genre: "题材",
aspectRatio: "画幅",
style: "画风",
visualStyle: "画风",
comedyStyle: "喜剧风格",
projectType: "项目类型",
model: "模型",
notes: "补充说明",
};
const AGENT_FORM_VALUE_LABELS: Record<string, string> = {
costume_time_travel: "古装穿越",
absurd: "荒诞",
};
function formAnswerRecord(value: unknown): Record<string, string> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const entries = Object.entries(value).filter(([, item]) => typeof item === "string" && item.trim());
return entries.length ? Object.fromEntries(entries.map(([key, item]) => [key, String(item)])) : undefined;
}
export function parseCloudAgentFormAnswer(value: string): CloudAgentFormAnswer | undefined {
try {
const parsed: unknown = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || (parsed as { type?: unknown }).type !== "form_answer") return undefined;
const record = parsed as Record<string, unknown>;
if (!record.answers || typeof record.answers !== "object" || Array.isArray(record.answers)) return undefined;
const answers = formAnswerRecord(record.answers) || {};
return {
type: "form_answer",
questionId: typeof record.questionId === "string" ? record.questionId : undefined,
answers,
displayAnswers: formAnswerRecord(record.displayAnswers),
fieldTitles: formAnswerRecord(record.fieldTitles),
skippedFields: Array.isArray(record.skippedFields) ? record.skippedFields.filter((item): item is string => typeof item === "string") : [],
useRecommendedDefaults: record.useRecommendedDefaults === true,
};
} catch {
return undefined;
}
}
function agentFormFieldLabel(key: string, fieldTitles?: Record<string, string>) {
return fieldTitles?.[key] || AGENT_FORM_FIELD_LABELS[key] || key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ");
}
function agentFormValueLabel(value: string) {
return AGENT_FORM_VALUE_LABELS[value] || value.replace(/_/g, " ");
}
function AgentFormAnswerCard({ answer, theme }: { answer: CloudAgentFormAnswer; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const values = answer.displayAnswers && Object.keys(answer.displayAnswers).length ? answer.displayAnswers : answer.answers;
const entries = Object.entries(values).filter(([, value]) => value.trim());
return (
<div className="agent-form-answer-card" style={{ color: theme.node.text }}>
<div className="agent-form-answer-heading">已确认创作方向</div>
{answer.useRecommendedDefaults ? <div className="agent-form-answer-note">已采用推荐方案</div> : null}
{entries.length ? (
<div className="agent-form-answer-list">
{entries.map(([key, value]) => (
<div className="agent-form-answer-row" key={key}>
<span className="agent-form-answer-label">{agentFormFieldLabel(key, answer.fieldTitles)}</span>
<span className="agent-form-answer-value">{answer.displayAnswers?.[key] || agentFormValueLabel(value)}</span>
</div>
))}
</div>
) : (
<div className="agent-form-answer-note">已确认,按推荐方案继续</div>
)}
</div>
);
}
/**
* 类别的图标语言:三档互不串味 —— 清单用 List(读到存在与规模)、画面用 Eye(图片字节真的
* 交给了模型)、写画布用 Pencil / 创建用 Plus;未登记的工具走中性的 Wrench。
@@ -181,21 +323,24 @@ export function rewriteAgentNodeLinks(text: string, references: CanvasResourceRe
const newNodePrefixPattern = /(新节点|节点|新建节点)\s*ID(?=\s*[::])/gu;
let inFence = false;
return text.split(/(\r?\n)/u).map((part) => {
if (/^\r?\n$/u.test(part)) return part;
const fence = /^\s{0,3}(`{3,}|~{3,})/u.test(part);
if (fence) {
inFence = !inFence;
return part;
}
if (inFence) return part;
return text
.split(/(\r?\n)/u)
.map((part) => {
if (/^\r?\n$/u.test(part)) return part;
const fence = /^\s{0,3}(`{3,}|~{3,})/u.test(part);
if (fence) {
inFence = !inFence;
return part;
}
if (inFence) return part;
const normalized = part.replace(newNodePrefixPattern, "$1");
return normalized.replace(nodeTokenPattern, (match, backtickedId: string | undefined, plainId: string | undefined) => {
const nodeId = backtickedId || plainId;
return nodeId ? createAgentNodeLink(nodeId, referenceByNodeId.get(nodeId)) : match;
});
}).join("");
const normalized = part.replace(newNodePrefixPattern, "$1");
return normalized.replace(nodeTokenPattern, (match, backtickedId: string | undefined, plainId: string | undefined) => {
const nodeId = backtickedId || plainId;
return nodeId ? createAgentNodeLink(nodeId, referenceByNodeId.get(nodeId)) : match;
});
})
.join("");
}
function agentNodeIdFromHref(href?: string) {
@@ -214,7 +359,11 @@ function createAgentMessageMarkdownComponents(references: CanvasResourceReferenc
a: ({ children, href, className, ...props }) => {
const nodeId = agentNodeIdFromHref(href);
if (!nodeId) {
return <a {...props} href={href} className={`ai-message-markdown-link ${className || ""}`.trim()} target="_blank" rel="noreferrer">{children}</a>;
return (
<a {...props} href={href} className={`ai-message-markdown-link ${className || ""}`.trim()} target="_blank" rel="noreferrer">
{children}
</a>
);
}
const reference = referenceByNodeId.get(nodeId);
@@ -233,7 +382,13 @@ function createAgentMessageMarkdownComponents(references: CanvasResourceReferenc
onFocusNode?.(nodeId);
}}
>
{previewUrl ? <img className="agent-message-node-link-preview" src={previewUrl} alt="" loading="lazy" /> : <span className="agent-message-node-link-icon" aria-hidden="true"><CircleDot className="size-3.5" /></span>}
{previewUrl ? (
<img className="agent-message-node-link-preview" src={previewUrl} alt="" loading="lazy" />
) : (
<span className="agent-message-node-link-icon" aria-hidden="true">
<CircleDot className="size-3.5" />
</span>
)}
<span className="agent-message-node-link-copy">
<span className="agent-message-node-link-kind">{agentNodeTypeLabel(nodeId, reference)}</span>
<span className="agent-message-node-link-title">{reference?.title || reference?.label || children}</span>
@@ -268,6 +423,7 @@ export function AgentChatMessage({
const isUser = item.role === "user";
const isSystem = item.role === "system";
const displayedText = useTypewriterText(item.text, item.role === "assistant" && isStreaming);
const formAnswer = item.formAnswer ?? (isUser ? parseCloudAgentFormAnswer(item.text) : undefined);
const markdownComponents = useMemo(() => createAgentMessageMarkdownComponents(references, onFocusNode), [onFocusNode, references]);
const agentMarkdownText = rewriteAgentNodeLinks(displayedText, references);
const errorTone = item.errorSeverity === "warning" ? "warning" : "error";
@@ -321,11 +477,6 @@ export function AgentChatMessage({
}
return (
<div className={`flex items-start gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
{!isUser ? (
<span className="agent-message-icon agent-message-icon--assistant" aria-hidden="true">
<MessageCircle className="size-4" />
</span>
) : null}
<div className={`agent-message-body min-w-0 text-sm leading-6 ${isUser ? "agent-message-user max-w-[82%] px-4 py-3 text-right" : "max-w-full flex-1 text-left"}`} style={{ color }}>
{item.interjection ? (
<span
@@ -339,17 +490,14 @@ export function AgentChatMessage({
<AIMessageMarkdown className="text-left" isStreaming={isStreaming} streamingAnimation="none" components={markdownComponents}>
{agentMarkdownText}
</AIMessageMarkdown>
) : formAnswer ? (
<AgentFormAnswerCard answer={formAnswer} theme={theme} />
) : (
<AgentMessageText text={item.text} references={references} />
)}
{item.attachments?.length ? <AgentMessageAttachments attachments={item.attachments} /> : null}
{item.meta ? <div className="mt-1 text-[var(--fs-label)] opacity-45">{item.meta}</div> : null}
</div>
{isUser ? (
<span className="agent-message-icon agent-message-icon--user" aria-hidden="true">
<UserRound className="size-4" />
</span>
) : null}
</div>
);
}
@@ -358,23 +506,17 @@ export function AgentChatMessage({
* 推理是辅助信息,不应与正文和工具输出争夺主视觉。一个事件流里的连续摘要
* 合并成一个入口,默认收起;需要排查时再展开查看完整内容。
*/
export function AgentReasoningFeed({
items,
theme,
}: {
items: CloudAgentChatMessage[];
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
}) {
export function AgentReasoningFeed({ items, theme }: { items: CloudAgentChatMessage[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
const streaming = items.some((item) => item.streaming);
const text = items.map((item) => item.text.trim()).filter(Boolean).join("\n\n");
const text = items
.map((item) => item.text.trim())
.filter(Boolean)
.join("\n\n");
const countLabel = items.length > 1 ? `${items.length} 段 · ` : "";
return (
<div className="agent-reasoning" style={{ "--agent-reasoning-accent": theme.accent.primary } as CSSProperties}>
<details className={`agent-reasoning-card${streaming ? " is-streaming" : ""}`}>
<summary className="agent-reasoning-summary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/20">
<span className={`agent-reasoning-icon${streaming ? " is-live" : ""}`} aria-hidden="true">
<BrainCircuit className="size-4" />
</span>
<span className="agent-reasoning-copy">
<span className="agent-reasoning-title">{streaming ? "模型正在思考" : "模型思考"}</span>
<span className="agent-reasoning-subtitle">{streaming ? "实时整理 · 点击查看" : `${countLabel}点击查看`}</span>
@@ -450,10 +592,13 @@ function useTypewriterText(targetText: string, shouldAnimate: boolean) {
startLoop();
}, [shouldAnimate, startLoop, targetText]);
useEffect(() => () => {
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
runningRef.current = false;
}, []);
useEffect(
() => () => {
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
runningRef.current = false;
},
[],
);
return visibleText;
}
@@ -813,7 +958,10 @@ export function AgentPlanBar({ items, theme, minimized, onToggle, terminal = fal
const Icon = done ? CheckCircle2 : terminal ? CircleAlert : doing ? LoaderCircle : CircleDot;
return (
<li key={entry.id} className="flex min-w-0 items-start gap-1.5 text-xs">
<Icon className={doing && !terminal ? "mt-[3px] size-3 shrink-0 animate-spin" : "mt-[3px] size-3 shrink-0"} style={{ color: done ? "#429477" : terminal ? theme.node.muted : doing ? theme.accent.primary : theme.node.muted }} />
<Icon
className={doing && !terminal ? "mt-[3px] size-3 shrink-0 animate-spin" : "mt-[3px] size-3 shrink-0"}
style={{ color: done ? "#429477" : terminal ? theme.node.muted : doing ? theme.accent.primary : theme.node.muted }}
/>
<span className={done ? "min-w-0 break-words line-through opacity-50" : terminal ? "min-w-0 break-words opacity-55" : "min-w-0 break-words"}>{entry.title}</span>
</li>
);
@@ -824,15 +972,175 @@ export function AgentPlanBar({ items, theme, minimized, onToggle, terminal = fal
);
}
export function AgentQuestionBar({ question, theme, onAnswer, disabled = false }: { question: CloudAgentUserQuestion; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onAnswer: (label: string) => void; disabled?: boolean }) {
export function AgentQuestionBar({ question, theme, onAnswer, disabled = false }: { question: CloudAgentUserQuestion; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onAnswer: (answer: string) => void; disabled?: boolean }) {
const fields = question.fields || [];
const isForm = question.kind === "form" || fields.length > 0;
const fieldDefaultValue = (field: CloudAgentFormField) => field.defaultValue || field.options?.find((option) => option.recommended)?.id || field.options?.find((option) => option.recommended)?.label || "";
const [values, setValues] = useState<Record<string, string>>(() => Object.fromEntries(fields.map((field) => [field.id, fieldDefaultValue(field)])));
const [customFields, setCustomFields] = useState<Record<string, boolean>>({});
const [validation, setValidation] = useState("");
useEffect(() => {
setValues(Object.fromEntries(fields.map((field) => [field.id, fieldDefaultValue(field)])));
setCustomFields({});
setValidation("");
}, [question.questionId, fields]);
const submitForm = (useDefaults = false) => {
const answers = useDefaults ? Object.fromEntries(fields.map((field) => [field.id, field.defaultValue || ""])) : values;
const missing = fields.find((field) => (field.required && !String(answers[field.id] || "").trim()) || (field.required && answers[field.id] === "other"));
if (missing) {
setValidation(`请填写“${missing.title}”`);
return;
}
setValidation("");
const displayAnswers: Record<string, string> = {};
for (const field of fields) {
const value = String(answers[field.id] || "").trim();
if (!value || value === "other") continue;
const option = field.options?.find((item) => (item.id || item.label) === value);
displayAnswers[field.id] = option?.label || value;
}
const fieldTitles = Object.fromEntries(fields.map((field) => [field.id, field.title]));
onAnswer(JSON.stringify({ type: "form_answer", questionId: question.questionId, answers, displayAnswers, fieldTitles, skippedFields: fields.filter((field) => !String(answers[field.id] || "").trim()).map((field) => field.id), useRecommendedDefaults: useDefaults }));
};
const stop = (event: SyntheticEvent) => {
event.stopPropagation();
};
const updateValue = (field: CloudAgentFormField, value: string) => {
setValues((current) => ({ ...current, [field.id]: value }));
if (field.allowCustom && value === "other") setCustomFields((current) => ({ ...current, [field.id]: true }));
};
if (isForm) {
return (
<div className="agent-question-bar agent-question-form mx-3 mb-2 overflow-hidden rounded-xl" style={{ color: theme.node.text }}>
<div className="flex items-start gap-2 px-3 pt-2.5">
<HelpCircle className="mt-[1px] size-3.5 shrink-0" style={{ color: theme.accent.primary }} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-[10px] opacity-55">{question.round && question.maxRounds ? `确认 ${question.round}/${question.maxRounds}` : "需求确认"}</div>
<div className="text-xs font-semibold leading-5">{question.question}</div>
<div className="mt-0.5 text-[10px] opacity-50">已填入推荐值,可按需修改;不重要的字段可以留空。</div>
</div>
</div>
<div className="agent-question-fields px-3 py-2">
{fields.map((field) => {
const options = field.options || [];
const custom = customFields[field.id] || values[field.id] === "other";
return (
<div className="agent-question-field" key={field.id}>
<label className="agent-question-field-label" htmlFor={`agent-field-${field.id}`}>
{field.title}
{field.required ? <span className="ml-0.5 opacity-70">*</span> : null}
</label>
<div className="min-w-0 flex-1">
{field.type === "textarea" ? (
<textarea
id={`agent-field-${field.id}`}
rows={2}
value={values[field.id] || ""}
placeholder={field.placeholder}
disabled={disabled}
className="agent-question-input agent-question-textarea"
onChange={(event) => updateValue(field, event.target.value)}
onMouseDown={stop}
onPointerDown={stop}
/>
) : field.type === "text" ? (
<input
id={`agent-field-${field.id}`}
value={values[field.id] || ""}
placeholder={field.placeholder}
disabled={disabled}
className="agent-question-input"
onChange={(event) => updateValue(field, event.target.value)}
onMouseDown={stop}
onPointerDown={stop}
/>
) : (
<div className="agent-question-options" role="radiogroup" aria-label={field.title}>
{options.map((option) => {
const optionId = option.id || option.label;
const selected = values[field.id] === optionId;
return (
<button
key={optionId}
type="button"
disabled={disabled}
aria-pressed={selected}
className={`agent-question-chip${selected ? " is-selected" : ""}`}
style={selected ? { background: theme.accent.primary, color: theme.accent.onPrimary } : { background: theme.toolbar.itemHover }}
title={option.detail || option.label}
onMouseDown={stop}
onPointerDown={stop}
onClick={(event) => {
stop(event);
updateValue(field, optionId);
}}
>
{option.label}
</button>
);
})}
</div>
)}
{field.allowCustom && (custom || field.type === "text") ? (
<input
value={custom && values[field.id] !== "other" ? values[field.id] || "" : ""}
placeholder={field.placeholder || "输入自定义内容"}
disabled={disabled}
className="agent-question-input mt-1.5"
onChange={(event) => updateValue(field, event.target.value)}
onMouseDown={stop}
onPointerDown={stop}
/>
) : null}
</div>
</div>
);
})}
</div>
{validation ? <div className="agent-question-validation px-3 pb-1 text-[10px]">{validation}</div> : null}
<div className="agent-question-actions px-3 pb-2.5">
<button
type="button"
disabled={disabled}
className="agent-question-default"
onMouseDown={stop}
onPointerDown={stop}
onClick={(event) => {
stop(event);
submitForm(true);
}}
>
按推荐方案开始
</button>
<button
type="button"
disabled={disabled}
className="agent-question-submit"
style={{ background: theme.accent.primary, color: theme.accent.onPrimary }}
onMouseDown={stop}
onPointerDown={stop}
onClick={(event) => {
stop(event);
submitForm();
}}
>
确认并继续
</button>
</div>
</div>
);
}
return (
<div className="agent-question-bar mx-3 mb-2 overflow-hidden rounded-xl" style={{ color: theme.node.text }}>
<div className="flex items-start gap-2 px-3 pt-2.5">
<HelpCircle className="mt-[1px] size-3.5 shrink-0" style={{ color: theme.accent.primary }} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-[10px] opacity-55">
{question.round && question.maxRounds ? `确认 ${question.round}/${question.maxRounds}` : "需要确认"}
</div>
<div className="flex items-center gap-2 text-[10px] opacity-55">{question.round && question.maxRounds ? `确认 ${question.round}/${question.maxRounds}` : "需要确认"}</div>
<div className="text-xs font-semibold leading-5">{question.question}</div>
</div>
</div>
@@ -843,12 +1151,12 @@ export function AgentQuestionBar({ question, theme, onAnswer, disabled = false }
type="button"
disabled={disabled}
title={option.detail || option.label}
className="max-w-full rounded-md border-0 px-3 py-1.5 text-left text-xs transition focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50"
className="agent-question-choice max-w-full rounded-md border-0 px-3 py-1.5 text-left text-xs transition focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50"
style={{ background: theme.toolbar.itemHover }}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
onMouseDown={stop}
onPointerDown={stop}
onClick={(event) => {
event.stopPropagation();
stop(event);
onAnswer(option.label);
}}
>
@@ -860,23 +1168,22 @@ export function AgentQuestionBar({ question, theme, onAnswer, disabled = false }
type="button"
disabled={disabled}
title="使用安全默认方案继续"
className="max-w-full rounded-md border-0 px-3 py-1.5 text-left text-xs font-medium transition focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50"
className="agent-question-default max-w-full rounded-md border-0 px-3 py-1.5 text-left text-xs font-medium transition focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50"
style={{ background: theme.accent.primary, color: theme.accent.onPrimary }}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
onMouseDown={stop}
onPointerDown={stop}
onClick={(event) => {
event.stopPropagation();
stop(event);
onAnswer("直接开始");
}}
>
按默认方案开始
</button>
</div>
<div className="px-3 pb-2 text-[10px] opacity-50">{question.allowFreeform === false ? "请从上面选一项。" : "点一项即可,也可以在下方输入框里自己说明。"}</div>
<div className="px-3 pb-2 text-[10px] opacity-50">{question.allowFreeform === false ? "请从上面选一项。" : "也可以在下方输入框里补充说明。"}</div>
</div>
);
}
/**
* 场景起步胶囊:把「我大概想做 X」一步翻译成一组技能。
* 两组来源,都不限剧典技能:
@@ -902,7 +1209,14 @@ export const AGENT_SCENE_DEFS: Array<{ key: string; label: string; icon: typeof
{ key: "others", label: "其他", icon: Shapes },
];
export function AgentSceneCapsules({ buckets, installedIds, theme, disabled = false, onPick, onPickSkill }: {
export function AgentSceneCapsules({
buckets,
installedIds,
theme,
disabled = false,
onPick,
onPickSkill,
}: {
buckets: AgentSceneBucket[];
installedIds: Set<string>;
theme: (typeof canvasThemes)[keyof typeof canvasThemes];