Merge remote-tracking branch 'upstream/main' into main (保留常驻通知与顺延迁移版本)
This commit is contained in:
@@ -42,7 +42,7 @@ export function CanvasAgentImageApprovalSettings({ initial, value, onChange, the
|
||||
return <fieldset disabled={disabled} className="min-w-0 space-y-3 border-0 p-0" data-canvas-no-zoom data-canvas-wheel-scroll aria-label="图片生成设置">
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs" style={{ color: theme.node.muted }}>生成模型</div>
|
||||
<ModelPicker config={config} capability="image" value={model} onChange={changeModel} requirements={requirements} fullWidth showOptionPrices placeholder="选择生成模型" />
|
||||
<ModelPicker config={config} capability="image" value={model} onChange={changeModel} requirements={requirements} variant="creation" fullWidth showOptionPrices placeholder="选择生成模型" popoverClassName="agent-model-picker-popover" />
|
||||
</div>
|
||||
{model ? <ImageSettingsPanel config={{ ...config, model, imageModel: model, size: settings.size, quality: settings.quality, count: "1" }} onConfigChange={changeOption} theme={theme} showTitle={false} showCount={false} showTransparent={false} className="min-w-0 space-y-3" /> : <p className="text-xs" style={{ color: theme.node.muted }}>当前模型不在可选目录中,可重新选择;提交时将重新校验模型与规格。</p>}
|
||||
<p className="text-xs" style={{ color: theme.node.text }}>本次规格:{settings.size}{settings.quality ? ` · ${settings.quality}` : ""}</p>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowUp, AtSign, CheckCircle2, ChevronDown, CircleAlert, CircleDot, Eye, ImagePlus, LoaderCircle, Pencil, Plus, RotateCcw, Sparkles, Square, X, XCircle } from "lucide-react";
|
||||
import { ArrowUp, AtSign, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Eye, HelpCircle, ImagePlus, ListChecks, LoaderCircle, Pencil, Plus, RotateCcw, Sparkles, Square, X, XCircle } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
|
||||
@@ -24,6 +24,12 @@ type CloudAgentOperationImpact = {
|
||||
items: string[];
|
||||
warning?: string;
|
||||
};
|
||||
export type CloudAgentPlanItem = { id: string; title: string; status: "pending" | "doing" | "done" };
|
||||
export type CloudAgentUserQuestion = {
|
||||
question: string;
|
||||
options: Array<{ label: string; detail?: string }>;
|
||||
allowFreeform?: boolean;
|
||||
};
|
||||
export type CloudAgentChatMessage = {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "system" | "tool" | "error";
|
||||
@@ -31,9 +37,12 @@ export type CloudAgentChatMessage = {
|
||||
text: string;
|
||||
streaming?: boolean;
|
||||
reasoning?: boolean;
|
||||
planItems?: CloudAgentPlanItem[];
|
||||
question?: CloudAgentUserQuestion;
|
||||
meta?: string;
|
||||
detail?: unknown;
|
||||
attachments?: CloudAgentChatAttachment[];
|
||||
interjection?: "sent" | "undelivered";
|
||||
};
|
||||
|
||||
export type CloudAgentQuickAction = { label: string; prompt: string };
|
||||
@@ -156,6 +165,16 @@ export function AgentChatMessage({
|
||||
<div className={`flex items-start gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
|
||||
{!isUser ? <AgentTimelineMarker theme={theme} tone="agent" /> : null}
|
||||
<div className={`min-w-0 text-sm leading-6 ${isUser ? "max-w-[82%] rounded-2xl rounded-br-md px-3.5 py-2.5 text-right" : "max-w-[calc(100%-36px)] flex-1 text-left"}`} style={{ color, ...(isUser ? { background: theme.node.agentUserMessage } : {}) }}>
|
||||
{item.interjection ? (
|
||||
<span
|
||||
className="mb-1 inline-flex items-center rounded-full px-1.5 py-[1px] text-[var(--fs-label)] leading-4"
|
||||
style={item.interjection === "undelivered"
|
||||
? { background: `${theme.accent.danger}22`, color: theme.accent.danger }
|
||||
: { background: "rgba(255,255,255,0.16)", opacity: 0.72 }}
|
||||
>
|
||||
{item.interjection === "undelivered" ? "插话未送达" : "插话"}
|
||||
</span>
|
||||
) : null}
|
||||
{item.role === "assistant" ? (
|
||||
<AIMessageMarkdown className="text-left" isStreaming={isStreaming}>
|
||||
{item.text}
|
||||
@@ -343,11 +362,89 @@ export function AgentWorkingMessage({ theme, label = WORKING_TEXT }: { theme: (t
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentPlanBar({ items, theme, minimized, onToggle }: {
|
||||
items: CloudAgentPlanItem[];
|
||||
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
|
||||
minimized: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const doneCount = items.filter((entry) => entry.status === "done").length;
|
||||
const allDone = doneCount === items.length;
|
||||
return (
|
||||
<div className="agent-plan-bar mx-3 mb-2 overflow-hidden rounded-xl" style={{ background: theme.node.fill, border: `1px solid ${theme.node.stroke}`, color: theme.node.text }}>
|
||||
<button type="button" className="flex w-full items-center gap-2 px-3 py-2 text-left focus-visible:outline focus-visible:outline-2" aria-expanded={!minimized} onClick={onToggle}>
|
||||
<ListChecks className="size-3.5 shrink-0" style={{ color: allDone ? "#429477" : theme.node.muted }} />
|
||||
<span className="text-xs font-semibold">本轮待办</span>
|
||||
<span className="text-[11px] tabular-nums opacity-50">{doneCount}/{items.length}</span>
|
||||
<span className="min-w-0 flex-1" />
|
||||
<span className="shrink-0 text-[11px] opacity-50">{minimized ? "展开" : "收起"}</span>
|
||||
{minimized ? <ChevronDown className="size-3.5 shrink-0 opacity-50" /> : <ChevronUp className="size-3.5 shrink-0 opacity-50" />}
|
||||
</button>
|
||||
{minimized ? null : (
|
||||
<ul className="thin-scrollbar max-h-40 space-y-1 overflow-y-auto px-3 pb-2">
|
||||
{items.map((entry) => {
|
||||
const done = entry.status === "done";
|
||||
const doing = entry.status === "doing";
|
||||
const Icon = done ? CheckCircle2 : doing ? LoaderCircle : CircleDot;
|
||||
return (
|
||||
<li key={entry.id} className="flex min-w-0 items-start gap-1.5 text-xs">
|
||||
<Icon className={doing ? "mt-[3px] size-3 shrink-0 animate-spin" : "mt-[3px] size-3 shrink-0"} style={{ color: done ? "#429477" : doing ? theme.accent.primary : theme.node.muted }} />
|
||||
<span className={done ? "min-w-0 break-words line-through opacity-50" : "min-w-0 break-words"}>{entry.title}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentQuestionBar({ question, theme, onAnswer, disabled = false }: {
|
||||
question: CloudAgentUserQuestion;
|
||||
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
|
||||
onAnswer: (label: string) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="agent-question-bar mx-3 mb-2 overflow-hidden rounded-xl" style={{ background: theme.node.fill, border: `1px solid ${theme.accent.primary}`, 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 }} />
|
||||
<span className="min-w-0 flex-1 text-xs font-semibold leading-5">{question.question}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 px-3 pb-2 pt-2">
|
||||
{question.options.map((option) => (
|
||||
<button
|
||||
key={option.label}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
title={option.detail || option.label}
|
||||
className="max-w-full rounded-md border 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={{ borderColor: theme.node.stroke, background: theme.toolbar.itemHover }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onAnswer(option.label);
|
||||
}}
|
||||
>
|
||||
<span className="block break-words font-medium">{option.label}</span>
|
||||
{option.detail ? <span className="mt-0.5 block break-words text-[10px] leading-4 opacity-60">{option.detail}</span> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-3 pb-2 text-[10px] opacity-50">
|
||||
{question.allowFreeform === false ? "请从上面选一项。" : "点一项即可,也可以在下方输入框里自己说明。"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentChatComposer({
|
||||
prompt,
|
||||
attachments = [],
|
||||
disabled,
|
||||
sending,
|
||||
running,
|
||||
placeholder,
|
||||
theme,
|
||||
onPromptChange,
|
||||
@@ -365,6 +462,7 @@ export function AgentChatComposer({
|
||||
attachments?: CloudAgentChatAttachment[];
|
||||
disabled?: boolean;
|
||||
sending?: boolean;
|
||||
running?: boolean;
|
||||
placeholder: string;
|
||||
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
|
||||
onPromptChange: (value: string) => void;
|
||||
@@ -397,7 +495,7 @@ export function AgentChatComposer({
|
||||
const attachmentReferences = useMemo(() => agentAttachmentReferences(attachments), [attachments]);
|
||||
const skillReferences = useMemo(() => buildSkillMentionReferences(availableSlashSkills), [availableSlashSkills]);
|
||||
const composerReferences = useMemo(() => [...references, ...skillReferences, ...attachmentReferences].filter((reference, index, all) => all.findIndex((item) => item.id === reference.id) === index), [attachmentReferences, references, skillReferences]);
|
||||
const canStop = Boolean(sending && onStop);
|
||||
const canStop = Boolean(running && onStop);
|
||||
const canSubmit = !disabled && !sending && Boolean(prompt.trim() || attachments.length);
|
||||
const reducedMotion = useReducedMotion();
|
||||
const activeSlashIndex = Math.min(Math.max(slashIndex, 0), Math.max(slashCandidates.length - 1, 0));
|
||||
@@ -643,32 +741,47 @@ export function AgentChatComposer({
|
||||
{left}
|
||||
</div>
|
||||
<div className="agent-composer-submit flex items-center gap-2">
|
||||
<span className="agent-composer-send-hint">Enter 换行 · ⌘/Ctrl+Enter 发送</span>
|
||||
<span className="agent-composer-send-hint">{canStop ? "运行中:发送即插话,下一步生效" : "Enter 换行 · ⌘/Ctrl+Enter 发送"}</span>
|
||||
{canStop ? <motion.button
|
||||
type="button"
|
||||
disabled={stopping}
|
||||
aria-label="停止本轮"
|
||||
title="停止当前 Agent 运行"
|
||||
onClick={() => void onStop?.()}
|
||||
whileHover={!reducedMotion && !stopping ? { scale: 1.06, y: -1 } : undefined}
|
||||
whileTap={!reducedMotion && !stopping ? { scale: 0.9, y: 1 } : undefined}
|
||||
animate={stopping && !reducedMotion ? { scale: [1, 0.94, 1] } : { scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 420, damping: 24 }}
|
||||
className="grid size-9 shrink-0 place-items-center rounded-full p-0 outline-none transition-[background-color,box-shadow,color,transform] duration-200 focus-visible:ring-2 focus-visible:ring-current/35 disabled:cursor-not-allowed"
|
||||
style={{ background: theme.accent.danger, color: theme.accent.onPrimary, boxShadow: `0 8px 20px ${theme.accent.danger}45` }}
|
||||
>
|
||||
{stopping ? <LoaderCircle className="size-4 animate-spin" /> : <Square className="size-3.5" fill="currentColor" />}
|
||||
</motion.button> : null}
|
||||
<motion.button
|
||||
type="button"
|
||||
disabled={!canSubmit && !canStop}
|
||||
aria-label={canStop ? "停止本轮" : sending ? "发送中" : "发送"}
|
||||
title={canStop ? "停止当前 Agent 运行" : "点击发送;⌘/Ctrl+Enter 发送"}
|
||||
onClick={() => void (canStop ? onStop?.() : onSubmit())}
|
||||
whileHover={(canSubmit || canStop) && !reducedMotion ? { scale: 1.06, y: -1 } : undefined}
|
||||
whileTap={(canSubmit || canStop) && !reducedMotion ? { scale: 0.9, y: 1 } : undefined}
|
||||
disabled={!canSubmit}
|
||||
aria-label={sending ? "发送中" : canStop ? "插话" : "发送"}
|
||||
title={canStop ? "插话:Agent 下一次开口时看到它" : "点击发送;⌘/Ctrl+Enter 发送"}
|
||||
onClick={() => onSubmit()}
|
||||
whileHover={canSubmit && !reducedMotion ? { scale: 1.06, y: -1 } : undefined}
|
||||
whileTap={canSubmit && !reducedMotion ? { scale: 0.9, y: 1 } : undefined}
|
||||
animate={stopping && !reducedMotion ? { scale: [1, 0.94, 1] } : { scale: 1, rotate: 0 }}
|
||||
transition={sending && !reducedMotion ? { duration: 0.42, ease: "easeOut" } : { type: "spring", stiffness: 420, damping: 24 }}
|
||||
className="grid size-9 shrink-0 place-items-center rounded-full p-0 outline-none transition-[background-color,box-shadow,color,transform] duration-200 focus-visible:ring-2 focus-visible:ring-current/35 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: canStop ? theme.accent.danger : canSubmit || sending ? theme.accent.primary : theme.spatial.surface,
|
||||
color: canStop || canSubmit || sending ? theme.accent.onPrimary : theme.node.muted,
|
||||
boxShadow: canStop || canSubmit || sending ? `0 8px 20px ${canStop ? theme.accent.danger : theme.accent.primary}45` : "none",
|
||||
background: canSubmit || sending ? theme.accent.primary : theme.spatial.surface,
|
||||
color: canSubmit || sending ? theme.accent.onPrimary : theme.node.muted,
|
||||
boxShadow: canSubmit || sending ? `0 8px 20px ${theme.accent.primary}45` : "none",
|
||||
}}
|
||||
>
|
||||
<motion.span
|
||||
key={stopping ? "stopping" : canStop ? "stop" : sending ? "sending" : "ready"}
|
||||
key={stopping ? "stopping" : sending ? "sending" : "ready"}
|
||||
initial={reducedMotion ? false : { opacity: 0, scale: 0.65, rotate: sending ? -25 : 25 }}
|
||||
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||
transition={{ duration: reducedMotion ? 0 : 0.18, ease: "easeOut" }}
|
||||
className="grid place-items-center"
|
||||
>
|
||||
{stopping || (sending && !canStop) ? <LoaderCircle className="size-4 animate-spin" /> : canStop ? <Square className="size-3.5" fill="currentColor" /> : <ArrowUp className="size-4" />}
|
||||
{sending ? <LoaderCircle className="size-4 animate-spin" /> : <ArrowUp className="size-4" />}
|
||||
</motion.span>
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowLeft, Bot, Check, ChevronRight, CircleDot, Clock3, Download, History, LoaderCircle, MessageSquarePlus, Settings2, ShieldCheck, Trash2, Sparkles, X } from "lucide-react";
|
||||
import { saveAs } from "file-saver";
|
||||
import { buildAgentDebugExport } from "@/lib/canvas/agent-debug-export";
|
||||
import { markdownPlainText } from "@/lib/markdown-plain-text";
|
||||
import { agentPlanVisible, latestAgentPlanItems, pendingAgentQuestion } from "@/lib/canvas/cloud-agent-plan";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -14,7 +14,7 @@ import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import { agentErrorPresentation, agentSubmissionErrorTitle } from "@/lib/canvas/agent-error-presentation";
|
||||
import { cancelAgentRun, getAgentCapabilities, getAgentProfile, getAgentRun, createAgentRun, decideAgentApproval, sendAgentMessage, subscribeAgentEvents, updateAgentProfile, type AgentEvent, type AgentPermissionMode, type AgentProfileScope, type AgentProfileView, type AgentReasoningMode, type AgentRun } from "@/services/api/agent";
|
||||
import { cancelAgentRun, getAgentCapabilities, getAgentProfile, getAgentRun, createAgentRun, decideAgentApproval, sendAgentInterjection, sendAgentMessage, subscribeAgentEvents, updateAgentProfile, type AgentEvent, type AgentPermissionMode, type AgentProfileScope, type AgentProfileView, type AgentReasoningMode, type AgentRun } from "@/services/api/agent";
|
||||
import { agentApprovalPresentation } from "@/lib/canvas/agent-approval-presentation";
|
||||
import { agentApprovalMatchesSettings, agentImageApproval } from "@/lib/canvas/agent-media-approval";
|
||||
import type { AgentMediaSettings } from "@/services/api/agent";
|
||||
@@ -26,7 +26,7 @@ import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
||||
import { applyAgentCanvasPatches, refreshCanvasAfterAgent, saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
import { createAgentCanvasSync } from "@/services/agent-canvas-sync";
|
||||
import { buildSkillMentionReferences, resolveSkillMentions } from "@/services/skill-runtime";
|
||||
import { AgentChatComposer, AgentChatMessage, AgentWorkingMessage, type CloudAgentChatMessage } from "./canvas-cloud-agent-chat-ui";
|
||||
import { AgentChatComposer, AgentChatMessage, AgentPlanBar, AgentQuestionBar, AgentWorkingMessage, type CloudAgentChatMessage, type CloudAgentPlanItem } from "./canvas-cloud-agent-chat-ui";
|
||||
import { CanvasAgentSkillLibraryModal } from "./canvas-agent-skill-library-modal";
|
||||
import { CanvasCloudAgentSettings, agentPermissionLabel, agentPermissionMenuItems, agentPermissionVisual, type AgentContextKey } from "./canvas-cloud-agent-settings";
|
||||
import { useAgentPanelLayout } from "./use-agent-panel-layout";
|
||||
@@ -79,6 +79,10 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
const [activeConversationId, setActiveConversationId] = useState(() => nanoid());
|
||||
const [historyHydrated, setHistoryHydrated] = useState(false);
|
||||
const [pendingHydrated, setPendingHydrated] = useState(false);
|
||||
const [planMinimized, setPlanMinimized] = useState(false);
|
||||
const planItems = useMemo(() => latestAgentPlanItems(messages), [messages]);
|
||||
const planVisible = agentPlanVisible(planItems);
|
||||
const pendingQuestion = useMemo(() => pendingAgentQuestion(messages), [messages]);
|
||||
const panelLayout = useAgentPanelLayout();
|
||||
const lastSeqRef = useRef(0);
|
||||
const canvasSyncRef = useRef<ReturnType<typeof createAgentCanvasSync> | null>(null);
|
||||
@@ -321,7 +325,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
lastSeqRef.current = event.seq;
|
||||
}
|
||||
setMessages((current) => current.filter((item) => item.id !== `stream-error-${run.id}`));
|
||||
applyAgentEvent(event, setMessages, setRun, setApproval);
|
||||
applyAgentEvent(event, setMessages, setRun, setApproval, setPrompt);
|
||||
canvasSyncRef.current?.receive(event);
|
||||
},
|
||||
{
|
||||
@@ -345,8 +349,33 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
return () => window.removeEventListener("online", reconnect);
|
||||
}, [run?.id, connectionStatus]);
|
||||
|
||||
const submit = async () => {
|
||||
const value = prompt.trim();
|
||||
const interject = async (value: string) => {
|
||||
const activeRun = run;
|
||||
if (!value || !activeRun?.id || busy || connectionStatus !== "connected" || currentScope.current !== conversationScope || !historyHydrated) return;
|
||||
const scope = conversationScope;
|
||||
const messageId = `user-${crypto.randomUUID()}`;
|
||||
setBusy(true);
|
||||
try {
|
||||
await sendAgentInterjection(activeRun.id, { text: value, messageId });
|
||||
if (currentScope.current !== scope) return;
|
||||
setPrompt("");
|
||||
setMessages((current) => appendUniqueMessage(current, { id: messageId, role: "user", text: value, interjection: "sent" }));
|
||||
} catch (cause) {
|
||||
if (currentScope.current !== scope) return;
|
||||
const status = (cause as { status?: number }).status;
|
||||
setMessages((current) => appendAgentError(current, `interject-error-${activeConversationId}-${messageId}`, cause, "插话没有送达"));
|
||||
if (status === 409) setPrompt(value);
|
||||
} finally {
|
||||
if (currentScope.current === scope) setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async (override?: string) => {
|
||||
const value = (override ?? prompt).trim();
|
||||
if (running) {
|
||||
await interject(value);
|
||||
return;
|
||||
}
|
||||
if (!value || busy || running || (run && connectionStatus !== "connected") || submissionRequestRef.current || !historyHydrated || !pendingHydrated || currentScope.current !== conversationScope) return;
|
||||
const scope = conversationScope;
|
||||
submissionRequestRef.current = true;
|
||||
@@ -689,11 +718,21 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
onApprove={(settings) => void submitApproval("approve", settings)}
|
||||
onReject={() => void submitApproval("reject")}
|
||||
/>
|
||||
{planVisible ? <AgentPlanBar items={planItems} theme={theme} minimized={planMinimized} onToggle={() => setPlanMinimized((value) => !value)} /> : null}
|
||||
{pendingQuestion ? (
|
||||
<AgentQuestionBar
|
||||
question={pendingQuestion}
|
||||
theme={theme}
|
||||
disabled={approvalSubmitting || connectionStatus !== "connected"}
|
||||
onAnswer={(label) => void submit(label)}
|
||||
/>
|
||||
) : null}
|
||||
<AgentChatComposer
|
||||
prompt={prompt}
|
||||
disabled={busy || running || Boolean(run && connectionStatus !== "connected") || !historyHydrated || !pendingHydrated}
|
||||
sending={busy || running}
|
||||
placeholder="输入操作指导;用 @ 引用画布节点,用 / 引用 Skills"
|
||||
disabled={Boolean(run && connectionStatus !== "connected") || !historyHydrated || !pendingHydrated}
|
||||
sending={busy}
|
||||
running={running}
|
||||
placeholder={running ? "运行中可直接插话,会在它下一步生效" : "输入操作指导;用 @ 引用画布节点,用 / 引用 Skills"}
|
||||
theme={theme}
|
||||
onPromptChange={setPrompt}
|
||||
onSubmit={() => void submit()}
|
||||
@@ -1047,7 +1086,7 @@ function ApprovalCard({ approval, theme, submitting, onFocusNode, onReasonChange
|
||||
}
|
||||
|
||||
function ApprovalPreviewItemView({ item, theme, onFocusNode }: { item: ReturnType<typeof agentApprovalPresentation>["items"][number]; theme: CanvasTheme; onFocusNode?: (nodeId: string) => void }) {
|
||||
const operationLabel = item.operation === "add_node" ? "新增" : item.operation === "update_node" ? "修改" : item.operation === "connect_nodes" ? "连线" : item.operation === "create_storyboard" ? "创建分镜" : item.operation === "edit_storyboard" ? "修改分镜" : "生成";
|
||||
const operationLabel = item.operation === "add_node" ? "新增" : item.operation === "update_node" ? "修改" : item.operation === "connect_nodes" ? "连线" : item.operation === "create_storyboard" ? "创建分镜" : item.operation === "edit_storyboard" ? "修改分镜" : item.operation === "plan_step" ? "计划" : "生成";
|
||||
const renderNode = (title: string | undefined, id: string | undefined, typeLabel: string | undefined, role: "source" | "target" | "node") => {
|
||||
if (!title) return null;
|
||||
const content = <><span className="canvas-agent-approval-node-title">{title}</span>{typeLabel ? <span className="canvas-agent-approval-node-type">{typeLabel}</span> : null}</>;
|
||||
@@ -1093,7 +1132,7 @@ function formatConversationTime(value: string) {
|
||||
if (date.toDateString() === today.toDateString()) return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
return date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" });
|
||||
}
|
||||
function applyAgentEvent(event: AgentEvent, setMessages: Dispatch<SetStateAction<CloudAgentChatMessage[]>>, setRun: Dispatch<SetStateAction<AgentRun | null>>, setApproval: Dispatch<SetStateAction<ApprovalState | null>>) {
|
||||
function applyAgentEvent(event: AgentEvent, setMessages: Dispatch<SetStateAction<CloudAgentChatMessage[]>>, setRun: Dispatch<SetStateAction<AgentRun | null>>, setApproval: Dispatch<SetStateAction<ApprovalState | null>>, setPrompt?: Dispatch<SetStateAction<string>>) {
|
||||
const payload = event.payload || {};
|
||||
const text = String(payload.text || payload.summary || payload.message || "");
|
||||
if (event.type === "run_status") {
|
||||
@@ -1132,6 +1171,51 @@ function applyAgentEvent(event: AgentEvent, setMessages: Dispatch<SetStateAction
|
||||
.map((item) => item.id === id ? { ...item, reasoning: true } : item));
|
||||
return;
|
||||
}
|
||||
if (event.type === "plan_updated" && Array.isArray(payload.items)) {
|
||||
const id = `plan-${event.runId}`;
|
||||
const planItems = payload.items as CloudAgentPlanItem[];
|
||||
setMessages((current) => {
|
||||
const index = current.findIndex((entry) => entry.id === id);
|
||||
const message: CloudAgentChatMessage = { id, role: "tool", text: "", planItems };
|
||||
if (index < 0) return [...current, message];
|
||||
const next = [...current];
|
||||
next[index] = message;
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.type === "user_interjection") {
|
||||
if (!text) return;
|
||||
setMessages((current) => appendUniqueMessage(current, { id: String(payload.messageId || event.eventId), role: "user", text, interjection: "sent" }));
|
||||
return;
|
||||
}
|
||||
if (event.type === "user_interjection_dropped") {
|
||||
const messageId = String(payload.messageId || event.eventId);
|
||||
const reason = String(payload.reason || "本轮已结束");
|
||||
setMessages((current) => {
|
||||
const marked = current.map((item) => item.id === messageId ? { ...item, interjection: "undelivered" as const } : item);
|
||||
return appendUniqueMessage(marked, { id: `interjection-dropped-${messageId}`, role: "system", text: `${reason},这条插话没有送到模型。需要的话重新发一次,它会作为新一轮。` });
|
||||
});
|
||||
setPrompt?.((current) => (current.trim() ? current : text));
|
||||
return;
|
||||
}
|
||||
if (event.type === "user_question") {
|
||||
const options = Array.isArray(payload.options)
|
||||
? (payload.options as Array<{ label?: unknown; detail?: unknown }>)
|
||||
.map((option) => ({ label: String(option?.label || "").trim(), detail: option?.detail === undefined ? undefined : String(option.detail) }))
|
||||
.filter((option) => option.label)
|
||||
: [];
|
||||
const question = String(payload.question || "").trim();
|
||||
if (!question || options.length < 2) return;
|
||||
const id = `question-${event.runId}:${event.seq ?? event.eventId}`;
|
||||
setMessages((current) => appendUniqueMessage(current, {
|
||||
id,
|
||||
role: "assistant",
|
||||
text: "",
|
||||
question: { question, options, allowFreeform: payload.allowFreeform !== false },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (event.type === "assistant_message") {
|
||||
setMessages((current) => upsertTextMessage(current, String(payload.messageId || event.eventId), text, false));
|
||||
return;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Button, Checkbox, Input } from "antd";
|
||||
import { ArrowLeft, Check, ChevronRight, Cpu, Gauge, LockKeyhole, PlugZap, Search, ShieldCheck, Sparkles, Wrench } from "lucide-react";
|
||||
import { ArrowLeft, BookMarked, Check, ChevronRight, Cpu, Gauge, LockKeyhole, PlugZap, Search, ShieldCheck, Sparkles, Wrench } from "lucide-react";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
import type { CanvasTheme } from "@/lib/canvas-theme";
|
||||
import AgentMemoryPane from "@/pages/settings/agent-memory-pane";
|
||||
import type { AgentPermissionMode, AgentProfileLayer, AgentProfileScope, AgentProfileView, AgentReasoningMode } from "@/services/api/agent";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
export type AgentContextKey = "canvas" | "resources" | "generation_history" | "skills" | "project";
|
||||
type SettingsSection = "home" | "profile" | "skills" | "mcp" | "context" | "budget";
|
||||
type SettingsSection = "home" | "profile" | "memories" | "skills" | "mcp" | "context" | "budget";
|
||||
|
||||
type AgentSettingsProps = {
|
||||
theme: CanvasTheme;
|
||||
@@ -83,6 +84,7 @@ export function CanvasCloudAgentSettings(props: AgentSettingsProps) {
|
||||
|
||||
{section === "home" ? <SettingsHome props={props} theme={theme} onOpen={setSection} /> : null}
|
||||
{section === "profile" ? <ProfileWorkspace props={props} theme={theme} /> : null}
|
||||
{section === "memories" ? <MemoriesWorkspace /> : null}
|
||||
{section === "skills" ? <SkillsWorkspace props={props} theme={theme} tab={skillTab} onTabChange={setSkillTab} /> : null}
|
||||
{section === "mcp" ? <McpWorkspace theme={theme} /> : null}
|
||||
{section === "context" ? <ContextWorkspace props={props} theme={theme} /> : null}
|
||||
@@ -126,6 +128,7 @@ function SettingsHome({ props, theme, onOpen }: { props: AgentSettingsProps; the
|
||||
<div className="space-y-1">
|
||||
<SettingRow theme={theme} icon={<Cpu className="size-4" />} title="上下文" summary={`${props.nodeCount} 个节点 · ${props.contextScope.length} 个范围`} onClick={() => onOpen("context")} />
|
||||
<SettingRow theme={theme} icon={<Sparkles className="size-4" />} title="长期偏好" summary={profileSummary(props.profileView)} onClick={() => onOpen("profile")} />
|
||||
<SettingRow theme={theme} icon={<BookMarked className="size-4" />} title="个人记忆" summary="批准、添加、导出导入;只影响你的会话" onClick={() => onOpen("memories")} />
|
||||
<SettingRow theme={theme} icon={<Sparkles className="size-4" />} title="Skills · 用户技能库" summary={`${props.installedSkills.length} 个已安装 · 本轮启用 ${props.selectedSkillIds.length} 个`} onClick={() => onOpen("skills")} />
|
||||
<SettingRow theme={theme} icon={<Wrench className="size-4" />} title="工具与连接" summary="画布、技能参考文件、生成任务" onClick={() => onOpen("mcp")} />
|
||||
<SettingRow theme={theme} icon={<Gauge className="size-4" />} title="预算" summary={`每轮最多 ${props.maxCredits || "未设置"} 积分 · 固定计价模型`} onClick={() => onOpen("budget")} />
|
||||
@@ -135,6 +138,14 @@ function SettingsHome({ props, theme, onOpen }: { props: AgentSettingsProps; the
|
||||
);
|
||||
}
|
||||
|
||||
function MemoriesWorkspace() {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden p-4">
|
||||
<AgentMemoryPane compact />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillsWorkspace({ props, theme, tab, onTabChange }: { props: AgentSettingsProps; theme: CanvasTheme; tab: "installed" | "market"; onTabChange: (tab: "installed" | "market") => void }) {
|
||||
const query = props.skillSearch.trim().toLocaleLowerCase();
|
||||
const source = tab === "installed" ? props.installedSkills.filter((skill) => `${skill.skillName} ${skill.description || ""}`.toLocaleLowerCase().includes(query)) : props.marketSkills;
|
||||
@@ -302,8 +313,8 @@ function SettingRow({ theme, icon, title, summary, onClick }: { theme: CanvasThe
|
||||
function TabButton({ active, label, onClick, theme }: { active: boolean; label: string; onClick: () => void; theme: CanvasTheme }) { return <button type="button" aria-pressed={active} className="min-w-0 flex-1 rounded-lg px-2 py-2 text-xs font-medium transition-colors focus-visible:outline focus-visible:outline-2" style={{ background: active ? theme.toolbar.itemHover : "transparent", color: active ? theme.node.text : theme.node.muted }} onClick={onClick}>{label}</button>; }
|
||||
function SettingLabel({ label, hint }: { label: string; hint?: string }) { return <div className="mb-2 flex items-center justify-between text-xs font-semibold"><span>{label}</span>{hint ? <span className="text-[10px] font-normal opacity-40">{hint}</span> : null}</div>; }
|
||||
function BudgetInput({ label, hint, value, onChange, theme }: { label: string; hint: string; value: string; onChange: (value: string) => void; theme: CanvasTheme }) { return <label className="block"><span className="flex items-center justify-between text-xs font-medium"><span>{label}</span><span className="text-[10px] opacity-40">{hint}</span></span><Input size="large" value={value} onChange={(event) => onChange(event.target.value.replace(/[^0-9]/g, ""))} inputMode="numeric" className="mt-2 !rounded-lg" style={{ background: theme.node.fill }} /></label>; }
|
||||
function sectionTitle(section: SettingsSection) { return section === "profile" ? "长期偏好" : section === "skills" ? "Skills" : section === "mcp" ? "MCP 与工具" : section === "context" ? "上下文" : "预算"; }
|
||||
function sectionSubtitle(section: SettingsSection) { return section === "profile" ? "用户、项目和画布的长期行为偏好" : section === "skills" ? "搜索、安装并选择本轮技能" : section === "mcp" ? "云端工具与连接状态" : section === "context" ? "控制 Agent 能读取的范围" : "控制本轮积分与生成消耗"; }
|
||||
function sectionTitle(section: SettingsSection) { return section === "profile" ? "长期偏好" : section === "memories" ? "个人记忆" : section === "skills" ? "Skills" : section === "mcp" ? "MCP 与工具" : section === "context" ? "上下文" : "预算"; }
|
||||
function sectionSubtitle(section: SettingsSection) { return section === "profile" ? "用户、项目和画布的长期行为偏好" : section === "memories" ? "只属于你,批准后才会注入会话" : section === "skills" ? "搜索、安装并选择本轮技能" : section === "mcp" ? "云端工具与连接状态" : section === "context" ? "控制 Agent 能读取的范围" : "控制本轮积分与生成消耗"; }
|
||||
|
||||
export function agentPermissionLabel(mode: AgentPermissionMode) { return permissionOptions.find((option) => option.value === mode)?.label || "请求审批"; }
|
||||
export function agentPermissionVisual(mode: AgentPermissionMode) { const option = permissionOptions.find((item) => item.value === mode) || permissionOptions[0]; return { color: option.color, soft: `${option.color}1f` }; }
|
||||
|
||||
@@ -3,28 +3,11 @@
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* Agent 浮窗高于普通画布层,模型菜单必须叠在对话窗之上。 */
|
||||
/* Agent 浮窗高于普通画布层,模型菜单必须叠在对话窗之上;菜单本身与全站二级选择器同一套。 */
|
||||
.agent-model-picker-popover {
|
||||
z-index: calc(var(--z-modal-overlay) + 1000) !important;
|
||||
}
|
||||
|
||||
.agent-model-picker-popover .canvas-model-picker-menu {
|
||||
width: min(340px, calc(100vw - 24px)) !important;
|
||||
max-width: min(340px, calc(100vw - 24px)) !important;
|
||||
}
|
||||
|
||||
.agent-model-picker-popover .canvas-model-picker-two-pane {
|
||||
grid-template-columns: minmax(0, 1fr) !important;
|
||||
}
|
||||
|
||||
.agent-model-picker-popover .canvas-model-picker-brand-rail {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.agent-model-picker-popover .canvas-model-picker-model-pane {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* Flex 子页必须允许收缩,长模型名和 Skill 文案不能决定窗口宽度。 */
|
||||
.canvas-agent-panel > div,
|
||||
.canvas-agent-settings,
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ModelPicker({
|
||||
onMissingConfig,
|
||||
showSelectedPrice = true,
|
||||
showOptionPrices = showSelectedPrice,
|
||||
variant = "default",
|
||||
variant = "creation",
|
||||
requirements,
|
||||
showConfiguredModelName = false,
|
||||
}: ModelPickerProps) {
|
||||
@@ -173,10 +173,8 @@ export function ModelPicker({
|
||||
ref={menuRef}
|
||||
data-canvas-no-zoom
|
||||
className={cn(
|
||||
"canvas-model-picker-menu max-w-[calc(100vw-24px)]",
|
||||
creationVariant
|
||||
? cn("creation-model-picker-menu", activeGroupKey === null ? "is-brand-list" : "is-model-list")
|
||||
: "w-[var(--panel-width-compact)]",
|
||||
"canvas-model-picker-menu creation-model-picker-menu max-w-[calc(100vw-24px)]",
|
||||
activeGroupKey === null ? "is-brand-list" : "is-model-list",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
@@ -252,7 +250,7 @@ export function ModelPicker({
|
||||
model={displayModel}
|
||||
capability={capability}
|
||||
theme={theme}
|
||||
creationVariant={creationVariant}
|
||||
creationVariant
|
||||
showConfiguredModelName={showConfiguredModelName}
|
||||
showPrice={showOptionPrices && creditsEnabled}
|
||||
disabledReason={disabledReason}
|
||||
@@ -283,8 +281,8 @@ export function ModelPicker({
|
||||
arrow={false}
|
||||
content={content}
|
||||
classNames={{
|
||||
root: cn("canvas-model-picker-popover", creationVariant && "creation-model-picker-popover", popoverClassName),
|
||||
container: cn("canvas-composer-popover-surface", creationVariant && "creation-model-picker-surface"),
|
||||
root: cn("canvas-model-picker-popover", "creation-model-picker-popover", popoverClassName),
|
||||
container: cn("canvas-composer-popover-surface", "creation-model-picker-surface"),
|
||||
content: "canvas-composer-popover-content",
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user