From 0122abeddb3ebe00c47debaf50be59c07d6f0d35 Mon Sep 17 00:00:00 2001 From: evanlowe <62918515+evanlowe@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:25:17 +0800 Subject: [PATCH 01/10] feat(studio): redesign agent builder workspace --- frontend/README.md | 30 +- frontend/src/App.tsx | 219 +- frontend/src/adk/client.ts | 62 + frontend/src/create/AgentBuildCanvas.css | 700 ++-- frontend/src/create/AgentBuildCanvas.tsx | 726 ++-- frontend/src/create/AgentBuilderChatPanel.css | 156 + frontend/src/create/AgentBuilderChatPanel.tsx | 153 + frontend/src/create/CustomCreate.css | 2017 +++++++++-- frontend/src/create/CustomCreate.tsx | 3083 +++++++++-------- frontend/src/create/IntelligentCreate.tsx | 4 +- .../create/agentBuilderConversationStorage.ts | 170 + frontend/src/create/veadkCatalog.ts | 6 +- frontend/src/styles.css | 16 +- frontend/src/ui/AddAgentMenu.css | 404 ++- frontend/src/ui/AddAgentMenu.tsx | 191 +- frontend/src/ui/CapabilityIcons.tsx | 124 + frontend/src/ui/ConversationActions.tsx | 74 + frontend/src/ui/CreateAgentHeader.css | 198 ++ frontend/src/ui/CreateAgentHeader.tsx | 157 + frontend/src/ui/Sidebar.tsx | 8 + .../src/ui/builtin-tools/builtin-tools.css | 4 + frontend/src/ui/builtin-tools/registry.ts | 9 + frontend/src/ui/icons/CreateAgentIcons.tsx | 299 ++ frontend/tests/agentBuildCanvas.test.mjs | 517 ++- .../agentBuilderConversationStorage.test.mjs | 111 + frontend/tests/builtinToolStatus.test.mjs | 10 + frontend/tests/cloudEnvironment.test.mjs | 5 +- frontend/tests/codePackageDeploy.test.mjs | 197 +- .../tests/debugErrorPresentation.test.mjs | 2 +- frontend/tests/deploymentConfigUi.test.mjs | 5 +- frontend/tests/deploymentEnv.test.mjs | 24 +- .../tests/generatedAgentConversation.test.mjs | 68 + frontend/tests/generatedAgentPlanner.test.mjs | 17 +- frontend/tests/generatedAgentTrace.test.mjs | 16 +- .../tests/intelligentDevelopment.test.mjs | 8 +- frontend/tests/mainShellLayout.test.mjs | 20 +- frontend/tests/markdownPromptEditor.test.mjs | 465 ++- frontend/tests/migrationWorkspace.test.mjs | 14 +- frontend/tests/modelSelector.test.mjs | 246 +- frontend/tests/siteBranding.test.mjs | 2 +- frontend/tests/timeout.test.mjs | 5 + .../cli/test_generated_agent_conversation.py | 221 ++ veadk/cli/cli_frontend.py | 70 + veadk/cli/generated_agent_conversation.py | 308 ++ 44 files changed, 8311 insertions(+), 2830 deletions(-) create mode 100644 frontend/src/create/AgentBuilderChatPanel.css create mode 100644 frontend/src/create/AgentBuilderChatPanel.tsx create mode 100644 frontend/src/create/agentBuilderConversationStorage.ts create mode 100644 frontend/src/ui/ConversationActions.tsx create mode 100644 frontend/src/ui/CreateAgentHeader.css create mode 100644 frontend/src/ui/CreateAgentHeader.tsx create mode 100644 frontend/src/ui/icons/CreateAgentIcons.tsx create mode 100644 frontend/tests/agentBuilderConversationStorage.test.mjs create mode 100644 frontend/tests/generatedAgentConversation.test.mjs create mode 100644 tests/cli/test_generated_agent_conversation.py create mode 100644 veadk/cli/generated_agent_conversation.py diff --git a/frontend/README.md b/frontend/README.md index b17c6901b..f5411c5b2 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -148,22 +148,22 @@ server that `veadk frontend` launches — no separate backend. instead of credentials. Long descriptions, names, component summaries, IDs, and environment values stay inside the scrollable panel. - **Custom-agent workbench**: configure an agent with a rich Markdown - system-prompt editor (including heading and list shortcuts), choose Harness - Sidecar optimizations, then debug with expandable, copyable runner error - details and per-result Trace inspection. The workbench order is `架构` → - `优化` → `调试` → `环境` → `发布`. On the optimization page, `自定义` - appears first and starts with no components, while `运维场景` applies the - `ops` component combination. Component checkboxes remain editable and an empty - selection keeps Sidecar disabled. The Environment step can optionally add the - official Lark CLI, GitHub CLI, and Pandoc to the cloud runtime. Selecting a - tool generates an inspectable provider-specific Dockerfile with pinned - releases, amd64/arm64 assets, and SHA-256 verification. Advanced configuration - can edit that Dockerfile directly or restore the generated version; selecting + system-prompt editor (including heading and list shortcuts), then debug with + expandable, copyable runner error details, per-result Trace inspection, and + review. Its intelligent-creation drawer keeps one expiring, user-scoped + multi-turn conversation and renders normal model and tool events through the + shared conversation blocks. The model asks for missing scenario details and + may call `generate_agent` only when the requirement is sufficiently complete; + the canvas changes only after that tool returns a validated Agent draft. + The lifecycle follows Architecture, Debug, Environment, and Publish. + The Environment step can optionally add the official Lark CLI, GitHub CLI, + and Pandoc to the cloud runtime. Selecting a tool generates an inspectable + provider-specific Dockerfile with pinned releases, amd64/arm64 assets, and + SHA-256 verification. Advanced configuration can edit that Dockerfile + directly or restore the provider-specific generated version; selecting no tool and leaving the Dockerfile unchanged keeps AgentKit's default image - build. Credentials are never written into the generated Dockerfile. Published - Agent details display the saved Sidecar scenario and selected components as - read-only information. In-progress drafts are stored only in the current - browser and scoped + build. Credentials are never written into the generated Dockerfile. + In-progress drafts are stored only in the current browser and scoped to the signed-in user. MCP tokens are converted to Runtime environment variables: generated source retains only the `${ENV_NAME}` reference, while YAML and browser drafts preserve the corresponding environment value. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 910fec59f..8621084d3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,8 +10,6 @@ import { import { Share } from "@openai/apps-sdk-ui/components/Icon"; import { ArrowLeft, - Check, - Copy, CornerDownRight, Loader2, } from "lucide-react"; @@ -90,6 +88,9 @@ import { type TurnActivityDetail, } from "./blocks"; import { Sidebar, type SidebarPage } from "./ui/Sidebar"; +import { + ConversationCopyButton as CopyButton, +} from "./ui/ConversationActions"; import { AgentInfoPanel } from "./ui/AgentTopology"; import type { SkillCenterWorkspaceLaunch } from "./ui/SkillCenter"; import { LibraryView, type LibraryTab } from "./ui/LibraryView"; @@ -127,7 +128,7 @@ import { Blocks, ThinkingPlaceholder } from "./ui/Blocks"; import { Composer } from "./ui/Composer"; import { InvocationChips } from "./ui/InvocationChips"; import { MediaGroup } from "./ui/Media"; -import { StackCards } from "./ui/AddAgentMenu"; +import { AddAgentMenu } from "./ui/AddAgentMenu"; import { IntelligentCreate, type IntelligentPreparationStage, @@ -547,66 +548,6 @@ function telemetrySandboxStatus( } } -/** Hand-drawn "from zero" mark: a blank Agent canvas ready to create. */ -function ScratchIcon({ className }: { className?: string }) { - return ( - - ); -} - -/** Hand-drawn code package mark: an archive with source inside. */ -function PackageIcon({ className }: { className?: string }) { - return ( - - ); -} - -/** Hand-drawn migration mark: an existing project moving into a new runtime. */ -function MigrationIcon({ className }: { className?: string }) { - return ( - - ); -} - /** Hand-drawn "tracing / observability" icon (stacked spans). */ function TraceIcon() { return ( @@ -769,29 +710,6 @@ function withAuthResponseUri(authConfig: unknown, callbackUrl: string): unknown return cfg; } -function CopyButton({ text }: { text: string }) { - const [copied, setCopied] = useState(false); - return ( - - ); -} - function ShareMessageButton({ onClick, }: { @@ -1945,6 +1863,7 @@ export default function App() { const [addAgent, setAddAgent] = useState(false); // The "添加 Agent" chooser (two cards: AgentKit / 从 0 快速创建). const [addMenu, setAddMenu] = useState(false); + const [intelligentInitialGoal, setIntelligentInitialGoal] = useState(""); // A draft imported from YAML, used to pre-fill the custom wizard once. const [importedDraft, setImportedDraft] = useState(null); const [customCreateMode, setCustomCreateMode] = @@ -5874,6 +5793,7 @@ export default function App() { sessions={sessions} currentSessionId={sessionId} activePage={sidebarActivePage} + collapseRequested={showAddMenu || Boolean(visibleCreateView)} streamingSids={streamingSids} evaluatingSids={evaluatingSids} sandboxHistory={sandboxSession && !sandboxSession.intelligentDevelopment @@ -6584,76 +6504,50 @@ export default function App() { }} /> ) : showAddMenu ? ( - { - setAddMenu(false); - setImportedDraft(null); - setCustomCreateMode("custom"); - setRuntimeUpdateTarget(null); - setFocusedDeploymentTaskId(""); - setFocusedWorkspaceAgentId(""); - setEditingDraftId(`draft-${Date.now().toString(36)}`); - editingDraftBaselineRef.current = null; - setCreateView("custom"); - }, - }, - { - key: "intelligent", - icon: ScratchIcon, - title: "智能模式", - desc: intelligentCapabilitiesError - || intelligentCapabilities?.reason - || "描述目标,按你的意图构建、调试并验证 Agent。", - status: intelligentCapabilitiesLoading - ? "能力检查中" - : intelligentCapabilities?.enabled - ? undefined - : "暂不可用", - disabled: - intelligentCapabilitiesLoading || - intelligentCapabilities?.enabled !== true, - onClick: () => { - setAddMenu(false); - setImportedDraft(null); - setRuntimeUpdateTarget(null); - setFocusedDeploymentTaskId(""); - setFocusedWorkspaceAgentId(""); - setEditingDraftId(""); - editingDraftBaselineRef.current = null; - setCreateView("intelligent"); - }, - }, - { - key: "package", - icon: PackageIcon, - title: "从代码包添加和部署", - desc: "上传 Agent 项目压缩包,查看代码并直接部署到 AgentKit Runtime。", - onClick: () => { - setAddMenu(false); - setImportedDraft(null); - setCreateView("package"); - }, - }, - { - key: "migration", - icon: MigrationIcon, - title: "从存量迁移", - desc: "从您的 LangChain / Dify 等存量项目迁移至 AgentKit Runtime", - onClick: () => { - setAddMenu(false); - setImportedDraft(null); - setCreateView("migration"); - }, - }, - ]} + setAddMenu(false)} + onDescribe={(goal) => { + setAddMenu(false); + setImportedDraft(null); + setRuntimeUpdateTarget(null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setEditingDraftId(""); + editingDraftBaselineRef.current = null; + setIntelligentInitialGoal(goal); + setEditingDraftId(`draft-${Date.now().toString(36)}`); + setCustomCreateMode("custom"); + setCreateView("custom"); + }} + onTemplate={() => { + setAddMenu(false); + setIntelligentInitialGoal(""); + setImportedDraft(null); + setCustomCreateMode("custom"); + setRuntimeUpdateTarget(null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setEditingDraftId(`draft-${Date.now().toString(36)}`); + editingDraftBaselineRef.current = null; + setCreateView("custom"); + }} + onPackage={() => { + setAddMenu(false); + setIntelligentInitialGoal(""); + setImportedDraft(null); + setCreateView("package"); + }} + onMigration={() => { + setAddMenu(false); + setIntelligentInitialGoal(""); + setImportedDraft(null); + setCreateView("migration"); + }} /> ) : searchView ? ( ) : visibleCreateView === "intelligent" ? ( { cancelIntelligentPreparation(); + setIntelligentInitialGoal(""); setCreateView(null); setAddMenu(true); }} @@ -6810,8 +6706,19 @@ export default function App() { { + setIntelligentInitialGoal(""); setCreateView(null); setAddMenu(true); }} diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index 25aad9e19..c85593af8 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -3493,6 +3493,21 @@ export interface GeneratedAgentDraftResult { unresolvedItems: string[]; } +export interface GeneratedAgentConversation { + conversationId: string; + expiresAt: number; +} + +export interface GeneratedAgentDraftEvent extends GeneratedAgentDraftResult { + type: "agent_draft"; +} + +export type GeneratedAgentConversationEvent = + | AdkEvent + | GeneratedAgentDraftEvent + | { type: "done" } + | { type: "error"; message: string }; + const GENERATED_AGENT_DRAFT_TIMEOUT_MS = 190_000; export async function generateAgentDraftFromRequirement( @@ -3514,6 +3529,53 @@ export async function generateAgentDraftFromRequirement( return parseJsonResponse(res, "生成 Agent 配置失败"); } +export async function createGeneratedAgentConversation( + signal?: AbortSignal, +): Promise { + const res = await apiFetch("/web/generated-agent-conversations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + signal, + }); + if (!res.ok) { + throw new Error(await httpErrorMessage(res, "创建智能创建会话失败")); + } + return parseJsonResponse( + res, + "创建智能创建会话失败", + ); +} + +export async function* runGeneratedAgentConversationSSE({ + conversationId, + message, + signal, +}: { + conversationId: string; + message: string; + signal?: AbortSignal; +}): AsyncGenerator { + const res = await apiFetch( + `/web/generated-agent-conversations/${encodeURIComponent(conversationId)}/run_sse`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message }), + signal, + }, + {}, + 0, + ); + if (!res.ok) { + throw new Error(await httpErrorMessage(res, "智能创建对话失败")); + } + for await (const event of parseSSE(res)) { + if (!event || typeof event !== "object") continue; + yield event as GeneratedAgentConversationEvent; + } +} + export async function createGeneratedAgentTestRun( draft: AgentDraft, runtime?: { runtimeId: string; region: string }, diff --git a/frontend/src/create/AgentBuildCanvas.css b/frontend/src/create/AgentBuildCanvas.css index 42942d522..7c4fa3872 100644 --- a/frontend/src/create/AgentBuildCanvas.css +++ b/frontend/src/create/AgentBuildCanvas.css @@ -11,13 +11,13 @@ flex-direction: column; overflow: hidden; border-right: 0; - background: hsl(var(--background)); + background: #f0f0f0; } .abc-canvas { flex: 1; min-height: 0; - background: hsl(var(--background)); + background: #f0f0f0; } .abc-canvas .react-flow__pane { @@ -29,302 +29,458 @@ } .abc-node { - --abc-type-tone: 220 9% 24%; - --abc-type-soft: 220 10% 97%; - --abc-type-border: 220 9% 78%; position: relative; - width: 220px; - height: 88px; - display: grid; - grid-template-columns: 38px minmax(0, 1fr); - align-items: center; - gap: 9px; - padding: 12px 14px; - border: 0.5px solid hsl(var(--abc-type-border) / 0.62); - border-radius: 13px; - background: hsl(var(--panel)); - box-shadow: 0 10px 30px hsl(var(--foreground) / 0.055); + width: 214px; + height: 168px; + border: 0; + border-radius: 12px; + background: transparent; + box-shadow: none !important; color: hsl(var(--foreground)); - transition: border-color 150ms ease, box-shadow 150ms ease, transform 150ms ease; -} - -.abc-node:hover { - border-color: hsl(var(--abc-type-tone) / 0.36); - box-shadow: 0 13px 34px hsl(var(--foreground) / 0.08); } -.abc-node.is-selected { - border-color: hsl(var(--abc-type-tone) / 0.62); - box-shadow: - 0 0 0 1px hsl(var(--abc-type-tone) / 0.06), - 0 14px 38px hsl(var(--foreground) / 0.09); +.abc-node:not(.is-llm) { + height: 138px; } -.abc-node.is-llm { - grid-template-columns: minmax(0, 1fr); - background: hsl(var(--panel)); +.abc-root.is-readonly .abc-node.is-llm { + height: 133px; } -.abc-node.is-a2a { - --abc-type-tone: 213 18% 38%; - --abc-type-soft: 214 20% 94%; - --abc-type-border: 213 15% 72%; - background: - linear-gradient(145deg, hsl(var(--abc-type-soft)), hsl(var(--panel)) 58%); +.abc-root.is-readonly .abc-node:not(.is-llm) { + height: 104px; } .abc-canvas .react-flow__node-group { padding: 0; border: 0; - border-radius: 18px; + border-radius: 14px; background: transparent; + box-shadow: none !important; } .abc-group { - --abc-type-tone: 213 40% 40%; - --abc-type-soft: 214 45% 96%; - --abc-type-border: 213 32% 62%; + --abc-type-tone: 220 9% 38%; + --abc-type-soft: 220 10% 96%; + --abc-group-tone: #e8ebf9; + --abc-group-summary-height: 84px; position: relative; + display: flow-root; width: 100%; height: 100%; box-sizing: border-box; - overflow: hidden; - border: 0.5px solid hsl(var(--abc-type-border) / 0.5); - border-radius: 18px; - background: - linear-gradient(180deg, hsl(var(--abc-type-soft) / 0.88), transparent 88px), - hsl(var(--panel) / 0.72); - box-shadow: 0 14px 42px hsl(var(--foreground) / 0.055); - transition: border-color 150ms ease, box-shadow 150ms ease; + overflow: visible; + border: 0; + border-radius: 14px; + background: transparent; + box-shadow: none; } -.abc-group.is-selected { - border-color: hsl(var(--abc-type-tone) / 0.62); - box-shadow: - 0 0 0 1px hsl(var(--abc-type-tone) / 0.06), - 0 16px 46px hsl(var(--foreground) / 0.08); +.abc-group.is-llm { + --abc-group-summary-height: 122px; } -.abc-group.is-parallel { - --abc-type-tone: 40 43% 38%; - --abc-type-soft: 43 52% 94%; - --abc-type-border: 40 38% 58%; - border-style: solid; +.abc-root.is-readonly .abc-group.is-llm { + --abc-group-summary-height: 84px; +} + +.abc-root.is-readonly .abc-group:not(.is-llm) { + --abc-group-summary-height: 46px; } .abc-group.is-sequential { --abc-type-tone: 213 40% 40%; --abc-type-soft: 214 45% 96%; - --abc-type-border: 213 32% 62%; } -.abc-group.is-llm { - --abc-type-tone: 220 9% 24%; - --abc-type-soft: 220 10% 97%; - --abc-type-border: 220 9% 66%; +.abc-group.is-parallel { + --abc-type-tone: 40 43% 38%; + --abc-type-soft: 43 52% 94%; } .abc-group.is-loop { --abc-type-tone: 151 34% 34%; --abc-type-soft: 148 32% 94%; - --abc-type-border: 151 28% 55%; +} + +.abc-group-body { + position: absolute; + inset: 0; + z-index: 0; + box-sizing: border-box; + border: 1px solid var(--abc-group-tone); + border-radius: 14px; + background: rgb(255 255 255 / 74%); + box-shadow: 0 8px 24px rgb(16 16 19 / 4%); + pointer-events: none; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.abc-group.is-selected > .abc-group-body { + border-color: #cdd3ef; + box-shadow: + inset 0 0 0 0.5px #cdd3ef, + 0 8px 24px rgb(16 16 19 / 5%); +} + +.abc-node.is-selected > .abc-agent-card { + border-color: var(--abc-agent-card-tone); + outline: 0; + box-shadow: inset 0 0 0 0.5px var(--abc-agent-card-tone); } .abc-group-head { position: relative; - height: 64px; + z-index: 3; + width: calc(100% - 16px); + height: 38px; + box-sizing: border-box; display: flex; align-items: center; + gap: 6px; + margin: 8px 8px 0; + padding: 6px 8px; + border: 0; + border-radius: 6px; + background: var(--abc-group-tone); + text-align: left; +} + +.abc-group-head-mark { + width: 16px; + height: 16px; + flex: 0 0 16px; + display: inline-flex; + align-items: center; justify-content: center; - padding: 9px 56px; - border-bottom: 1px solid hsl(var(--border) / 0.75); + color: #101013; } -.abc-group.is-compact-empty .abc-group-head { - border-bottom: 0; +.abc-group-head-mark svg { + width: 16px; + height: 16px; } -.abc-group-head > span:first-child { - width: 100%; +.abc-group-head-identity { min-width: 0; + flex: 1 1 auto; display: flex; flex-direction: column; - align-items: center; - gap: 2px; - text-align: center; + gap: 0; + text-align: left; } -.abc-group-head strong { - color: hsl(var(--abc-type-tone)); - font-size: 13px; - letter-spacing: -0.02em; +.abc-group-head-identity strong, +.abc-group-head-identity small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.abc-group-head-identity strong { + color: #0c0d0e; + font-size: 11px; + font-weight: 500; + line-height: 13px; +} + +.abc-group-head-identity strong.is-name-missing { + color: hsl(var(--destructive)); +} + +.abc-group-head-identity small { + color: #4f5d58; + font-size: 9.5px; + font-weight: 400; + line-height: 11px; } -.abc-group-head small { +.abc-group-summary { + position: relative; + z-index: 2; + width: 100%; + height: var(--abc-group-summary-height); + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: 8px 16px; + text-align: left; +} + +.abc-group-description { + width: 100%; + min-width: 0; + flex: 0 0 30px; display: -webkit-box; overflow: hidden; - color: hsl(var(--muted-foreground)); - font-size: 9.5px; - line-height: 1.3; - white-space: normal; + margin: 0; + color: #383740; + font-size: 10.5px; + font-weight: 400; + line-height: 15px; + text-align: left; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } -.abc-group-add { - height: 40px; - display: flex; - align-items: center; - justify-content: center; - gap: 7px; - border: 1px dashed hsl(var(--abc-type-tone) / 0.42); - border-radius: 10px; - background: hsl(var(--background) / 0.58); - color: hsl(var(--muted-foreground)); - cursor: pointer; - font: inherit; - font-size: 10px; - font-weight: 600; +.abc-group-summary-add { + width: 100% !important; + min-width: 0 !important; + min-height: 28px !important; + height: 28px !important; + flex: 0 0 auto; + margin-top: 10px; + justify-content: center !important; + gap: 4px !important; + padding: 4px 8px !important; + border: 1px dashed #d8dddb !important; + border-radius: 6px !important; + background: #fff !important; + color: #383740 !important; + font-size: 11px !important; + font-weight: 400 !important; + line-height: 16px !important; + white-space: nowrap; transition: - border-color 150ms ease, - background-color 150ms ease, - color 150ms ease; + border-color 160ms ease, + background-color 160ms ease; } -.abc-group-boundary-actions { - position: absolute; - inset: 64px 0 0; - z-index: 2; - pointer-events: none; +.abc-group-summary-add::before { + box-shadow: none !important; } -.abc-group-boundary-add { - position: absolute; - top: 50%; - width: 28px; - height: 28px; +.abc-group-summary-add:hover { + border-color: var(--abc-group-tone) !important; + background: var(--abc-group-tone) !important; +} + +.abc-group-summary-add svg { + width: 10px; + height: 10px; +} + +.abc-group-summary-stats { + min-width: 0; + flex: 0 0 30px; + display: flex; + align-items: flex-end; + gap: 6px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid #ececef; + color: #101013; +} + +.abc-group-summary-stats > span { + height: 20px; display: inline-flex; align-items: center; - justify-content: center; - padding: 0; - border: 1px solid hsl(var(--abc-type-tone) / 0.32); - border-radius: 50%; - background: hsl(var(--background) / 0.94); - box-shadow: 0 6px 18px hsl(var(--foreground) / 0.08); - color: hsl(var(--abc-type-tone)); - cursor: pointer; - opacity: 0.72; - pointer-events: auto; - transform: translateY(-50%); - transition: - border-color 150ms ease, - background-color 150ms ease, - box-shadow 150ms ease, - opacity 150ms ease; + gap: 4px; + padding: 0 7px; + border-radius: 6px; + background: #f5f5f5; } -.abc-group-boundary-add.is-start { - left: 18px; +.abc-group-summary-stats svg { + width: 13px; + height: 13px; } -.abc-group-boundary-add.is-end { - right: 18px; +.abc-group-summary-stats b { + font-size: 11px; + font-weight: 500; + line-height: 14px; } -.abc-root.is-vertical .abc-group-boundary-actions { - inset: 64px 0 0; +.abc-node.is-contained-in-parallel .abc-handle { + opacity: 0; } -.abc-root.is-vertical .abc-group-boundary-add { - left: 50%; - transform: translateX(-50%); +.abc-agent-card { + --abc-agent-card-tone: #e8ebf9; + + box-sizing: border-box; + width: 214px; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 8px; + border: 0.5px solid var(--abc-agent-card-tone); + border-radius: 12px; + background: #fff; + box-shadow: none; + color: #0c0d0e; + text-align: left; } -.abc-root.is-vertical .abc-group-boundary-add.is-start { - top: 18px; +.abc-agent-card-head { + flex: 0 0 42px; + min-width: 0; + display: flex; + align-items: center; + gap: 6px; + padding: 0 8px; + border-radius: 6px; + background: var(--abc-agent-card-tone); } -.abc-root.is-vertical .abc-group-boundary-add.is-end { - top: auto; - right: auto; - bottom: 18px; +.abc-agent-card-mark { + flex: 0 0 16px; + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #101013; } -.abc-group-boundary-add:hover { - border-color: hsl(var(--abc-type-tone) / 0.64); - background: hsl(var(--abc-type-soft) / 0.92); - box-shadow: 0 8px 22px hsl(var(--foreground) / 0.1); - opacity: 1; +.abc-agent-card-mark svg { + width: 16px; + height: 16px; } -.abc-group-boundary-add:focus-visible { - outline: 2px solid hsl(var(--abc-type-tone) / 0.5); - outline-offset: 2px; - opacity: 1; +.abc-agent-card-identity { + min-width: 0; + flex: 1 1 auto; + display: flex; + flex-direction: column; + gap: 3px; + justify-content: center; } -.abc-group-boundary-add svg { - width: 14px; - height: 14px; +.abc-agent-card-head strong { + min-width: 0; + overflow: hidden; + color: #0c0d0e; + font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 12px; + font-weight: 500; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; } -.abc-group-add-empty, -.abc-group-add-bottom { - position: absolute; - right: 24px; - bottom: 24px; - left: 24px; +.abc-agent-card-head strong.is-name-missing { + color: hsl(var(--destructive)); } -.abc-group-add:hover { - border-color: hsl(var(--abc-type-tone) / 0.72); - background: hsl(var(--abc-type-soft) / 0.86); - color: hsl(var(--abc-type-tone)); +.abc-agent-card-model { + min-width: 0; + width: 100%; + overflow: hidden; + color: #4f5d58; + font-size: 9.5px; + font-weight: 400; + line-height: 12px; + text-overflow: ellipsis; + white-space: nowrap; } -.abc-group-add:focus-visible { - outline: 2px solid hsl(var(--abc-type-tone) / 0.5); - outline-offset: 2px; +.abc-agent-card-main { + min-height: 0; + flex: 0 0 80px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 8px 0; } -.abc-group-add svg { - width: 14px; - height: 14px; +.abc-root.is-readonly .abc-agent-card-main { + flex: 0 0 46px; } -.abc-node.is-contained-in-parallel .abc-handle { - opacity: 0; +.abc-agent-card-main p { + min-width: 0; + width: 100%; + flex: 0 1 30px; + display: -webkit-box; + overflow: hidden; + margin: 0; + color: #383740; + font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 10.5px; + font-weight: 400; + line-height: 15px; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } -.abc-node-icon { - width: 38px; - height: 38px; +.abc-agent-card-add { + width: 100% !important; + min-width: 0 !important; + min-height: 26px !important; + height: 26px !important; + flex: 0 0 auto; + justify-content: center !important; + gap: 3px !important; + padding: 4px 6px !important; + border: 1px dashed #d8dddb !important; + border-radius: 6px !important; + background: #fff !important; + color: #383740 !important; + font-size: 11px !important; + font-weight: 400 !important; + line-height: 16px !important; + white-space: nowrap; + transition: + border-color 160ms ease, + background-color 160ms ease; +} + +.abc-agent-card-add::before { + box-shadow: none !important; +} + +.abc-agent-card-add:hover { + border-color: var(--abc-agent-card-tone) !important; + background: var(--abc-agent-card-tone) !important; +} + +.abc-agent-card-add svg { + width: 10px; + height: 10px; +} + +.abc-agent-card-stats { + flex: 0 0 29px; + display: flex; + align-items: flex-end; + gap: 6px; + padding: 8px 0 0; + border-top: 1px solid #ececef; + color: #101013; +} + +.abc-agent-card-stats > span { + height: 20px; display: inline-flex; align-items: center; - justify-content: center; - border-radius: 10px; - background: hsl(var(--abc-type-soft)); - color: hsl(var(--abc-type-tone)); + gap: 4px; + padding: 0 7px; + border-radius: 6px; + background: #f5f5f5; } -.abc-node-icon svg { - width: 17px; - height: 17px; +.abc-agent-card-stats svg { + width: 13px; + height: 13px; } -.abc-node-copy { - min-width: 0; - display: flex; - flex-direction: column; - gap: 2px; - padding-right: 18px; +.abc-agent-card-stats b { + font-size: 11px; + font-weight: 500; + line-height: 14px; } .abc-node-delete { position: absolute; z-index: 3; - top: 7px; - right: 7px; + top: -8px; + right: -8px; width: 24px; height: 24px; display: inline-flex; @@ -373,8 +529,8 @@ } .abc-group > .abc-node-delete { - top: 19px; - right: 12px; + top: -8px; + right: -8px; } .abc-group > .abc-node-delete + .abc-handle { @@ -387,49 +543,122 @@ pointer-events: none; } -.abc-node-meta { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - color: hsl(var(--abc-type-tone)); - font-size: 9px; - font-weight: 700; - letter-spacing: 0.04em; +.abc-canvas .react-flow__node-junction { + width: 18px !important; + height: 18px !important; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; + pointer-events: none; } -.abc-node-copy > strong { - overflow: hidden; - font-size: 13px; - letter-spacing: -0.015em; - text-overflow: ellipsis; +.abc-junction { + --abc-junction-tone: 40 43% 38%; + position: relative; + width: 18px; + height: 18px; + display: grid; + place-items: center; + color: hsl(var(--abc-junction-tone)); +} + +.abc-junction-mark { + width: 11px; + height: 11px; + display: block; + box-sizing: border-box; + border: 1.5px solid currentColor; + background: hsl(var(--panel)); +} + +.abc-junction.is-split .abc-junction-mark { + border-radius: 2px; + transform: rotate(45deg); +} + +.abc-junction.is-merge .abc-junction-mark { + border-radius: 50%; + background: hsl(var(--abc-junction-tone) / 0.18); + box-shadow: inset 0 0 0 2px hsl(var(--panel)); +} + +.abc-junction-label { + position: absolute; + top: 50%; + left: calc(100% + 4px); + padding: 1px 4px; + border-radius: 4px; + background: hsl(var(--panel) / 0.94); + color: hsl(var(--abc-junction-tone)); + font-size: 10px; + font-weight: 600; + line-height: 14px; + transform: translateY(-50%); white-space: nowrap; } -.abc-node-copy > small { - display: -webkit-box; - overflow: hidden; - color: hsl(var(--muted-foreground)); - font-size: 9.5px; - line-height: 1.35; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; +.abc-junction.is-horizontal .abc-junction-label { + top: calc(100% + 3px); + left: 50%; + transform: translateX(-50%); } + .abc-terminal { - width: 96px; - height: 34px; + box-sizing: border-box; + width: 120px; + height: 52px; display: flex; align-items: center; - justify-content: center; - border: 0.5px solid hsl(var(--border) / 0.68); - border-radius: 999px; - background: hsl(var(--secondary) / 0.68); + gap: 10px; + padding: 10px 16px 10px 10px; + border: 0; + border-radius: 10px; + background: #fff; box-shadow: none; - color: hsl(var(--foreground) / 0.76); - font-size: 10.5px; - font-weight: 650; - letter-spacing: 0.03em; + color: #0c0d0e; + font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 13px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0; + white-space: nowrap; +} + +.abc-terminal-mark { + width: 32px; + height: 32px; + flex: 0 0 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 5px; + color: #0c0d0e; +} + +.abc-terminal.is-input .abc-terminal-mark { + background: #f9eea2; +} + +.abc-terminal.is-output .abc-terminal-mark { + background: #d2e6ff; +} + +.abc-terminal.is-input .abc-terminal-mark svg { + width: 13px; + height: 13px; +} + +.abc-terminal.is-output .abc-terminal-mark svg { + width: 16px; + height: 16px; +} + +.abc-terminal-title { + min-width: 0; + flex: 0 0 52px; + text-align: center; } .abc-handle { @@ -441,6 +670,27 @@ pointer-events: none; } +.abc-root.is-horizontal .abc-group > .react-flow__handle-left { + top: 32px !important; + left: 0 !important; +} + +.abc-root.is-horizontal .abc-group > .react-flow__handle-right { + top: 32px !important; + right: 0 !important; +} + +.abc-root.is-vertical .abc-group > .react-flow__handle-top { + top: 0 !important; + left: 50% !important; +} + +.abc-root.is-vertical .abc-group > .react-flow__handle-bottom { + top: auto !important; + bottom: 0 !important; + left: 50% !important; +} + .abc-group.is-sequential > .abc-handle { background: hsl(213 40% 40%) !important; } @@ -556,6 +806,10 @@ .abc-edge-add { transition: none; } + + .abc-canvas .react-flow__edge.animated path { + animation: none; + } } .abc-canvas .react-flow__controls { diff --git a/frontend/src/create/AgentBuildCanvas.tsx b/frontend/src/create/AgentBuildCanvas.tsx index 9f6970672..9860942ce 100644 --- a/frontend/src/create/AgentBuildCanvas.tsx +++ b/frontend/src/create/AgentBuildCanvas.tsx @@ -13,6 +13,7 @@ import { Controls, EdgeLabelRenderer, getSmoothStepPath, + getStraightPath, Handle, MarkerType, MiniMap, @@ -30,16 +31,18 @@ import { type NodeProps, } from "@xyflow/react"; import dagre from "@dagrejs/dagre"; -import { - ArrowRightLeft, - Bot, - Globe, - ListOrdered, - Plus, - Repeat, - Trash2, -} from "lucide-react"; +import { Button } from "@openai/apps-sdk-ui/components/Button"; +import { Plus, Trash2 } from "lucide-react"; import type { AgentDraft } from "./types"; +import { + AgentSkillCountIcon, + AgentToolCountIcon, + CanvasAgentTypeIcon, +} from "../ui/CapabilityIcons"; +import { + TerminalFinalReplyIcon, + TerminalUserRequestIcon, +} from "../ui/icons/CreateAgentIcons"; import "@xyflow/react/dist/style.css"; import "./AgentBuildCanvas.css"; @@ -48,68 +51,132 @@ type AgentType = NonNullable; const PATTERN_COPY: Record< AgentType, - { label: string; description: string; icon: typeof Bot } + { label: string; description: string } > = { llm: { label: "智能体", description: "理解任务并直接完成一个具体工作", - icon: Bot, }, sequential: { label: "分步协作", description: "内部步骤按照顺序依次执行", - icon: ListOrdered, }, parallel: { label: "同时处理", description: "内部步骤同时工作,完成后统一汇总", - icon: ArrowRightLeft, }, loop: { label: "循环执行", description: "重复执行内部步骤,直到满足停止条件", - icon: Repeat, }, a2a: { label: "远程智能体", description: "调用已经存在的远程 Agent", - icon: Globe, }, }; type CanvasNodeData = { - kind: "agent" | "terminal"; + kind: "agent" | "terminal" | "junction"; path?: NodePath; agent?: AgentDraft; title: string; + nameMissing?: boolean; pattern?: AgentType; + modelLabel?: string; description?: string; + toolCount?: number; + skillCount?: number; childCount?: number; containedIn?: AgentType; layoutWidth?: number; layoutHeight?: number; - compactEmptyGroup?: boolean; + terminalKind?: "input" | "output"; + junctionKind?: "split" | "merge"; + junctionDirection?: CanvasDirection; }; type CanvasNode = Node; -const NODE_WIDTH = 220; -const NODE_HEIGHT = 88; -const TERMINAL_WIDTH = 96; -const TERMINAL_HEIGHT = 34; -const GROUP_HEADER_HEIGHT = 64; +const NODE_WIDTH = 214; +const NODE_HEIGHT = 168; +const COMPACT_NODE_HEIGHT = 138; +const READ_ONLY_NODE_HEIGHT = 133; +const READ_ONLY_COMPACT_NODE_HEIGHT = 104; + +function canvasAgentTitle(agent: AgentDraft): string { + if (agent.agentType === "a2a") return "远程智能体"; + return agent.name.trim() || "名称未配置"; +} +function canvasAgentModel(agent: AgentDraft): string { + return agent.modelName?.trim() || agent.model?.trim() || "模型未配置"; +} + +function uniqueCapabilityCount(values: Array): number { + return new Set( + values.map((value) => value?.trim()).filter((value): value is string => !!value), + ).size; +} + +function canvasAgentCardData(agent: AgentDraft) { + return { + modelLabel: canvasAgentModel(agent), + description: agent.description.trim() || "描述未配置", + toolCount: uniqueCapabilityCount([ + ...agent.tools, + ...(agent.builtinTools ?? []), + ...(agent.customTools ?? []).map((tool) => tool.name), + ...(agent.mcpTools ?? []).map((tool) => tool.name), + ]), + skillCount: uniqueCapabilityCount([ + ...agent.skills, + ...(agent.selectedSkills ?? []).map((skill) => skill.name), + ]), + }; +} +const TERMINAL_WIDTH = 120; +const TERMINAL_HEIGHT = 52; +const FIGMA_FLOW_LINE_LENGTH = 69; +const FLOW_HANDLE_SIZE = 7; +const GROUP_HEADER_HEIGHT = 46; +const GROUP_SUMMARY_HEIGHT = 122; +const GROUP_COMPACT_SUMMARY_HEIGHT = 84; +const READ_ONLY_GROUP_SUMMARY_HEIGHT = 84; +const READ_ONLY_GROUP_COMPACT_SUMMARY_HEIGHT = 46; const GROUP_MIN_WIDTH = 310; const GROUP_PADDING = 24; const GROUP_BOUNDARY_PADDING = 56; const GROUP_GAP = 40; -const GROUP_ADD_HEIGHT = 40; -const GROUP_ADD_GAP = 18; const LOOP_EDGE_SPACE = 58; +const PARALLEL_RAIL_SPACE = 44; +const JUNCTION_SIZE = 18; const MINIMAP_ENABLED = false; const isContainerType = (type: AgentType) => type === "sequential" || type === "parallel" || type === "loop"; +const showsModelCapabilities = (type: AgentType): boolean => type === "llm"; + +const nodeHeight = (type: AgentType, readOnly: boolean): number => + showsModelCapabilities(type) + ? readOnly + ? READ_ONLY_NODE_HEIGHT + : NODE_HEIGHT + : readOnly + ? READ_ONLY_COMPACT_NODE_HEIGHT + : COMPACT_NODE_HEIGHT; + +const groupSummaryHeight = (type: AgentType, readOnly: boolean): number => + showsModelCapabilities(type) + ? readOnly + ? READ_ONLY_GROUP_SUMMARY_HEIGHT + : GROUP_SUMMARY_HEIGHT + : readOnly + ? READ_ONLY_GROUP_COMPACT_SUMMARY_HEIGHT + : GROUP_COMPACT_SUMMARY_HEIGHT; + +const groupContentTop = (type: AgentType, readOnly: boolean): number => + GROUP_HEADER_HEIGHT + groupSummaryHeight(type, readOnly); + function rendersAsGroup(agent: AgentDraft, path: NodePath): boolean { const type = agent.agentType ?? "llm"; return ( @@ -124,17 +191,15 @@ function measureAgent( agent: AgentDraft, path: NodePath = [], direction: CanvasDirection = "horizontal", - compactEmptyGroups = false, + readOnly = false, ): { width: number; height: number } { const type = agent.agentType ?? "llm"; if (!rendersAsGroup(agent, path)) { - return { width: NODE_WIDTH, height: NODE_HEIGHT }; - } - if (compactEmptyGroups && agent.subAgents.length === 0) { - return { width: GROUP_MIN_WIDTH, height: GROUP_HEADER_HEIGHT }; + return { width: NODE_WIDTH, height: nodeHeight(type, readOnly) }; } + const contentTop = groupContentTop(type, readOnly); const sizes = agent.subAgents.map((child, index) => - measureAgent(child, [...path, index], direction, compactEmptyGroups), + measureAgent(child, [...path, index], direction, readOnly), ); const widestChild = sizes.length ? Math.max(...sizes.map((size) => size.width)) @@ -148,21 +213,24 @@ function measureAgent( const horizontalChildren = direction === "horizontal" ? type !== "parallel" : type === "parallel"; - const bottomSpace = sizes.length - ? type === "parallel" - ? GROUP_ADD_GAP + GROUP_ADD_HEIGHT - : type === "loop" - ? LOOP_EDGE_SPACE - : 0 - : GROUP_ADD_HEIGHT; + if (sizes.length === 0) { + return { width: GROUP_MIN_WIDTH, height: contentTop }; + } + const bottomSpace = type === "loop" ? LOOP_EDGE_SPACE : 0; + const parallelHorizontalRails = type === "parallel" && !horizontalChildren + ? PARALLEL_RAIL_SPACE * 2 + : 0; + const parallelVerticalRails = type === "parallel" && horizontalChildren + ? PARALLEL_RAIL_SPACE * 2 + : 0; if (!horizontalChildren) { return { width: Math.max( GROUP_MIN_WIDTH, - widestChild + GROUP_PADDING * 2, + widestChild + GROUP_PADDING * 2 + parallelHorizontalRails, ), height: - GROUP_HEADER_HEIGHT + + contentTop + flowPadding + sizes.reduce((sum, size) => sum + size.height, 0) + GROUP_GAP * Math.max(0, sizes.length - 1) + @@ -178,8 +246,9 @@ function measureAgent( flowPadding * 2, ), height: - GROUP_HEADER_HEIGHT + + contentTop + GROUP_PADDING + + parallelVerticalRails + tallestChild + bottomSpace + GROUP_PADDING, @@ -206,6 +275,8 @@ type CanvasEdgeData = { insert?: { parentPath: NodePath; index: number }; loop?: boolean; tone?: AgentType; + figmaFlow?: boolean; + semantic?: "call" | "sequence" | "branch" | "merge" | "return"; }; type CanvasEdge = Edge; @@ -218,20 +289,27 @@ function makeEdge( loop?: boolean; tone?: AgentType; insert?: CanvasEdgeData["insert"]; + figmaFlow?: boolean; + semantic?: CanvasEdgeData["semantic"]; + sourceHandle?: string; + targetHandle?: string; }, ): CanvasEdge { - const edgeColor = - options?.tone === "sequential" + const edgeColor = options?.figmaFlow + ? "#C9CDD4" + : options?.tone === "sequential" ? "hsl(213 40% 40%)" - : options?.tone === "loop" - ? "hsl(151 34% 34%)" - : "hsl(220 9% 38%)"; + : options?.tone === "parallel" + ? "hsl(40 43% 38%)" + : options?.tone === "loop" + ? "hsl(151 34% 34%)" + : "hsl(220 9% 38%)"; return { id: `${source}-${target}${options?.loop ? "-loop" : ""}`, source, target, - sourceHandle: options?.loop ? "loop-source" : undefined, - targetHandle: options?.loop ? "loop-target" : undefined, + sourceHandle: options?.loop ? "loop-source" : options?.sourceHandle, + targetHandle: options?.loop ? "loop-target" : options?.targetHandle, label, type: "insertStep", data: options @@ -239,19 +317,24 @@ function makeEdge( insert: options.insert, loop: options.loop, tone: options.tone, + figmaFlow: options.figmaFlow, + semantic: options.semantic, } : undefined, animated: options?.loop, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 16, - height: 16, - color: edgeColor, - }, + markerEnd: options?.figmaFlow + ? undefined + : { + type: MarkerType.ArrowClosed, + width: 16, + height: 16, + color: edgeColor, + }, style: { stroke: edgeColor, strokeWidth: 1.5, }, + zIndex: options?.figmaFlow ? 0 : 2, labelStyle: { fill: "hsl(215 14% 42%)", fontSize: 10, @@ -267,7 +350,7 @@ function makeEdge( function buildCanvasGraph( root: AgentDraft, direction: CanvasDirection, - compactEmptyGroups = false, + readOnly = false, ): { nodes: CanvasNode[]; edges: CanvasEdge[]; @@ -277,7 +360,7 @@ function buildCanvasGraph( id: "terminal-input", type: "terminal", position: { x: 0, y: 0 }, - data: { kind: "terminal", title: "用户请求" }, + data: { kind: "terminal", title: "用户请求", terminalKind: "input" }, selectable: false, draggable: false, }, @@ -285,12 +368,13 @@ function buildCanvasGraph( id: "terminal-output", type: "terminal", position: { x: 0, y: 0 }, - data: { kind: "terminal", title: "最终回复" }, + data: { kind: "terminal", title: "最终回复", terminalKind: "output" }, selectable: false, draggable: false, }, ]; const edges: CanvasEdge[] = []; + const groupExitIds = new Map(); function addContainedNode( agent: AgentDraft, @@ -305,24 +389,54 @@ function buildCanvasGraph( addGroupNode(agent, path, parentId, position, containedIn); return id; } + const height = nodeHeight(type, readOnly); nodes.push({ id, type: "agent", parentId, extent: "parent", position, + style: { width: NODE_WIDTH, height }, data: { kind: "agent", path, agent, - title: - type === "a2a" - ? "远程智能体" - : agent.name.trim() || (path.length === 0 ? "主 Agent" : "未命名步骤"), + title: canvasAgentTitle(agent), + ...canvasAgentCardData(agent), + nameMissing: type !== "a2a" && agent.name.trim().length === 0, pattern: type, - description: agent.description.trim() || PATTERN_COPY[type].description, childCount: agent.subAgents.length, containedIn, + layoutWidth: NODE_WIDTH, + layoutHeight: height, + }, + }); + return id; + } + + function addParallelJunction( + kind: "split" | "merge", + parentId: string, + path: NodePath, + position: { x: number; y: number }, + junctionDirection: CanvasDirection, + ): string { + const id = `${pathId(path)}-parallel-${kind}`; + nodes.push({ + id, + type: "junction", + parentId, + extent: "parent", + position, + style: { width: JUNCTION_SIZE, height: JUNCTION_SIZE }, + selectable: false, + draggable: false, + connectable: false, + data: { + kind: "junction", + title: kind === "split" ? "并行分发" : "并行汇合", + junctionKind: kind, + junctionDirection, }, }); return id; @@ -336,8 +450,9 @@ function buildCanvasGraph( containedIn?: AgentType, ): string { const type = agent.agentType ?? "sequential"; + const contentTop = groupContentTop(type, readOnly); const id = pathId(path); - const size = measureAgent(agent, path, direction, compactEmptyGroups); + const size = measureAgent(agent, path, direction, readOnly); nodes.push({ id, type: "group", @@ -349,21 +464,19 @@ function buildCanvasGraph( kind: "agent", path, agent, - title: - agent.name.trim() || - (path.length === 0 ? "主 Agent" : PATTERN_COPY[type].label), + title: canvasAgentTitle(agent), + ...canvasAgentCardData(agent), + nameMissing: type !== "a2a" && agent.name.trim().length === 0, pattern: type, - description: agent.description.trim() || PATTERN_COPY[type].description, childCount: agent.subAgents.length, containedIn, layoutWidth: size.width, layoutHeight: size.height, - compactEmptyGroup: compactEmptyGroups && agent.subAgents.length === 0, }, }); const childSizes = agent.subAgents.map((child, index) => - measureAgent(child, [...path, index], direction, compactEmptyGroups), + measureAgent(child, [...path, index], direction, readOnly), ); const flowPadding = childSizes.length && type !== "parallel" ? GROUP_BOUNDARY_PADDING @@ -371,18 +484,24 @@ function buildCanvasGraph( const horizontalChildren = direction === "horizontal" ? type !== "parallel" : type === "parallel"; - let cursor = flowPadding; + let cursor = horizontalChildren ? flowPadding : GROUP_BOUNDARY_PADDING; const childIds = agent.subAgents.map((child, index) => { const childSize = childSizes[index]; const childPosition = horizontalChildren ? { x: cursor, - y: GROUP_HEADER_HEIGHT + GROUP_PADDING, + y: + contentTop + + GROUP_PADDING + + (type === "parallel" ? PARALLEL_RAIL_SPACE : 0), } : { - x: (size.width - childSize.width) / 2, - y: GROUP_HEADER_HEIGHT + cursor, + x: + type === "parallel" + ? GROUP_PADDING + PARALLEL_RAIL_SPACE + : (size.width - childSize.width) / 2, + y: contentTop + cursor, }; cursor += (horizontalChildren ? childSize.width : childSize.height) + GROUP_GAP; @@ -395,11 +514,30 @@ function buildCanvasGraph( ); }); + if (childIds.length === 0) { + groupExitIds.set(id, [id]); + return id; + } + + if (type === "llm") { + for (let index = 0; index < childIds.length - 1; index += 1) { + edges.push( + makeEdge(childIds[index], childIds[index + 1], "然后", { + tone: "llm", + semantic: "sequence", + insert: { parentPath: path, index: index + 1 }, + }), + ); + } + groupExitIds.set(id, [id]); + } + if (type === "sequential" || type === "loop") { for (let index = 0; index < childIds.length - 1; index += 1) { edges.push( makeEdge(childIds[index], childIds[index + 1], "然后", { tone: type, + semantic: "sequence", insert: { parentPath: path, index: index + 1 }, }), ); @@ -409,9 +547,90 @@ function buildCanvasGraph( makeEdge(childIds[childIds.length - 1], childIds[0], "继续循环", { loop: true, tone: "loop", + semantic: "return", }), ); } + groupExitIds.set(id, [id]); + } + + if (type === "parallel") { + const junctionDirection = horizontalChildren ? "vertical" : "horizontal"; + const tallestChild = Math.max(...childSizes.map((child) => child.height)); + const widestChild = Math.max(...childSizes.map((child) => child.width)); + const stackedHeight = childSizes.reduce( + (total, child) => total + child.height, + GROUP_GAP * Math.max(0, childSizes.length - 1), + ); + const splitPosition = horizontalChildren + ? { + x: size.width / 2 - JUNCTION_SIZE / 2, + y: + contentTop + + GROUP_PADDING + + PARALLEL_RAIL_SPACE / 2 - + JUNCTION_SIZE / 2, + } + : { + x: + GROUP_PADDING + + PARALLEL_RAIL_SPACE / 2 - + JUNCTION_SIZE / 2, + y: + contentTop + + GROUP_PADDING + + (stackedHeight - JUNCTION_SIZE) / 2, + }; + const mergePosition = horizontalChildren + ? { + x: size.width / 2 - JUNCTION_SIZE / 2, + y: + contentTop + + GROUP_PADDING + + PARALLEL_RAIL_SPACE + + tallestChild + + PARALLEL_RAIL_SPACE / 2 - + JUNCTION_SIZE / 2, + } + : { + x: + GROUP_PADDING + + PARALLEL_RAIL_SPACE + + widestChild + + PARALLEL_RAIL_SPACE / 2 - + JUNCTION_SIZE / 2, + y: + contentTop + + GROUP_PADDING + + (stackedHeight - JUNCTION_SIZE) / 2, + }; + const splitId = addParallelJunction( + "split", + id, + path, + splitPosition, + junctionDirection, + ); + const mergeId = addParallelJunction( + "merge", + id, + path, + mergePosition, + junctionDirection, + ); + childIds.forEach((childId) => { + edges.push( + makeEdge(splitId, childId, undefined, { + tone: "parallel", + semantic: "branch", + }), + makeEdge(childId, mergeId, undefined, { + tone: "parallel", + semantic: "merge", + }), + ); + }); + groupExitIds.set(id, [id]); } return id; } @@ -421,46 +640,35 @@ function buildCanvasGraph( const id = pathId(path); if (rendersAsGroup(agent, path)) { addGroupNode(agent, path); - return [id]; + return groupExitIds.get(id) ?? [id]; } + const height = nodeHeight(type, readOnly); nodes.push({ id, type: "agent", position: { x: 0, y: 0 }, + style: { width: NODE_WIDTH, height }, data: { kind: "agent", path, agent, - title: - type === "a2a" - ? "远程智能体" - : agent.name.trim() || (path.length === 0 ? "主 Agent" : "未命名步骤"), + title: canvasAgentTitle(agent), + ...canvasAgentCardData(agent), + nameMissing: type !== "a2a" && agent.name.trim().length === 0, pattern: type, - description: agent.description.trim() || PATTERN_COPY[type].description, childCount: agent.subAgents.length, + layoutWidth: NODE_WIDTH, + layoutHeight: height, }, }); - if (agent.subAgents.length === 0) return [id]; - - const exits: string[] = []; - agent.subAgents.forEach((child, index) => { - const childPath = [...path, index]; - const childId = pathId(childPath); - edges.push( - makeEdge(id, childId, "调用", { - insert: { parentPath: path, index }, - }), - ); - exits.push(...addTopLevelNode(child, childPath)); - }); - return exits; + return [id]; }; const rootId = pathId([]); const exits = addTopLevelNode(root, []); - edges.push(makeEdge("terminal-input", rootId)); + edges.push(makeEdge("terminal-input", rootId, undefined, { figmaFlow: true })); exits.forEach((exitId) => - edges.push(makeEdge(exitId, "terminal-output")), + edges.push(makeEdge(exitId, "terminal-output", undefined, { figmaFlow: true })), ); return layoutGraph(nodes, edges, direction); } @@ -473,43 +681,55 @@ function layoutGraph( const graph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); graph.setGraph({ rankdir: direction === "vertical" ? "TB" : "LR", - ranksep: 50, + // Invisible React Flow handles inset each endpoint by half their diameter. + ranksep: FIGMA_FLOW_LINE_LENGTH + FLOW_HANDLE_SIZE, nodesep: 34, edgesep: 14, marginx: 24, marginy: 24, }); - const topLevelIds = new Set( - nodes.filter((node) => !node.parentId).map((node) => node.id), - ); + const nodeById = new Map(nodes.map((node) => [node.id, node] as const)); + const topLevelId = (nodeId: string): string => { + let node = nodeById.get(nodeId); + while (node?.parentId) { + node = nodeById.get(node.parentId); + } + return node?.id ?? nodeId; + }; nodes.filter((node) => !node.parentId).forEach((node) => { const terminal = node.data.kind === "terminal"; + const groupHeaderAligned = node.type === "group" && direction === "horizontal"; graph.setNode(node.id, { width: terminal ? TERMINAL_WIDTH : node.data.layoutWidth ?? NODE_WIDTH, height: terminal ? TERMINAL_HEIGHT - : node.data.layoutHeight ?? NODE_HEIGHT, + : groupHeaderAligned + ? GROUP_HEADER_HEIGHT + : node.data.layoutHeight ?? NODE_HEIGHT, }); }); - edges - .filter( - (edge) => topLevelIds.has(edge.source) && topLevelIds.has(edge.target), - ) - .forEach((edge) => graph.setEdge(edge.source, edge.target)); + edges.forEach((edge) => { + const source = topLevelId(edge.source); + const target = topLevelId(edge.target); + if (source !== target) graph.setEdge(source, target); + }); dagre.layout(graph); return { nodes: nodes.map((node) => { if (node.parentId) return node; const position = graph.node(node.id) as { x: number; y: number }; const terminal = node.data.kind === "terminal"; + const groupHeaderAligned = node.type === "group" && direction === "horizontal"; const width = terminal ? TERMINAL_WIDTH : node.data.layoutWidth ?? NODE_WIDTH; const height = terminal ? TERMINAL_HEIGHT - : node.data.layoutHeight ?? NODE_HEIGHT; + : groupHeaderAligned + ? GROUP_HEADER_HEIGHT + : node.data.layoutHeight ?? NODE_HEIGHT; return { ...node, position: { x: position.x - width / 2, y: position.y - height / 2 }, @@ -543,15 +763,17 @@ function InsertStepEdge({ }: EdgeProps) { const actions = useContext(CanvasActionsContext); const [showInsert, setShowInsert] = useState(false); - const [edgePath, labelX, labelY] = getSmoothStepPath({ - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - offset: data?.loop ? 28 : 20, - }); + const [edgePath, labelX, labelY] = data?.figmaFlow + ? getStraightPath({ sourceX, sourceY, targetX, targetY }) + : getSmoothStepPath({ + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + offset: data?.loop ? 28 : 20, + }); return ( <> void; +}) { + const patternLabel = PATTERN_COPY[data.pattern ?? "llm"].label; + const type = data.pattern ?? "llm"; + const showModelCapabilities = showsModelCapabilities(type); + return ( +
+
+ + + + + + {data.title} + + {showModelCapabilities && ( + + {data.modelLabel} + + )} + +
+
+

