feat(canvas): 云端 Agent - 完成策略分层、节点能力注册与可解释审批改造
This commit is contained in:
289
web/src/components/canvas/canvas-agent-skill-library-modal.tsx
Normal file
289
web/src/components/canvas/canvas-agent-skill-library-modal.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Input } from "antd";
|
||||
import { Check, LibraryBig, LoaderCircle, Plus, Search, Sparkles, Users } from "lucide-react";
|
||||
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import type { CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { Skill, SkillCategory } from "@/services/api/skills";
|
||||
|
||||
type SkillLibraryTab = "enabled" | "installed" | "market";
|
||||
|
||||
type CanvasAgentSkillLibraryModalProps = {
|
||||
open: boolean;
|
||||
theme: CanvasTheme;
|
||||
installedSkills: Skill[];
|
||||
marketSkills: Skill[];
|
||||
selectedSkillIds: string[];
|
||||
categories: SkillCategory[];
|
||||
category: string;
|
||||
search: string;
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
onClose: () => void;
|
||||
onCategoryChange: (value: string) => void;
|
||||
onSearch: (value: string) => void;
|
||||
onToggle: (skillId: string) => void;
|
||||
onInstall: (skill: Skill) => Promise<void>;
|
||||
onLoadMore: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function CanvasAgentSkillLibraryModal({
|
||||
open,
|
||||
theme,
|
||||
installedSkills,
|
||||
marketSkills,
|
||||
selectedSkillIds,
|
||||
categories,
|
||||
category,
|
||||
search,
|
||||
loading,
|
||||
hasMore,
|
||||
onClose,
|
||||
onCategoryChange,
|
||||
onSearch,
|
||||
onToggle,
|
||||
onInstall,
|
||||
onLoadMore,
|
||||
}: CanvasAgentSkillLibraryModalProps) {
|
||||
const [tab, setTab] = useState<SkillLibraryTab>("enabled");
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
const wasOpenRef = useRef(false);
|
||||
const selectedCount = selectedSkillIds.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (open && !wasOpenRef.current) setTab(selectedCount > 0 ? "enabled" : "installed");
|
||||
wasOpenRef.current = open;
|
||||
}, [open, selectedCount]);
|
||||
|
||||
const visibleSkills = useMemo(() => {
|
||||
const keyword = search.trim().toLocaleLowerCase("zh-CN");
|
||||
const source = tab === "enabled"
|
||||
? installedSkills.filter((skill) => selectedSkillIds.includes(skill.skillId))
|
||||
: tab === "installed"
|
||||
? installedSkills
|
||||
: marketSkills;
|
||||
|
||||
return source.filter((skill) => {
|
||||
if (category !== "all" && skill.tag !== category) return false;
|
||||
if (!keyword) return true;
|
||||
return `${skill.skillName} ${skill.description || ""} ${skill.effectiveUser?.name || ""}`
|
||||
.toLocaleLowerCase("zh-CN")
|
||||
.includes(keyword);
|
||||
});
|
||||
}, [category, installedSkills, marketSkills, search, selectedSkillIds, tab]);
|
||||
|
||||
useEffect(() => {
|
||||
const target = loadMoreRef.current;
|
||||
if (tab !== "market" || !target || !hasMore || loading || typeof IntersectionObserver === "undefined") return;
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) void onLoadMore();
|
||||
}, { root: listRef.current, rootMargin: "180px 0px" });
|
||||
observer.observe(target);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, loading, onLoadMore, tab, visibleSkills.length]);
|
||||
|
||||
const categoryItems = useMemo(() => {
|
||||
const unique = new Map<string, SkillCategory>();
|
||||
categories.forEach((item) => {
|
||||
if (item.value && item.label) unique.set(item.value, item);
|
||||
});
|
||||
return [{ value: "all", label: "全部" }, ...unique.values()];
|
||||
}, [categories]);
|
||||
|
||||
const emptyText = tab === "enabled"
|
||||
? "本轮还没有启用 Skill,可在“已加入”中选择"
|
||||
: tab === "installed"
|
||||
? "还没有匹配的已加入 Skill,可前往“发现 Skills”添加"
|
||||
: "没有匹配的公开 Skill,换个关键词或分类试试";
|
||||
|
||||
return (
|
||||
<AppModal
|
||||
rootClassName="canvas-agent-skill-library-modal"
|
||||
open={open}
|
||||
title={null}
|
||||
footer={null}
|
||||
centered
|
||||
width="min(1180px, calc(100vw - 24px))"
|
||||
onCancel={onClose}
|
||||
flush
|
||||
>
|
||||
<section
|
||||
className="canvas-agent-skill-library-shell"
|
||||
style={{ color: theme.node.text, background: theme.node.panel }}
|
||||
data-canvas-no-zoom
|
||||
data-canvas-wheel-scroll
|
||||
aria-label="Agent Skills 技能库"
|
||||
>
|
||||
<header className="canvas-agent-skill-library-header">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-lg font-semibold">
|
||||
<LibraryBig className="size-5" aria-hidden="true" />
|
||||
Skills 技能库
|
||||
</div>
|
||||
<p className="mt-1 text-xs" style={{ color: theme.node.muted }}>按分类浏览和搜索技能,本轮最多启用 8 个,发送消息时固定版本。</p>
|
||||
</div>
|
||||
<div className="canvas-agent-skill-library-count" style={{ color: theme.node.muted }}>
|
||||
已启用 <strong style={{ color: selectedCount >= 8 ? "var(--destructive)" : theme.accent.primary }}>{selectedCount}</strong>/8
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="canvas-agent-skill-library-toolbar">
|
||||
<div className="canvas-agent-skill-library-tabs" role="tablist" aria-label="Skills 视图">
|
||||
<SkillTab active={tab === "enabled"} label="已启用" count={selectedCount} onClick={() => setTab("enabled")} />
|
||||
<SkillTab active={tab === "installed"} label="已加入" count={installedSkills.length} onClick={() => setTab("installed")} />
|
||||
<SkillTab active={tab === "market"} label="发现 Skills" onClick={() => setTab("market")} />
|
||||
</div>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<Search className="size-4" style={{ color: theme.node.muted }} aria-hidden="true" />}
|
||||
value={search}
|
||||
onChange={(event) => onSearch(event.target.value)}
|
||||
placeholder="搜索 Skill 名称、说明或作者"
|
||||
aria-label="搜索 Skills"
|
||||
className="canvas-agent-skill-library-search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav className="canvas-agent-skill-library-categories" aria-label="Skill 分类">
|
||||
{categoryItems.map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
className={`canvas-agent-skill-category ${category === item.value ? "is-active" : ""}`}
|
||||
aria-pressed={category === item.value}
|
||||
onClick={() => onCategoryChange(item.value)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div ref={listRef} className="canvas-agent-skill-library-list thin-scrollbar" role="tabpanel">
|
||||
{visibleSkills.map((skill) => (
|
||||
<SkillLibraryCard
|
||||
key={skill.skillId}
|
||||
skill={skill}
|
||||
theme={theme}
|
||||
categories={categories}
|
||||
selected={selectedSkillIds.includes(skill.skillId)}
|
||||
canSelect={selectedCount < 8 || selectedSkillIds.includes(skill.skillId)}
|
||||
onToggle={() => onToggle(skill.skillId)}
|
||||
onInstall={() => onInstall(skill)}
|
||||
/>
|
||||
))}
|
||||
{loading && visibleSkills.length === 0 ? (
|
||||
<div className="canvas-agent-skill-library-state" style={{ color: theme.node.muted }}>
|
||||
<LoaderCircle className="size-5 animate-spin motion-reduce:animate-none" aria-hidden="true" />
|
||||
正在读取技能库…
|
||||
</div>
|
||||
) : null}
|
||||
{!loading && visibleSkills.length === 0 ? (
|
||||
<div className="canvas-agent-skill-library-state" style={{ color: theme.node.muted }}>
|
||||
<Sparkles className="size-6" aria-hidden="true" />
|
||||
<span>{emptyText}</span>
|
||||
{tab !== "market" ? <Button size="small" onClick={() => setTab("market")}>浏览公开 Skills</Button> : null}
|
||||
</div>
|
||||
) : null}
|
||||
{tab === "market" && hasMore ? <div ref={loadMoreRef} className="col-span-full h-px" aria-hidden="true" /> : null}
|
||||
</div>
|
||||
|
||||
<footer className="canvas-agent-skill-library-footer" style={{ color: theme.node.muted, borderColor: theme.node.stroke }}>
|
||||
<span>{tab === "enabled" ? `${selectedCount} 个技能将在本轮生效` : tab === "installed" ? `${visibleSkills.length} 个已加入技能` : `已加载 ${marketSkills.length} 个公开技能`}</span>
|
||||
{tab === "market" && hasMore ? (
|
||||
<Button size="small" disabled={loading} loading={loading} onClick={() => void onLoadMore()}>{loading ? "加载中" : "加载更多"}</Button>
|
||||
) : <span>{tab === "market" ? "已加载全部" : "最多启用 8 个"}</span>}
|
||||
</footer>
|
||||
</section>
|
||||
</AppModal>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillTab({ active, label, count, onClick }: { active: boolean; label: string; count?: number; onClick: () => void }) {
|
||||
return (
|
||||
<button type="button" role="tab" aria-selected={active} className={`canvas-agent-skill-library-tab ${active ? "is-active" : ""}`} onClick={onClick}>
|
||||
{label}
|
||||
{typeof count === "number" ? <span>{count}</span> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillLibraryCard({ skill, theme, categories, selected, canSelect, onToggle, onInstall }: {
|
||||
skill: Skill;
|
||||
theme: CanvasTheme;
|
||||
categories: SkillCategory[];
|
||||
selected: boolean;
|
||||
canSelect: boolean;
|
||||
onToggle: () => void;
|
||||
onInstall: () => Promise<void>;
|
||||
}) {
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [coverFailed, setCoverFailed] = useState(false);
|
||||
const coverUrl = skill.showcaseMedia?.find((item) => item.showcaseUrl)?.showcaseUrl;
|
||||
const categoryLabel = categories.find((item) => item.value === skill.tag)?.label || "其他";
|
||||
const author = skill.effectiveUser?.name || "影策创作者";
|
||||
const addedCount = formatSkillCount(skill.addedCount || 0);
|
||||
|
||||
useEffect(() => setCoverFailed(false), [coverUrl]);
|
||||
|
||||
const install = async () => {
|
||||
if (installing) return;
|
||||
setInstalling(true);
|
||||
try {
|
||||
await onInstall();
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article className={`canvas-agent-skill-card ${selected ? "is-selected" : ""}`} style={{ borderColor: selected ? theme.accent.primary : theme.node.stroke }}>
|
||||
<div className="canvas-agent-skill-card-cover" style={{ background: theme.node.fill }}>
|
||||
{coverUrl && !coverFailed ? (
|
||||
<img src={coverUrl} alt="" loading="lazy" onError={() => setCoverFailed(true)} />
|
||||
) : (
|
||||
<div className="canvas-agent-skill-card-placeholder" aria-hidden="true">
|
||||
<Sparkles className="size-7" />
|
||||
</div>
|
||||
)}
|
||||
<span className="canvas-agent-skill-card-category">{categoryLabel}</span>
|
||||
</div>
|
||||
<div className="canvas-agent-skill-card-body">
|
||||
<div className="canvas-agent-skill-card-title-row">
|
||||
<h3 title={skill.skillName}>{skill.skillName}</h3>
|
||||
{skill.version ? <span className="canvas-agent-skill-card-version">v{skill.version}</span> : null}
|
||||
</div>
|
||||
<p title={skill.description || "暂无技能说明"}>{skill.description || "暂无技能说明"}</p>
|
||||
<div className="canvas-agent-skill-card-meta" style={{ color: theme.node.muted }}>
|
||||
<span className="min-w-0 truncate">{author}</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span className="inline-flex shrink-0 items-center gap-1"><Users className="size-3.5" aria-hidden="true" />{addedCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="canvas-agent-skill-card-action">
|
||||
{skill.isAdded ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
className={selected ? "is-selected" : ""}
|
||||
icon={selected ? <Check className="size-3.5" /> : <Plus className="size-3.5" />}
|
||||
disabled={!canSelect}
|
||||
aria-pressed={selected}
|
||||
title={!canSelect ? "本轮最多启用 8 个 Skills" : undefined}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{selected ? "已启用" : "启用"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="small" icon={<Plus className="size-3.5" />} loading={installing} disabled={installing} onClick={() => void install()}>
|
||||
加入
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSkillCount(value: number) {
|
||||
return new Intl.NumberFormat("zh-CN", { notation: "compact", maximumFractionDigits: 1 }).format(value);
|
||||
}
|
||||
@@ -570,8 +570,8 @@ export function AgentChatComposer({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<div className="agent-composer-toolbar mt-2">
|
||||
<div className="agent-composer-controls flex min-w-0 items-center gap-1">
|
||||
{onAddFiles ? (
|
||||
<>
|
||||
<input
|
||||
@@ -600,7 +600,7 @@ export function AgentChatComposer({
|
||||
) : null}
|
||||
{left}
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="agent-composer-submit flex items-center gap-2">
|
||||
<span className="agent-composer-send-hint">Enter 换行 · ⌘/Ctrl+Enter 发送</span>
|
||||
<motion.button
|
||||
type="button"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { Button, Dropdown, Input, Popover } from "antd";
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { Button, Dropdown, Input } from "antd";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowLeft, Bot, Check, ChevronRight, CircleDot, Clock3, Download, History, LoaderCircle, MessageSquarePlus, Plus, Search, Settings2, ShieldCheck, Trash2, Sparkles, X } from "lucide-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";
|
||||
@@ -11,10 +11,10 @@ import { ModelPicker } from "@/components/model-picker";
|
||||
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 } from "@/lib/canvas/agent-error-presentation";
|
||||
import { cancelAgentRun, getAgentCapabilities, getAgentRun, createAgentRun, decideAgentApproval, sendAgentMessage, subscribeAgentEvents, type AgentEvent, type AgentPermissionMode, type AgentPersonality, type AgentRun } from "@/services/api/agent";
|
||||
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 { agentApprovalPresentation } from "@/lib/canvas/agent-approval-presentation";
|
||||
import { addSkill, listAddedSkills, listSkills, type Skill } from "@/services/api/skills";
|
||||
import { addSkill, listAddedSkills, listSkills, type Skill, type SkillCategory } from "@/services/api/skills";
|
||||
import { clearCloudAgentPendingSubmission, cloudAgentConversationTitle, loadCloudAgentConversations, loadCloudAgentPendingSubmission, saveCloudAgentConversations, saveCloudAgentPendingSubmission, type CloudAgentConversation, type CloudAgentPendingSubmission } from "@/services/cloud-agent-conversations";
|
||||
import { logicalModelIDForConfig, modelOptionName, resolveModelRequestConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
@@ -22,15 +22,16 @@ import { applyAgentCanvasPatches, refreshCanvasAfterAgent, saveRemoteUserDataNow
|
||||
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 { 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";
|
||||
import "./canvas-cloud-agent.css";
|
||||
|
||||
type CloudAgentPanelProps = { canvasId: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
|
||||
type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
|
||||
type ApprovalState = { approvalId: string; detail: Record<string, unknown>; reason: string };
|
||||
type AgentPanelView = "chat" | "history" | "settings";
|
||||
|
||||
export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
|
||||
export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const config = useEffectiveConfig();
|
||||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||||
@@ -41,8 +42,12 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
const [connectionEpoch, setConnectionEpoch] = useState(0);
|
||||
const [messages, setMessages] = useState<CloudAgentChatMessage[]>([]);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [thinking, setThinking] = useState(false);
|
||||
const [personality, setPersonality] = useState<AgentPersonality>("务实");
|
||||
const [reasoningMode, setReasoningMode] = useState<AgentReasoningMode>("off");
|
||||
const [profileView, setProfileView] = useState<AgentProfileView | null>(null);
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
const [profileSaving, setProfileSaving] = useState(false);
|
||||
const [profileError, setProfileError] = useState<string>();
|
||||
const profileRequestRef = useRef(0);
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]);
|
||||
const [marketSkills, setMarketSkills] = useState<Skill[]>([]);
|
||||
@@ -51,6 +56,8 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
const [skillsLoading, setSkillsLoading] = useState(false);
|
||||
const [skillHasMore, setSkillHasMore] = useState(false);
|
||||
const [skillPage, setSkillPage] = useState(1);
|
||||
const [skillCategories, setSkillCategories] = useState<SkillCategory[]>([]);
|
||||
const [skillTag, setSkillTag] = useState("all");
|
||||
const [skillsOpen, setSkillsOpen] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [approvalSubmitting, setApprovalSubmitting] = useState(false);
|
||||
@@ -88,8 +95,8 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
currentScope.current = conversationScope;
|
||||
const running = Boolean(run?.cleanupPending) || run?.status === "running" || run?.status === "queued" || run?.status === "waiting_approval";
|
||||
const selectedModel = config.textModel || config.model || "";
|
||||
const thinkingSupported = Boolean(modelCapabilityConfigFor(config, selectedModel).text?.thinking);
|
||||
useEffect(() => { if (!thinkingSupported && thinking) setThinking(false); }, [thinkingSupported, thinking]);
|
||||
const reasoningSupported = Boolean(modelCapabilityConfigFor(config, selectedModel).text?.thinking);
|
||||
useEffect(() => { if (!reasoningSupported && reasoningMode !== "off") setReasoningMode("off"); }, [reasoningSupported, reasoningMode]);
|
||||
const installedSkills = useMemo(() => skills.filter((skill) => skill.isAdded), [skills]);
|
||||
const enabledSkills = useMemo(() => installedSkills.filter((skill) => selectedSkillIds.includes(skill.skillId)), [installedSkills, selectedSkillIds]);
|
||||
const status = run?.status || "idle";
|
||||
@@ -100,6 +107,37 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
if (!open || view !== "chat") setSkillsOpen(false);
|
||||
}, [open, view]);
|
||||
|
||||
const reloadProfile = useCallback(async () => {
|
||||
const requestId = ++profileRequestRef.current;
|
||||
setProfileLoading(true);
|
||||
setProfileError(undefined);
|
||||
setProfileView(null);
|
||||
try {
|
||||
const result = await getAgentProfile({ projectId: domainProjectId, canvasId });
|
||||
if (profileRequestRef.current === requestId) setProfileView(result);
|
||||
} catch (cause) {
|
||||
if (profileRequestRef.current === requestId) setProfileError(cause instanceof Error ? cause.message : String(cause));
|
||||
} finally {
|
||||
if (profileRequestRef.current === requestId) setProfileLoading(false);
|
||||
}
|
||||
}, [canvasId, domainProjectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void reloadProfile();
|
||||
}, [reloadProfile]);
|
||||
|
||||
const saveProfile = async (input: { scope: AgentProfileScope; projectId?: string; canvasId?: string; content: string; revision: number }) => {
|
||||
setProfileSaving(true);
|
||||
try {
|
||||
const result = await updateAgentProfile(input);
|
||||
setProfileView(result);
|
||||
setProfileError(undefined);
|
||||
return result;
|
||||
} finally {
|
||||
setProfileSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => setDebouncedSkillSearch(skillSearch.trim()), 250);
|
||||
return () => window.clearTimeout(timer);
|
||||
@@ -128,12 +166,19 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
if (view !== "settings" && !skillsOpen) return;
|
||||
let active = true;
|
||||
setSkillsLoading(true);
|
||||
void listSkills({ scope: "public", search: debouncedSkillSearch || undefined, pageSize: 20, sort: "popular" })
|
||||
void listSkills({
|
||||
scope: "public",
|
||||
search: debouncedSkillSearch || undefined,
|
||||
tag: skillsOpen && skillTag !== "all" ? skillTag : undefined,
|
||||
pageSize: 20,
|
||||
sort: "popular",
|
||||
})
|
||||
.then((result) => {
|
||||
if (active) {
|
||||
setMarketSkills(result.skills);
|
||||
setSkillHasMore(result.hasMore);
|
||||
setSkillPage(result.page);
|
||||
if (result.categories.length > 0) setSkillCategories(result.categories);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -145,14 +190,21 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [view, skillsOpen, debouncedSkillSearch]);
|
||||
}, [view, skillsOpen, debouncedSkillSearch, skillTag]);
|
||||
|
||||
const loadMoreSkills = async () => {
|
||||
if (skillsLoading || skillPageRequestRef.current || !skillHasMore || skillSearch.trim() !== debouncedSkillSearch) return;
|
||||
skillPageRequestRef.current = true;
|
||||
setSkillsLoading(true);
|
||||
try {
|
||||
const result = await listSkills({ scope: "public", search: debouncedSkillSearch || undefined, page: skillPage + 1, pageSize: 20, sort: "popular" });
|
||||
const result = await listSkills({
|
||||
scope: "public",
|
||||
search: debouncedSkillSearch || undefined,
|
||||
tag: skillsOpen && skillTag !== "all" ? skillTag : undefined,
|
||||
page: skillPage + 1,
|
||||
pageSize: 20,
|
||||
sort: "popular",
|
||||
});
|
||||
setMarketSkills((current) => [...current, ...result.skills.filter((skill) => !current.some((item) => item.skillId === skill.skillId))]);
|
||||
setSkillPage(result.page);
|
||||
setSkillHasMore(result.hasMore);
|
||||
@@ -288,6 +340,8 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
// Editing model settings or prompt must not silently create a new charge.
|
||||
if (pending?.request && pending.request.prompt !== value) throw new Error("上一条请求结果待确认,请先原样重试上一条消息,再发送新要求");
|
||||
if (!pending?.request) {
|
||||
if (profileLoading) throw new Error("正在确认长期偏好快照,请稍后再发送");
|
||||
if (!profileView || profileError) throw new Error("长期偏好快照尚未确认,请重新读取后再发送");
|
||||
const capabilities = await getAgentCapabilities();
|
||||
if (currentScope.current !== scope) return;
|
||||
if (!capabilities.permissionModes.includes(permissionMode)) throw new Error("当前后端不支持所选 Agent 权限,请更新后端");
|
||||
@@ -298,7 +352,7 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
const requestConfig = resolveModelRequestConfig(agentConfig, selectedModel);
|
||||
const logicalModelId = logicalModelIDForConfig(agentConfig);
|
||||
const input = {
|
||||
canvasId, prompt: value, thinking, personality,
|
||||
canvasId, prompt: value, reasoningMode: reasoningSupported ? reasoningMode : "off", profileRevision: profileView.revision,
|
||||
model: modelOptionName(selectedModel) || undefined,
|
||||
...(logicalModelId ? { logicalModelId } : requestConfig.channelId ? { channelId: requestConfig.channelId, channelModelKey: modelOptionName(selectedModel) || undefined } : {}),
|
||||
skillIds: [...new Set([...selectedSkillIds, ...resolveSkillMentions(value, installedSkills).map((skill) => skill.skillId)])],
|
||||
@@ -351,7 +405,7 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
}
|
||||
}
|
||||
}
|
||||
setMessages((current) => appendAgentError(current, `submit-error-${activeConversationId}`, cause, accepted ? "运行已接收,但本地提交记录清理失败" : "请求未确认;重试将核对原请求,不重复创建"));
|
||||
setMessages((current) => appendAgentError(current, `submit-error-${activeConversationId}`, cause, agentSubmissionErrorTitle(cause, accepted)));
|
||||
} finally {
|
||||
submissionRequestRef.current = false;
|
||||
if (currentScope.current === scope) setBusy(false);
|
||||
@@ -423,7 +477,7 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
const result = await addSkill(skill.skillId);
|
||||
setSkills((current) => [...current.filter((item) => item.skillId !== skill.skillId), result.skill]);
|
||||
setMarketSkills((current) => current.map((item) => (item.skillId === skill.skillId ? result.skill : item)));
|
||||
setSelectedSkillIds((current) => (current.includes(skill.skillId) ? current : [...current, skill.skillId]));
|
||||
setSelectedSkillIds((current) => (current.includes(skill.skillId) || current.length >= 8 ? current : [...current, skill.skillId]));
|
||||
} catch (cause) {
|
||||
setMessages((current) => appendAgentError(current, `skill-error-${Date.now()}`, cause, "添加 Skill 失败"));
|
||||
}
|
||||
@@ -536,8 +590,16 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
onBack={() => setView("chat")}
|
||||
onModelChange={setModel}
|
||||
onPermissionChange={setPermissionMode}
|
||||
personality={personality}
|
||||
onPersonalityChange={setPersonality}
|
||||
reasoningMode={reasoningMode}
|
||||
onReasoningModeChange={setReasoningMode}
|
||||
profileView={profileView}
|
||||
profileLoading={profileLoading}
|
||||
profileSaving={profileSaving}
|
||||
profileError={profileError}
|
||||
projectId={domainProjectId}
|
||||
canvasId={canvasId}
|
||||
onReloadProfile={reloadProfile}
|
||||
onSaveProfile={saveProfile}
|
||||
onContextToggle={(value) => setContextScope((current) => (current.includes(value) ? current.filter((item) => item !== value) : [...current, value]))}
|
||||
onSkillSearch={setSkillSearch}
|
||||
onSkillToggle={(id) => setSelectedSkillIds((current) => (current.includes(id) ? current.filter((item) => item !== id) : [...current, id]))}
|
||||
@@ -622,29 +684,17 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
includeAssetLibrary={false}
|
||||
left={
|
||||
<ComposerControls
|
||||
thinking={thinkingSupported && thinking}
|
||||
onThinkingChange={(value) => { if (thinkingSupported) setThinking(value); }}
|
||||
thinkingSupported={thinkingSupported}
|
||||
reasoningMode={reasoningSupported ? reasoningMode : "off"}
|
||||
reasoningSupported={reasoningSupported}
|
||||
onReasoningModeChange={(value) => { if (reasoningSupported) setReasoningMode(value); }}
|
||||
config={config}
|
||||
selectedModel={selectedModel}
|
||||
permissionMode={permissionMode}
|
||||
theme={theme}
|
||||
onModelChange={setModel}
|
||||
onPermissionChange={setPermissionMode}
|
||||
personality={personality}
|
||||
onPersonalityChange={setPersonality}
|
||||
skillsOpen={skillsOpen}
|
||||
onSkillsOpenChange={setSkillsOpen}
|
||||
installedSkills={installedSkills}
|
||||
marketSkills={marketSkills}
|
||||
selectedSkillIds={selectedSkillIds}
|
||||
skillSearch={skillSearch}
|
||||
skillsLoading={skillsLoading}
|
||||
skillHasMore={skillHasMore}
|
||||
onSkillSearch={setSkillSearch}
|
||||
onSkillToggle={(id) => setSelectedSkillIds((current) => (current.includes(id) ? current.filter((item) => item !== id) : [...current, id]))}
|
||||
onSkillInstall={installSkill}
|
||||
onLoadMoreSkills={loadMoreSkills}
|
||||
selectedSkillCount={selectedSkillIds.length}
|
||||
/>
|
||||
}
|
||||
@@ -655,6 +705,27 @@ export function CanvasCloudAgentPanel({ canvasId, nodeCount, references, open, o
|
||||
</motion.aside>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<CanvasAgentSkillLibraryModal
|
||||
open={skillsOpen}
|
||||
theme={theme}
|
||||
installedSkills={installedSkills}
|
||||
marketSkills={marketSkills}
|
||||
selectedSkillIds={selectedSkillIds}
|
||||
categories={skillCategories}
|
||||
category={skillTag}
|
||||
search={skillSearch}
|
||||
loading={skillsLoading}
|
||||
hasMore={skillHasMore}
|
||||
onClose={() => setSkillsOpen(false)}
|
||||
onCategoryChange={setSkillTag}
|
||||
onSearch={setSkillSearch}
|
||||
onToggle={(id) => setSelectedSkillIds((current) => {
|
||||
if (current.includes(id)) return current.filter((item) => item !== id);
|
||||
return current.length < 8 ? [...current, id] : current;
|
||||
})}
|
||||
onInstall={installSkill}
|
||||
onLoadMore={loadMoreSkills}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -810,7 +881,7 @@ function AgentConversation({
|
||||
{messages.map((item) => (
|
||||
<AgentChatMessage key={item.id} item={item} theme={theme} references={references} onFocusNode={onFocusNode} isStreaming={busy && !approval && item.streaming === true && item === messages.at(-1)} />
|
||||
))}
|
||||
{approval ? <ApprovalCard key={approval.approvalId} approval={approval} theme={theme} submitting={approvalSubmitting} onReasonChange={onApprovalReasonChange} onApprove={onApprove} onReject={onReject} /> : null}
|
||||
{approval ? <ApprovalCard key={approval.approvalId} approval={approval} theme={theme} submitting={approvalSubmitting} onFocusNode={onFocusNode} onReasonChange={onApprovalReasonChange} onApprove={onApprove} onReject={onReject} /> : null}
|
||||
{busy && !approval ? (
|
||||
<AgentWorkingMessage theme={theme} label="正在处理当前画布" />
|
||||
) : null}
|
||||
@@ -832,11 +903,9 @@ function AgentEmptyState({ theme, nodeCount }: { theme: CanvasTheme; nodeCount:
|
||||
}
|
||||
|
||||
function ComposerControls({
|
||||
thinking,
|
||||
thinkingSupported,
|
||||
personality,
|
||||
onPersonalityChange,
|
||||
onThinkingChange,
|
||||
reasoningMode,
|
||||
reasoningSupported,
|
||||
onReasoningModeChange,
|
||||
config,
|
||||
selectedModel,
|
||||
permissionMode,
|
||||
@@ -845,23 +914,11 @@ function ComposerControls({
|
||||
onPermissionChange,
|
||||
skillsOpen,
|
||||
onSkillsOpenChange,
|
||||
installedSkills,
|
||||
marketSkills,
|
||||
selectedSkillIds,
|
||||
skillSearch,
|
||||
skillsLoading,
|
||||
skillHasMore,
|
||||
onSkillSearch,
|
||||
onSkillToggle,
|
||||
onSkillInstall,
|
||||
onLoadMoreSkills,
|
||||
selectedSkillCount,
|
||||
}: {
|
||||
thinking: boolean;
|
||||
thinkingSupported: boolean;
|
||||
personality: AgentPersonality;
|
||||
onPersonalityChange: (value: AgentPersonality) => void;
|
||||
onThinkingChange: (value: boolean) => void;
|
||||
reasoningMode: AgentReasoningMode;
|
||||
reasoningSupported: boolean;
|
||||
onReasoningModeChange: (value: AgentReasoningMode) => void;
|
||||
config: ReturnType<typeof useEffectiveConfig>;
|
||||
selectedModel: string;
|
||||
permissionMode: AgentPermissionMode;
|
||||
@@ -870,16 +927,6 @@ function ComposerControls({
|
||||
onPermissionChange: (mode: AgentPermissionMode) => void;
|
||||
skillsOpen: boolean;
|
||||
onSkillsOpenChange: (open: boolean) => void;
|
||||
installedSkills: Skill[];
|
||||
marketSkills: Skill[];
|
||||
selectedSkillIds: string[];
|
||||
skillSearch: string;
|
||||
skillsLoading: boolean;
|
||||
skillHasMore: boolean;
|
||||
onSkillSearch: (value: string) => void;
|
||||
onSkillToggle: (skillId: string) => void;
|
||||
onSkillInstall: (skill: Skill) => Promise<void>;
|
||||
onLoadMoreSkills: () => Promise<void>;
|
||||
selectedSkillCount: number;
|
||||
}) {
|
||||
const permissionVisual = agentPermissionVisual(permissionMode);
|
||||
@@ -896,9 +943,11 @@ function ComposerControls({
|
||||
showOptionPrices
|
||||
placeholder="选择模型"
|
||||
/>
|
||||
{thinkingSupported ? <button type="button" aria-pressed={thinking} title="思考模式:由当前文本模型提供" onClick={() => onThinkingChange(!thinking)} className="flex h-8 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/25" style={{ color: thinking ? theme.accent.primary : theme.node.muted, background: thinking ? theme.node.fill : "transparent" }}>
|
||||
<Sparkles className="size-3.5" />思考
|
||||
</button> : null}
|
||||
{reasoningSupported ? <Dropdown trigger={["click"]} placement="topLeft" menu={{ items: reasoningMenuItems(reasoningMode, onReasoningModeChange) }}>
|
||||
<button type="button" aria-label="选择 Agent 推理模式" title="推理模式:只用于规划和工具选择" className="flex h-8 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/25" style={{ color: reasoningMode === "off" ? theme.node.muted : theme.accent.primary, background: reasoningMode === "off" ? "transparent" : theme.node.fill }}>
|
||||
<Sparkles className="size-3.5" />{reasoningModeLabel(reasoningMode)}
|
||||
</button>
|
||||
</Dropdown> : null}
|
||||
<Dropdown trigger={["click"]} placement="topLeft" menu={{ items: agentPermissionMenuItems(permissionMode, onPermissionChange) }}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -910,163 +959,37 @@ function ComposerControls({
|
||||
<span className="max-w-20 truncate">{agentPermissionLabel(permissionMode)}</span>
|
||||
</button>
|
||||
</Dropdown>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="topLeft"
|
||||
arrow={false}
|
||||
open={skillsOpen}
|
||||
onOpenChange={onSkillsOpenChange}
|
||||
classNames={{ root: "canvas-agent-skill-popover" }}
|
||||
content={
|
||||
<AgentSkillPicker
|
||||
theme={theme}
|
||||
installedSkills={installedSkills}
|
||||
marketSkills={marketSkills}
|
||||
selectedSkillIds={selectedSkillIds}
|
||||
search={skillSearch}
|
||||
loading={skillsLoading}
|
||||
hasMore={skillHasMore}
|
||||
onSearch={onSkillSearch}
|
||||
onToggle={onSkillToggle}
|
||||
onInstall={onSkillInstall}
|
||||
onLoadMore={onLoadMoreSkills}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/25"
|
||||
style={{ color: selectedSkillCount ? theme.accent.primary : theme.node.muted }}
|
||||
aria-label={`选择 Skills${selectedSkillCount ? `,已选 ${selectedSkillCount} 个` : ""}`}
|
||||
aria-expanded={skillsOpen}
|
||||
title="选择 Skills"
|
||||
>
|
||||
<Sparkles className="size-3.5" />
|
||||
<span className="max-w-28 truncate">Skills技能包({selectedSkillCount})</span>
|
||||
</button>
|
||||
</Popover>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/25"
|
||||
style={{ color: selectedSkillCount ? theme.accent.primary : theme.node.muted, background: skillsOpen ? theme.node.fill : "transparent" }}
|
||||
aria-label={`打开 Skills 技能库${selectedSkillCount ? `,已启用 ${selectedSkillCount} 个` : ""}`}
|
||||
aria-expanded={skillsOpen}
|
||||
aria-haspopup="dialog"
|
||||
title="打开 Skills 技能库"
|
||||
onClick={() => onSkillsOpenChange(true)}
|
||||
>
|
||||
<Sparkles className="size-3.5" />
|
||||
<span className="max-w-28 truncate">Skills技能包({selectedSkillCount})</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentSkillPicker({
|
||||
theme,
|
||||
installedSkills,
|
||||
marketSkills,
|
||||
selectedSkillIds,
|
||||
search,
|
||||
loading,
|
||||
hasMore,
|
||||
onSearch,
|
||||
onToggle,
|
||||
onInstall,
|
||||
onLoadMore,
|
||||
}: {
|
||||
theme: CanvasTheme;
|
||||
installedSkills: Skill[];
|
||||
marketSkills: Skill[];
|
||||
selectedSkillIds: string[];
|
||||
search: string;
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
onSearch: (value: string) => void;
|
||||
onToggle: (skillId: string) => void;
|
||||
onInstall: (skill: Skill) => Promise<void>;
|
||||
onLoadMore: () => Promise<void>;
|
||||
}) {
|
||||
const [tab, setTab] = useState<"installed" | "market">("installed");
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
const normalizedSearch = search.trim().toLocaleLowerCase();
|
||||
const source = (tab === "installed" ? installedSkills : marketSkills).filter((skill) => `${skill.skillName} ${skill.description || ""}`.toLocaleLowerCase().includes(normalizedSearch));
|
||||
const selectedCount = selectedSkillIds.length;
|
||||
const allSelected = tab === "installed" && source.length > 0 && source.every((skill) => selectedSkillIds.includes(skill.skillId));
|
||||
const toggleAll = () => {
|
||||
let added = 0;
|
||||
source.forEach((skill) => {
|
||||
const selected = selectedSkillIds.includes(skill.skillId);
|
||||
if (allSelected ? selected : !selected && added < Math.max(0, 8 - selectedCount)) {
|
||||
onToggle(skill.skillId);
|
||||
added += 1;
|
||||
}
|
||||
});
|
||||
};
|
||||
const reasoningLabels: Record<AgentReasoningMode, string> = { off: "直达", auto: "自动推理", deep: "深入推理" };
|
||||
|
||||
useEffect(() => {
|
||||
const target = loadMoreRef.current;
|
||||
if (tab !== "market" || !target || !hasMore || loading || typeof IntersectionObserver === "undefined") return;
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) void onLoadMore();
|
||||
}, { root: listRef.current, rootMargin: "120px 0px" });
|
||||
observer.observe(target);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, loading, onLoadMore, tab]);
|
||||
function reasoningModeLabel(mode: AgentReasoningMode) { return reasoningLabels[mode]; }
|
||||
|
||||
return (
|
||||
<div className="canvas-agent-skill-picker" data-canvas-no-zoom data-canvas-wheel-scroll>
|
||||
<div className="canvas-agent-skill-picker-header flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5 text-base font-semibold"><Sparkles className="size-5" />选择 Skills</div>
|
||||
<div className="mt-1.5 text-xs" style={{ color: theme.node.muted }}>本轮启用 {selectedCount}/8 个,发送时固定版本</div>
|
||||
</div>
|
||||
<div className="min-w-14 text-right">
|
||||
{tab === "installed" && source.length > 0 ? <button type="button" className="rounded-lg px-2 py-1 text-xs font-medium" style={{ color: theme.accent.primary }} onClick={toggleAll}>{allSelected ? "取消全选" : "全选"}</button> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="canvas-agent-skill-picker-tabs flex gap-1.5 rounded-xl p-1.5" style={{ background: theme.node.fill }}>
|
||||
<button type="button" className="flex-1 rounded-lg px-3 text-sm font-medium" style={{ background: tab === "installed" ? theme.toolbar.itemHover : "transparent", color: tab === "installed" ? theme.node.text : theme.node.muted }} onClick={() => setTab("installed")} aria-pressed={tab === "installed"}>已加入 <span className="ml-1 opacity-60">{installedSkills.length}</span></button>
|
||||
<button type="button" className="flex-1 rounded-lg px-3 text-sm font-medium" style={{ background: tab === "market" ? theme.toolbar.itemHover : "transparent", color: tab === "market" ? theme.node.text : theme.node.muted }} onClick={() => setTab("market")} aria-pressed={tab === "market"}>发现 Skills</button>
|
||||
</div>
|
||||
<Input allowClear prefix={<Search className="size-4" style={{ color: theme.node.muted }} />} value={search} onChange={(event) => onSearch(event.target.value)} placeholder={tab === "market" ? "搜索技能市场" : "搜索已加入 Skills"} aria-label="搜索 Skills" className="canvas-agent-skill-picker-search !rounded-xl" />
|
||||
<div ref={listRef} className="canvas-agent-skill-picker-list thin-scrollbar space-y-2 overflow-y-auto">
|
||||
{loading && source.length === 0 ? <div className="grid h-full place-items-center text-xs" style={{ color: theme.node.muted }}><span className="inline-flex items-center gap-2"><LoaderCircle className="size-4 animate-spin motion-reduce:animate-none" />正在读取技能…</span></div> : null}
|
||||
{source.map((skill) => <SkillPickerRow key={skill.skillId} skill={skill} theme={theme} selected={selectedSkillIds.includes(skill.skillId)} canSelect={selectedCount < 8 || selectedSkillIds.includes(skill.skillId)} onToggle={() => onToggle(skill.skillId)} onInstall={() => void onInstall(skill)} />)}
|
||||
{!loading && source.length === 0 ? <div className="grid h-full place-items-center px-8 text-center text-xs leading-5" style={{ color: theme.node.muted }}>{tab === "market" ? "没有匹配的公开 Skill,换个关键词试试" : "还没有加入 Skill,可前往“发现 Skills”添加"}</div> : null}
|
||||
{tab === "market" && hasMore ? <div ref={loadMoreRef} className="h-px" aria-hidden /> : null}
|
||||
</div>
|
||||
<div className="canvas-agent-skill-picker-footer flex items-center justify-between gap-3 border-t text-xs" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
|
||||
<span>{tab === "installed" ? `已选择 ${selectedCount} 个技能包` : `已加载 ${marketSkills.length} 个技能包`}</span>
|
||||
{tab === "market" && hasMore ? <button type="button" className="rounded-lg px-3 py-1.5 font-medium" style={{ background: theme.node.fill, color: theme.node.text }} disabled={loading} onClick={() => void onLoadMore()}>{loading ? "加载中…" : "加载更多"}</button> : <span>{tab === "installed" ? "最多 8 个" : "已加载全部"}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
function reasoningMenuItems(mode: AgentReasoningMode, onChange: (value: AgentReasoningMode) => void) {
|
||||
return (Object.keys(reasoningLabels) as AgentReasoningMode[]).map((value) => ({
|
||||
key: value,
|
||||
label: reasoningLabels[value],
|
||||
icon: value === mode ? <Check className="size-3.5" /> : undefined,
|
||||
onClick: () => onChange(value),
|
||||
}));
|
||||
}
|
||||
|
||||
function SkillPickerRow({ skill, theme, selected, canSelect, onToggle, onInstall }: { skill: Skill; theme: CanvasTheme; selected: boolean; canSelect: boolean; onToggle: () => void; onInstall: () => void }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const description = skill.description || "暂无技能说明";
|
||||
const hasMore = description.length > 112 || description.includes("\n");
|
||||
return (
|
||||
<div className={`canvas-agent-skill-picker-row flex min-w-0 items-start gap-3.5 rounded-xl border ${selected ? "is-selected" : ""}`} style={{ borderColor: selected ? theme.accent.primary : theme.node.stroke }} title={skill.description || skill.skillName}>
|
||||
{skill.isAdded ? (
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={selected}
|
||||
disabled={!selected && !canSelect}
|
||||
className="canvas-agent-skill-picker-check mt-0.5 grid shrink-0 place-items-center rounded-md border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/30 disabled:cursor-not-allowed disabled:opacity-35"
|
||||
style={{ borderColor: selected ? theme.accent.primary : theme.node.stroke, background: selected ? theme.accent.primary : "transparent", color: selected ? theme.toolbar.panel : theme.node.muted }}
|
||||
onClick={onToggle}
|
||||
aria-label={`${selected ? "停用" : "启用"} ${skill.skillName}`}
|
||||
>
|
||||
{selected ? <Check className="size-3.5" strokeWidth={3} /> : null}
|
||||
</button>
|
||||
) : <span className="canvas-agent-skill-picker-check mt-0.5 grid shrink-0 place-items-center rounded-md" style={{ background: theme.node.fill, color: theme.node.muted }}><Sparkles className="size-3.5" /></span>}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-3"><span className="min-w-0 flex-1 break-words text-sm font-semibold [overflow-wrap:anywhere]">{skill.skillName}</span>{skill.version ? <span className="shrink-0 text-xs" style={{ color: theme.node.muted }}>v{skill.version}</span> : null}</div>
|
||||
<div className={`mt-1 break-words text-xs leading-5 ${expanded ? "" : "line-clamp-2"}`} style={{ color: theme.node.muted }}>{description}</div>
|
||||
</div>
|
||||
{!skill.isAdded ? <Button size="small" type="text" className="!h-8 !shrink-0 !rounded-lg !px-2 !text-xs" icon={<Plus className="size-3.5" />} onClick={onInstall}>加入</Button> : null}
|
||||
{hasMore ? (
|
||||
<button type="button" className="canvas-agent-skill-picker-expand shrink-0 self-end text-[var(--fs-label)]" style={{ color: theme.accent.primary }} aria-expanded={expanded} onClick={() => setExpanded((value) => !value)}>
|
||||
{expanded ? "收起" : "查看全部"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalCard({ approval, theme, submitting, onReasonChange, onApprove, onReject }: { approval: ApprovalState; theme: CanvasTheme; submitting: boolean; onReasonChange: (value: string) => void; onApprove: () => void; onReject: () => void }) {
|
||||
function ApprovalCard({ approval, theme, submitting, onFocusNode, onReasonChange, onApprove, onReject }: { approval: ApprovalState; theme: CanvasTheme; submitting: boolean; onFocusNode?: (nodeId: string) => void; onReasonChange: (value: string) => void; onApprove: () => void; onReject: () => void }) {
|
||||
const [showReason, setShowReason] = useState(Boolean(approval.reason));
|
||||
const action = agentApprovalPresentation(approval.detail);
|
||||
return (
|
||||
@@ -1077,7 +1000,11 @@ function ApprovalCard({ approval, theme, submitting, onReasonChange, onApprove,
|
||||
<span className="canvas-agent-approval-badge">等待你的确认</span>
|
||||
</div>
|
||||
<p className="canvas-agent-approval-description" style={{ color: theme.node.muted }}>{action.description}</p>
|
||||
{action.items.length ? <div className="canvas-agent-approval-items" aria-label="涉及节点">{action.items.map((item, index) => <span key={`${item}-${index}`} title={item}>{item}</span>)}</div> : null}
|
||||
{action.items.length ? (
|
||||
<div className="canvas-agent-approval-items" aria-label="涉及节点">
|
||||
{action.items.map((item, index) => <ApprovalPreviewItemView key={`${item.operation}-${item.nodeId || item.nodeTitle || index}-${index}`} item={item} theme={theme} onFocusNode={onFocusNode} />)}
|
||||
</div>
|
||||
) : <div className="canvas-agent-approval-empty" style={{ color: theme.node.muted }}>无法确认具体目标,继续前请重新读取画布。</div>}
|
||||
<button type="button" className="canvas-agent-approval-reason-toggle" aria-expanded={showReason} onClick={() => setShowReason((value) => !value)} disabled={submitting}>
|
||||
{showReason ? "收起拒绝理由" : "填写拒绝理由(可选)"}
|
||||
</button>
|
||||
@@ -1093,6 +1020,37 @@ function ApprovalCard({ approval, theme, submitting, onReasonChange, onApprove,
|
||||
);
|
||||
}
|
||||
|
||||
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" ? "连线" : "生成";
|
||||
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}</>;
|
||||
return id && onFocusNode ? <button type="button" className="canvas-agent-approval-node canvas-agent-approval-node-button" onClick={() => onFocusNode(id)} title="定位到画布节点">{content}</button> : <span className={`canvas-agent-approval-node canvas-agent-approval-node-${role}`}>{content}</span>;
|
||||
};
|
||||
return (
|
||||
<article className="canvas-agent-approval-item">
|
||||
<div className="canvas-agent-approval-item-main">
|
||||
<span className={`canvas-agent-approval-operation canvas-agent-approval-operation-${item.operation}`}>{operationLabel}</span>
|
||||
{item.operation === "connect_nodes" ? (
|
||||
<div className="canvas-agent-approval-connection">
|
||||
{renderNode(item.nodeTitle, item.nodeId, item.nodeTypeLabel, "source")}
|
||||
<span className="canvas-agent-approval-arrow" aria-hidden="true">→</span>
|
||||
{renderNode(item.targetNodeTitle, item.targetNodeId, undefined, "target")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="canvas-agent-approval-node-summary">
|
||||
{renderNode(item.nodeTitle, item.nodeId, item.nodeTypeLabel, "node")}
|
||||
{item.resultTitle ? <><span className="canvas-agent-approval-change-arrow" aria-hidden="true">改为</span><span className="canvas-agent-approval-result-title">《{item.resultTitle}》</span></> : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.fields?.length ? <div className="canvas-agent-approval-field-list">修改:{item.fields.map((field) => <span key={field}>{field}</span>)}</div> : null}
|
||||
{item.details?.length ? <div className="canvas-agent-approval-detail-list">{item.details.map((detail) => <span key={detail}>{detail}</span>)}</div> : null}
|
||||
<div className="canvas-agent-approval-summary">{item.summary}</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function positiveNumber(value: string) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number > 0 ? number : undefined;
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
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 { ModelPicker } from "@/components/model-picker";
|
||||
import type { CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { AgentPermissionMode, AgentPersonality } from "@/services/api/agent";
|
||||
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" | "skills" | "mcp" | "context" | "budget";
|
||||
type SettingsSection = "home" | "profile" | "skills" | "mcp" | "context" | "budget";
|
||||
|
||||
type AgentSettingsProps = {
|
||||
theme: CanvasTheme;
|
||||
config: AiConfig;
|
||||
selectedModel: string;
|
||||
permissionMode: AgentPermissionMode;
|
||||
personality: AgentPersonality;
|
||||
onPersonalityChange: (value: AgentPersonality) => void;
|
||||
reasoningMode: AgentReasoningMode;
|
||||
onReasoningModeChange: (value: AgentReasoningMode) => void;
|
||||
profileView: AgentProfileView | null;
|
||||
profileLoading: boolean;
|
||||
profileSaving: boolean;
|
||||
profileError?: string;
|
||||
projectId?: string;
|
||||
canvasId: string;
|
||||
onReloadProfile: () => Promise<void>;
|
||||
onSaveProfile: (input: { scope: AgentProfileScope; projectId?: string; canvasId?: string; content: string; revision: number }) => Promise<AgentProfileView>;
|
||||
contextScope: AgentContextKey[];
|
||||
nodeCount: number;
|
||||
installedSkills: Skill[];
|
||||
@@ -52,6 +60,12 @@ const contextOptions: Array<{ value: AgentContextKey; label: string; description
|
||||
{ value: "canvas", label: "已保存画布摘要", description: "最多 80 个节点的标题与文本片段,不含媒体正文或未同步修改" },
|
||||
];
|
||||
|
||||
const reasoningOptions: Array<{ value: AgentReasoningMode; label: string; description: string }> = [
|
||||
{ value: "off", label: "关闭", description: "直接执行" },
|
||||
{ value: "auto", label: "自动", description: "按任务决定" },
|
||||
{ value: "deep", label: "深入", description: "复杂任务优先规划" },
|
||||
];
|
||||
|
||||
export function CanvasCloudAgentSettings(props: AgentSettingsProps) {
|
||||
const [section, setSection] = useState<SettingsSection>("home");
|
||||
const [skillTab, setSkillTab] = useState<"installed" | "market">("installed");
|
||||
@@ -68,6 +82,7 @@ export function CanvasCloudAgentSettings(props: AgentSettingsProps) {
|
||||
</header>
|
||||
|
||||
{section === "home" ? <SettingsHome props={props} theme={theme} onOpen={setSection} /> : null}
|
||||
{section === "profile" ? <ProfileWorkspace props={props} theme={theme} /> : 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}
|
||||
@@ -80,8 +95,9 @@ function SettingsHome({ props, theme, onOpen }: { props: AgentSettingsProps; the
|
||||
return (
|
||||
<div className="canvas-agent-settings-scroll thin-scrollbar min-h-0 flex-1 space-y-6 overflow-y-auto p-4">
|
||||
<section>
|
||||
<SettingLabel label="表达风格" hint="新运行生效" />
|
||||
<div className="grid grid-cols-2 gap-2">{([['务实','先说结果,少铺垫'],['简洁','只保留必要信息'],['详细','补充步骤与依据'],['温和','更耐心地解释']] as const).map(([value, description]) => <button key={value} type="button" aria-pressed={props.personality === value} onClick={() => props.onPersonalityChange(value)} className="rounded-xl p-3 text-left" style={{ background: props.personality === value ? theme.toolbar.itemHover : theme.node.fill, border: `1px solid ${props.personality === value ? theme.accent.primary : 'transparent'}` }}><div className="text-xs font-semibold">{value}</div><div className="mt-1 text-[10px] opacity-50">{description}</div></button>)}</div>
|
||||
<SettingLabel label="推理模式" hint="新运行生效" />
|
||||
<div className="grid grid-cols-3 gap-2">{reasoningOptions.map((option) => <button key={option.value} type="button" aria-pressed={props.reasoningMode === option.value} onClick={() => props.onReasoningModeChange(option.value)} className="rounded-xl p-3 text-left" style={{ background: props.reasoningMode === option.value ? theme.toolbar.itemHover : theme.node.fill, border: `1px solid ${props.reasoningMode === option.value ? theme.accent.primary : 'transparent'}` }}><div className="text-xs font-semibold">{option.label}</div><div className="mt-1 text-[10px] opacity-50">{option.description}</div></button>)}</div>
|
||||
<p className="mt-2 text-xs leading-5" style={{ color: theme.node.muted }}>推理只用于目标拆解、缺口判断和下一步工具选择,不会把内部思考过程展示给用户。</p>
|
||||
</section>
|
||||
<section>
|
||||
<SettingLabel label="对话模型" hint="新运行生效" />
|
||||
@@ -109,6 +125,7 @@ function SettingsHome({ props, theme, onOpen }: { props: AgentSettingsProps; the
|
||||
<SettingLabel label="能力与范围" />
|
||||
<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={<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")} />
|
||||
@@ -146,6 +163,104 @@ function SkillsWorkspace({ props, theme, tab, onTabChange }: { props: AgentSetti
|
||||
);
|
||||
}
|
||||
|
||||
type ProfileDraft = Pick<AgentProfileLayer, "content" | "revision" | "hash"> & { dirty: boolean };
|
||||
|
||||
function ProfileWorkspace({ props, theme }: { props: AgentSettingsProps; theme: CanvasTheme }) {
|
||||
const [scope, setScope] = useState<AgentProfileScope>("user");
|
||||
const [drafts, setDrafts] = useState<Record<string, ProfileDraft>>({});
|
||||
const [error, setError] = useState("");
|
||||
const layer = profileLayer(props.profileView, scope, props.projectId, props.canvasId);
|
||||
const draftKey = profileDraftKey(scope, props.projectId, props.canvasId);
|
||||
const serverDraft: ProfileDraft = { content: layer.content, revision: layer.revision, hash: layer.hash, dirty: false };
|
||||
const draft = drafts[draftKey] || serverDraft;
|
||||
const profileUnavailable = props.profileLoading || !props.profileView || Boolean(props.profileError);
|
||||
|
||||
useEffect(() => {
|
||||
setDrafts((current) => {
|
||||
const existing = current[draftKey];
|
||||
if (existing?.dirty) return current;
|
||||
if (existing && existing.revision === layer.revision && existing.hash === layer.hash && existing.content === layer.content) return current;
|
||||
return { ...current, [draftKey]: { content: layer.content, revision: layer.revision, hash: layer.hash, dirty: false } };
|
||||
});
|
||||
setError("");
|
||||
}, [draftKey, layer.content, layer.hash, layer.revision]);
|
||||
|
||||
const updateDraft = (content: string) => {
|
||||
setDrafts((current) => {
|
||||
const base = current[draftKey] || serverDraft;
|
||||
return { ...current, [draftKey]: { ...base, content, dirty: true } };
|
||||
});
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setError("");
|
||||
try {
|
||||
const result = await props.onSaveProfile({
|
||||
scope,
|
||||
projectId: scope === "project" ? props.projectId : undefined,
|
||||
canvasId: scope === "canvas" ? props.canvasId : undefined,
|
||||
content: draft.content,
|
||||
revision: draft.revision,
|
||||
});
|
||||
const savedLayer = profileLayer(result, scope, props.projectId, props.canvasId);
|
||||
setDrafts((current) => ({
|
||||
...current,
|
||||
[draftKey]: { content: savedLayer.content, revision: savedLayer.revision, hash: savedLayer.hash, dirty: false },
|
||||
}));
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : String(cause));
|
||||
}
|
||||
};
|
||||
|
||||
const disabled = profileUnavailable || props.profileSaving || (scope === "project" && !props.projectId);
|
||||
return (
|
||||
<div className="canvas-agent-settings-scroll thin-scrollbar min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<div className="rounded-xl p-4" style={{ background: theme.node.fill }}>
|
||||
<div className="text-sm font-semibold">长期偏好文档</div>
|
||||
<p className="mt-2 text-xs leading-5" style={{ color: theme.node.muted }}>这里定义 Agent 的长期工作习惯、语气和输出结构。它不会增加工具、节点、预算或审批权限;安全契约始终由服务端代码控制。</p>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-1 rounded-xl p-1" style={{ background: theme.node.fill }}>
|
||||
<TabButton theme={theme} active={scope === "user"} label="用户" onClick={() => setScope("user")} />
|
||||
<TabButton theme={theme} active={scope === "project"} label="项目" onClick={() => setScope("project")} />
|
||||
<TabButton theme={theme} active={scope === "canvas"} label="画布" onClick={() => setScope("canvas")} />
|
||||
</div>
|
||||
{scope === "project" && !props.projectId ? <p className="mt-3 text-xs leading-5" style={{ color: theme.node.muted }}>当前画布尚未关联项目,项目偏好暂不可用。</p> : null}
|
||||
{props.profileLoading ? <p className="mt-4 text-xs" style={{ color: theme.node.muted }}>正在读取偏好快照…</p> : null}
|
||||
{props.profileError ? <ProfileError message={props.profileError} onRetry={props.onReloadProfile} theme={theme} /> : null}
|
||||
<label className="mt-4 block">
|
||||
<span className="flex items-center justify-between text-xs font-semibold"><span>{scopeLabel(scope)}</span><span className="text-[10px] font-normal opacity-40">修订 {draft.revision}{draft.dirty ? " · 未保存" : ""}</span></span>
|
||||
<Input.TextArea value={draft.content} onChange={(event) => updateDraft(event.target.value)} disabled={disabled} maxLength={12000} showCount autoSize={{ minRows: 10, maxRows: 20 }} placeholder="例如:每次先给结论,再列出将改动的镜头;提示词同时给中文和英文。" className="mt-2 !rounded-xl" style={{ background: theme.node.fill }} />
|
||||
</label>
|
||||
<div className="mt-3 flex items-center justify-between gap-3">
|
||||
<span className="text-[10px] leading-4 opacity-45">保存后只影响下一次新运行。正在运行的 Agent 会固定原快照。</span>
|
||||
<Button type="primary" onClick={() => void save()} loading={props.profileSaving} disabled={disabled || !draft.dirty}>保存</Button>
|
||||
</div>
|
||||
{error ? <div className="mt-3 rounded-lg px-3 py-2 text-xs" style={{ background: "#d66b6b1f", color: "#d66b6b" }}>{error}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function profileDraftKey(scope: AgentProfileScope, projectId: string | undefined, canvasId: string) {
|
||||
if (scope === "project") return `project:${projectId || "unbound"}`;
|
||||
if (scope === "canvas") return `canvas:${canvasId}`;
|
||||
return "user";
|
||||
}
|
||||
|
||||
function profileLayer(view: AgentProfileView | null, scope: AgentProfileScope, projectId: string | undefined, canvasId: string): AgentProfileLayer {
|
||||
return view?.layers.find((item) => item.scope === scope) || { scope, projectId: scope === "project" ? projectId : undefined, canvasId: scope === "canvas" ? canvasId : undefined, content: "", revision: 0, hash: "" };
|
||||
}
|
||||
|
||||
function profileSummary(view: AgentProfileView | null) {
|
||||
const count = view?.layers.filter((layer) => layer.content.trim()).length || 0;
|
||||
return count ? `${count} 层已配置 · 仅影响行为偏好` : "未配置 · 仅影响行为偏好";
|
||||
}
|
||||
|
||||
function scopeLabel(scope: AgentProfileScope) { return scope === "user" ? "用户偏好" : scope === "project" ? "项目偏好" : "当前画布偏好"; }
|
||||
|
||||
function ProfileError({ message, onRetry, theme }: { message: string; onRetry: () => Promise<void>; theme: CanvasTheme }) {
|
||||
return <div className="mt-3 rounded-lg px-3 py-2 text-xs" style={{ background: "#d6a24a1f", color: theme.node.text }}><div>{message}</div><button type="button" className="mt-2 underline" onClick={() => void onRetry()}>重新读取</button></div>;
|
||||
}
|
||||
|
||||
function SkillRow({ skill, theme, selected, installed, selectable, onToggle, onInstall }: { skill: Skill; theme: CanvasTheme; selected: boolean; installed: boolean; selectable: boolean; onToggle: () => void; onInstall: () => void }) {
|
||||
return (
|
||||
<div className="canvas-agent-skill-row flex min-w-0 items-start gap-3 rounded-xl p-3 transition-colors">
|
||||
@@ -187,8 +302,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 === "skills" ? "Skills" : section === "mcp" ? "MCP 与工具" : section === "context" ? "上下文" : "预算"; }
|
||||
function sectionSubtitle(section: SettingsSection) { return section === "skills" ? "搜索、安装并选择本轮技能" : section === "mcp" ? "云端工具与连接状态" : section === "context" ? "控制 Agent 能读取的范围" : "控制本轮积分与生成消耗"; }
|
||||
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 能读取的范围" : "控制本轮积分与生成消耗"; }
|
||||
|
||||
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` }; }
|
||||
|
||||
@@ -131,6 +131,18 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.agent-composer-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-composer-submit {
|
||||
min-width: 0;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.agent-composer-send-hint {
|
||||
color: color-mix(in srgb, currentColor 52%, transparent);
|
||||
font-size: var(--fs-tiny);
|
||||
@@ -138,7 +150,8 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@container (max-width: 420px) {
|
||||
/* 面板宽度包含内边距;默认 448px 面板的输入区已不足以同时容纳控制区和快捷键提示。 */
|
||||
@container (max-width: 520px) {
|
||||
.agent-composer-send-hint {
|
||||
display: none;
|
||||
}
|
||||
@@ -243,24 +256,137 @@
|
||||
}
|
||||
|
||||
.canvas-agent-approval-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: min(360px, 42vh);
|
||||
margin-top: 12px;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-items span {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--foreground) 7%, transparent);
|
||||
padding: 4px 7px;
|
||||
font-size: 11px;
|
||||
.canvas-agent-approval-item {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 9%, transparent);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--foreground) 3%, transparent);
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-item-main,
|
||||
.canvas-agent-approval-node-summary,
|
||||
.canvas-agent-approval-connection {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-operation {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 5px;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-operation-add_node { background: #dcfce7; color: #166534; }
|
||||
.canvas-agent-approval-operation-update_node { background: #dbeafe; color: #1d4ed8; }
|
||||
.canvas-agent-approval-operation-connect_nodes { background: #fef3c7; color: #92400e; }
|
||||
.canvas-agent-approval-operation-generate_media { background: #f3e8ff; color: #7e22ce; }
|
||||
|
||||
.canvas-agent-approval-node,
|
||||
.canvas-agent-approval-node-button {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
color: var(--foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-node-button {
|
||||
cursor: pointer;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-node-button:hover {
|
||||
background: color-mix(in srgb, var(--agent-approval-blue) 10%, transparent);
|
||||
color: var(--agent-approval-blue);
|
||||
}
|
||||
|
||||
.canvas-agent-approval-node-title,
|
||||
.canvas-agent-approval-result-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-node-type {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
padding: 1px 5px;
|
||||
color: color-mix(in srgb, var(--foreground) 58%, transparent);
|
||||
font-size: 10px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-arrow,
|
||||
.canvas-agent-approval-change-arrow {
|
||||
flex: 0 0 auto;
|
||||
color: color-mix(in srgb, var(--foreground) 48%, transparent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-result-title {
|
||||
color: var(--agent-approval-blue);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-field-list,
|
||||
.canvas-agent-approval-detail-list,
|
||||
.canvas-agent-approval-summary,
|
||||
.canvas-agent-approval-empty {
|
||||
color: color-mix(in srgb, var(--foreground) 62%, transparent);
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-field-list,
|
||||
.canvas-agent-approval-detail-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-field-list span,
|
||||
.canvas-agent-approval-detail-list span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--foreground) 7%, transparent);
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-summary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-empty {
|
||||
margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, #f59e0b 10%, transparent);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.canvas-agent-approval-reason-toggle {
|
||||
display: inline-flex;
|
||||
margin-top: 10px;
|
||||
@@ -356,82 +482,486 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-popover .ant-popover-inner {
|
||||
padding: 0;
|
||||
.canvas-agent-skill-library-modal .ant-modal-container {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
border-radius: var(--r-2xl);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.18), var(--elevation-hairline) !important;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker {
|
||||
.canvas-agent-skill-library-modal .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-modal .ant-modal-close {
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--r-md);
|
||||
color: var(--foreground);
|
||||
background: color-mix(in srgb, var(--foreground) 7%, transparent);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-modal .ant-modal-close:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 12%, transparent);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-shell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
height: min(780px, calc(100dvh - 36px));
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
background:
|
||||
radial-gradient(circle at 0 0, color-mix(in srgb, var(--workspace-accent) 7%, transparent), transparent 34%),
|
||||
var(--cn-spatial-elevated, var(--background));
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-header {
|
||||
display: flex;
|
||||
width: min(520px, calc(100vw - 24px));
|
||||
max-height: calc(100vh - 24px);
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 18px 64px 14px 20px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-tabs {
|
||||
height: 48px;
|
||||
margin-top: 10px;
|
||||
.canvas-agent-skill-library-header p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-tabs button {
|
||||
transition: background-color 140ms ease, color 140ms ease;
|
||||
.canvas-agent-skill-library-count {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 9%, transparent);
|
||||
border-radius: var(--r-lg);
|
||||
background: color-mix(in srgb, var(--foreground) 4%, transparent);
|
||||
padding: 7px 10px;
|
||||
font-size: var(--fs-label);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-search {
|
||||
height: 44px;
|
||||
margin-top: 10px;
|
||||
.canvas-agent-skill-library-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 430px);
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 20px 12px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-list {
|
||||
.canvas-agent-skill-library-tabs {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tab {
|
||||
display: inline-flex;
|
||||
min-height: 38px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
border-radius: var(--r-md);
|
||||
padding: 0 12px;
|
||||
color: color-mix(in srgb, var(--foreground) 56%, transparent);
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 560;
|
||||
transition: background-color var(--motion-dur-fast) var(--motion-ease-out), color var(--motion-dur-fast) var(--motion-ease-out);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tab span {
|
||||
min-width: 20px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, currentColor 9%, transparent);
|
||||
padding: 1px 6px;
|
||||
font-size: var(--fs-tiny);
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tab:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 4%, transparent);
|
||||
color: color-mix(in srgb, var(--foreground) 76%, transparent);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tab.is-active {
|
||||
background: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
color: color-mix(in srgb, var(--foreground) 78%, transparent);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-search {
|
||||
height: 40px;
|
||||
border-radius: var(--r-lg);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-categories {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid color-mix(in srgb, var(--foreground) 7%, transparent);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 7%, transparent);
|
||||
padding: 10px 20px;
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-categories::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-category {
|
||||
min-height: 34px;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 9%, transparent);
|
||||
border-radius: var(--r-md);
|
||||
padding: 0 13px;
|
||||
color: color-mix(in srgb, var(--foreground) 58%, transparent);
|
||||
font-size: var(--fs-caption);
|
||||
transition: border-color var(--motion-dur-fast) var(--motion-ease-out), background-color var(--motion-dur-fast) var(--motion-ease-out), color var(--motion-dur-fast) var(--motion-ease-out);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-category:hover {
|
||||
border-color: color-mix(in srgb, var(--foreground) 20%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-category.is-active {
|
||||
border-color: color-mix(in srgb, var(--workspace-accent) 34%, var(--border));
|
||||
background: color-mix(in srgb, var(--workspace-accent) 11%, transparent);
|
||||
color: var(--workspace-accent);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-list {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
max-height: min(360px, 44vh);
|
||||
margin-top: 10px;
|
||||
padding-right: 5px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-auto-rows: max-content;
|
||||
align-content: start;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-row {
|
||||
padding: 8px 10px;
|
||||
transition: background-color 140ms ease, border-color 140ms ease;
|
||||
.canvas-agent-skill-library-state {
|
||||
display: grid;
|
||||
min-height: 260px;
|
||||
grid-column: 1 / -1;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 10px;
|
||||
padding: 28px;
|
||||
font-size: var(--fs-caption);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-row:hover {
|
||||
background: var(--library-surface-hover);
|
||||
.canvas-agent-skill-card {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 136px;
|
||||
grid-template-columns: 164px minmax(0, 1fr) auto;
|
||||
gap: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-xl);
|
||||
background: color-mix(in srgb, var(--foreground) 3%, transparent);
|
||||
padding: 11px;
|
||||
transition: border-color var(--motion-dur-fast) var(--motion-ease-out), background-color var(--motion-dur-fast) var(--motion-ease-out), transform var(--motion-dur-fast) var(--motion-ease-out);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-row.is-selected {
|
||||
.canvas-agent-skill-card:hover {
|
||||
border-color: color-mix(in srgb, var(--foreground) 22%, transparent) !important;
|
||||
background: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card.is-selected {
|
||||
background: color-mix(in srgb, var(--workspace-accent) 7%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--workspace-accent) 24%, transparent);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-check {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
.canvas-agent-skill-card-cover {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border-radius: var(--r-lg);
|
||||
aspect-ratio: 16 / 10;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-footer {
|
||||
min-height: 36px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
.canvas-agent-skill-card-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform var(--motion-dur-base) var(--motion-ease-out);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-expand {
|
||||
border-radius: 5px;
|
||||
padding: 2px 4px;
|
||||
.canvas-agent-skill-card:hover .canvas-agent-skill-card-cover img {
|
||||
transform: scale(1.025);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-expand:hover {
|
||||
background: color-mix(in srgb, var(--workspace-accent) 10%, transparent);
|
||||
.canvas-agent-skill-card-placeholder {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
place-items: center;
|
||||
color: color-mix(in srgb, var(--workspace-accent) 64%, var(--foreground));
|
||||
background:
|
||||
radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--workspace-accent) 28%, transparent), transparent 46%),
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--foreground) 8%, transparent), color-mix(in srgb, var(--foreground) 2%, transparent));
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-category {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
max-width: calc(100% - 14px);
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, white 18%, transparent);
|
||||
border-radius: var(--r-md);
|
||||
background: color-mix(in srgb, black 62%, transparent);
|
||||
padding: 2px 7px;
|
||||
color: white;
|
||||
font-size: var(--fs-tiny);
|
||||
line-height: 19px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-body {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
padding: 4px 0 2px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-title-row {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-title-row h3 {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
color: var(--foreground);
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
line-height: 22px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-version {
|
||||
flex: 0 0 auto;
|
||||
color: color-mix(in srgb, var(--foreground) 42%, transparent);
|
||||
font-size: var(--fs-tiny);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-body > p {
|
||||
display: -webkit-box;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
margin: 7px 0 10px;
|
||||
color: color-mix(in srgb, var(--foreground) 54%, transparent);
|
||||
font-size: var(--fs-label);
|
||||
line-height: 19px;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: auto;
|
||||
font-size: var(--fs-label);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-action {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-action .ant-btn {
|
||||
min-width: 76px;
|
||||
border-radius: var(--r-md);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-action .ant-btn.is-selected {
|
||||
border-color: color-mix(in srgb, var(--workspace-accent) 32%, var(--border));
|
||||
background: color-mix(in srgb, var(--workspace-accent) 13%, transparent);
|
||||
color: var(--workspace-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-footer {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-top: 1px solid;
|
||||
padding: 8px 20px;
|
||||
font-size: var(--fs-label);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tab:focus-visible,
|
||||
.canvas-agent-skill-category:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.canvas-agent-skill-library-list {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.canvas-agent-skill-library-shell {
|
||||
height: calc(100dvh - 24px);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-header {
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 16px 56px 12px 14px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-header p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-count {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-toolbar {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
padding: 0 14px 10px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tabs {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-categories {
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-list {
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-footer {
|
||||
min-height: 44px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.canvas-agent-skill-library-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-count {
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
right: 56px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-tab {
|
||||
min-height: 34px;
|
||||
padding: 0 9px;
|
||||
font-size: var(--fs-label);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card {
|
||||
min-height: 122px;
|
||||
grid-template-columns: 104px minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
gap: 8px 10px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-cover {
|
||||
grid-row: 1 / 3;
|
||||
align-self: stretch;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-body {
|
||||
padding: 1px 0 0;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-body > p {
|
||||
margin: 4px 0 6px;
|
||||
font-size: var(--fs-tiny);
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-meta {
|
||||
font-size: var(--fs-tiny);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-action {
|
||||
justify-content: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card-action .ant-btn {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-footer > span:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 560px) {
|
||||
.canvas-agent-skill-picker {
|
||||
max-height: calc(100vh - 16px);
|
||||
.canvas-agent-skill-library-shell {
|
||||
height: calc(100dvh - 16px);
|
||||
}
|
||||
|
||||
.canvas-agent-skill-picker-list {
|
||||
max-height: min(280px, 38vh);
|
||||
.canvas-agent-skill-library-header {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-library-categories {
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.canvas-agent-skill-card,
|
||||
.canvas-agent-skill-card-cover img,
|
||||
.canvas-agent-skill-category,
|
||||
.canvas-agent-skill-library-tab {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.canvas-agent-skill-card:hover,
|
||||
.canvas-agent-skill-card:hover .canvas-agent-skill-card-cover img {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user