feat(canvas): 完善画布 Agent 交互能力
This commit is contained in:
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];
|
||||
|
||||
Reference in new issue
Block a user