{data.description}

+ {onAdd && ( + + )} +
+ {showModelCapabilities && ( +
+ + + {data.skillCount ?? 0} + + + + {data.toolCount ?? 0} + +
+ )} +
+ ); +} + function AgentCanvasNode({ data, selected }: NodeProps) { const actions = useContext(CanvasActionsContext); const direction = useContext(CanvasDirectionContext); @@ -612,8 +900,6 @@ function AgentCanvasNode({ data, selected }: NodeProps) { const sourcePosition = direction === "vertical" ? Position.Bottom : Position.Right; const loopPosition = direction === "vertical" ? Position.Right : Position.Bottom; const type = data.pattern ?? "llm"; - const copy = PATTERN_COPY[type]; - const Icon = copy.icon; return (
) { }${selected ? " is-selected" : ""}`} > - {type !== "llm" && ( - - )} - - - {copy.label} - - {data.title} - {data.description} - + actions.onAdd(data.path!) + : undefined + } + /> {actions && data.path !== undefined && data.path.length > 0 && ( - -
- )} - {actions && - data.path !== undefined && - childCount > 0 && - type === "parallel" && ( - - )} - {actions && data.path !== undefined && childCount === 0 && ( - - )} + 添加子 Agent + + )} + {showModelCapabilities && ( +
+ + + {data.skillCount ?? 0} + + + + {data.toolCount ?? 0} + +
+ )} + {actions && data.path !== undefined && data.path.length > 0 && ( + + +
+ {messages.map((message) => ( + + {message.role === "user" ? ( +
+ +
+ ) : ( + <> + + {message.error ? ( +
+ {message.error} +
+ ) : null} + + )} +
+ ))} +
+ +
+