From a8d2f5d90ecc6fc699dcb50bc436d2b4b81302ac Mon Sep 17 00:00:00 2001 From: evanlowe <62918515+evanlowe@users.noreply.github.com> Date: Mon, 27 Jul 2026 08:41:07 +0800 Subject: [PATCH 1/4] feat(studio): add unified agent workspace and deployment lifecycle --- frontend/package-lock.json | 16 + frontend/package.json | 1 + frontend/src/App.tsx | 326 +++- frontend/src/adk/client.ts | 28 + frontend/src/adk/connections.ts | 19 +- frontend/src/assets/feishu-logo.svg | 3 + frontend/src/create/AgentBuildCanvas.css | 720 ++++++++ frontend/src/create/AgentBuildCanvas.tsx | 1148 +++++++++++++ frontend/src/create/CustomCreate.css | 907 +++++++++- frontend/src/create/CustomCreate.tsx | 680 +++++--- frontend/src/styles.css | 65 +- frontend/src/ui/AgentSelector.tsx | 41 +- frontend/src/ui/AgentTopology.tsx | 16 +- frontend/src/ui/AgentWorkspace.css | 1516 +++++++++++++++++ frontend/src/ui/AgentWorkspace.tsx | 982 +++++++++++ frontend/src/ui/DeploymentErrorMessage.tsx | 4 +- frontend/src/ui/ProjectPreview.css | 747 +++++--- frontend/src/ui/ProjectPreview.tsx | 698 ++++---- frontend/src/ui/Search.tsx | 4 +- frontend/src/ui/Sidebar.tsx | 130 +- frontend/tests/agentBuildCanvas.test.mjs | 51 + frontend/tests/agentInfoRail.test.mjs | 12 + frontend/tests/agentSelectorInfo.test.mjs | 9 + frontend/tests/agentWorkspace.test.mjs | 104 ++ frontend/tests/deploymentConfigUi.test.mjs | 175 +- frontend/tests/deploymentEnv.test.mjs | 46 +- frontend/tests/deploymentIcon.test.mjs | 7 +- .../tests/manageAgentsConnection.test.mjs | 10 +- frontend/tests/markdownPromptEditor.test.mjs | 62 +- frontend/tests/newChatComposerLayout.test.mjs | 8 + .../tests/newChatModeCapabilities.test.mjs | 12 + frontend/tests/sessionHeader.test.mjs | 12 + frontend/tests/siteBranding.test.mjs | 37 +- frontend/tests/skillCreateMode.test.mjs | 2 +- frontend/tests/studioAccess.test.mjs | 11 +- ...enerated_agent_backend_codegen_extended.py | 39 +- .../test_generated_agent_component_matrix.py | 17 +- tests/cli/test_studio_rbac.py | 76 + tests/integrations/agentkit/test_app.py | 55 + veadk/cli/cli_frontend.py | 71 +- veadk/cli/generated_agent_codegen.py | 68 +- veadk/integrations/agentkit/app.py | 17 +- ...tor-CVAziWe1.js => CodeEditor-CQaaLV9H.js} | 2 +- ...Ar.js => MarkdownPromptEditor-Sz0mlQDJ.js} | 2 +- veadk/webui/assets/index-D6SFqNe-.css | 10 + veadk/webui/assets/index-DxTWMaej.js | 697 -------- veadk/webui/assets/index-IHJQWhpe.js | 697 ++++++++ veadk/webui/assets/index-kH5rdUDl.css | 10 - veadk/webui/index.html | 4 +- 49 files changed, 8537 insertions(+), 1837 deletions(-) create mode 100644 frontend/src/assets/feishu-logo.svg create mode 100644 frontend/src/create/AgentBuildCanvas.css create mode 100644 frontend/src/create/AgentBuildCanvas.tsx create mode 100644 frontend/src/ui/AgentWorkspace.css create mode 100644 frontend/src/ui/AgentWorkspace.tsx create mode 100644 frontend/tests/agentBuildCanvas.test.mjs create mode 100644 frontend/tests/agentWorkspace.test.mjs rename veadk/webui/assets/{CodeEditor-CVAziWe1.js => CodeEditor-CQaaLV9H.js} (99%) rename veadk/webui/assets/{MarkdownPromptEditor-C2yRMCAr.js => MarkdownPromptEditor-Sz0mlQDJ.js} (99%) create mode 100644 veadk/webui/assets/index-D6SFqNe-.css delete mode 100644 veadk/webui/assets/index-DxTWMaej.js create mode 100644 veadk/webui/assets/index-IHJQWhpe.js delete mode 100644 veadk/webui/assets/index-kH5rdUDl.css diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 94ec3e1c..9e932695 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@codemirror/lang-markdown": "^6.5.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-yaml": "^6.1.3", + "@dagrejs/dagre": "^3.0.0", "@mdxeditor/editor": "^4.1.0", "@uiw/react-codemirror": "^4.25.11", "@xyflow/react": "^12.11.0", @@ -753,6 +754,21 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@dagrejs/dagre": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@dagrejs/dagre/-/dagre-3.0.0.tgz", + "integrity": "sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "4.0.1" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@dagrejs/graphlib/-/graphlib-4.0.1.tgz", + "integrity": "sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 06042f48..9ad31b4e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@codemirror/lang-markdown": "^6.5.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-yaml": "^6.1.3", + "@dagrejs/dagre": "^3.0.0", "@mdxeditor/editor": "^4.1.0", "@uiw/react-codemirror": "^4.25.11", "@xyflow/react": "^12.11.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c5adf4a8..e243229a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,7 @@ import { deleteSession, getAgentInfo, getSessionCapabilities, + getRuntimes, getSession, getStudioAccess, listApps, @@ -45,8 +46,8 @@ import { type AdkSession, type AddSessionCapability, type Attachment, + type CloudRuntime, type FrontendInvocation, - type ManagedRuntime, type MessageFeedbackRating, type SiteBranding, type SessionCapabilities, @@ -67,7 +68,10 @@ import { AgentInfoDrawer, AgentInfoPanel } from "./ui/AgentTopology"; import { AgentIdentityIcon } from "./ui/AgentIdentityIcon"; import { SkillCenterView } from "./ui/SkillCenter"; import { AddAgentKitView } from "./ui/AddAgentKit"; -import { ManageAgentsView } from "./ui/ManageAgents"; +import { + AgentWorkspace, + type WorkspaceAgentDraft, +} from "./ui/AgentWorkspace"; import { SearchView } from "./ui/Search"; import { buildAgentEntries, @@ -178,6 +182,24 @@ function loadView(): CreateView { ? v : null; } + +function workspaceDraftsKey(userId: string): string { + return `veadk.agentDrafts.${encodeURIComponent(userId)}`; +} + +function activeWorkspaceDraftKey(userId: string): string { + return `${workspaceDraftsKey(userId)}.active`; +} + +function loadWorkspaceDrafts(userId: string): WorkspaceAgentDraft[] { + if (!userId) return []; + try { + const value = JSON.parse(localStorage.getItem(workspaceDraftsKey(userId)) || "[]"); + return Array.isArray(value) ? value : []; + } catch { + return []; + } +} import { TraceDrawer } from "./ui/TraceDrawer"; import { LoginPage } from "./ui/LoginPage"; import { Markdown } from "./ui/Markdown"; @@ -623,6 +645,8 @@ export default function App() { const [attachments, setAttachments] = useState([]); const [invocation, setInvocation] = useState(emptyInvocation); const [agentInfo, setAgentInfo] = useState(null); + const [agentInfoAgentId, setAgentInfoAgentId] = useState(""); + const [agentInfoRefreshKey, setAgentInfoRefreshKey] = useState(0); const [capabilitiesLoading, setCapabilitiesLoading] = useState(false); const [sessionCapabilities, setSessionCapabilities] = useState(null); @@ -891,6 +915,8 @@ export default function App() { const [addMenu, setAddMenu] = useState(false); // A draft imported from YAML, used to pre-fill the custom wizard once. const [importedDraft, setImportedDraft] = useState(null); + const [savedAgentDrafts, setSavedAgentDrafts] = useState([]); + const [editingDraftId, setEditingDraftId] = useState(""); const [searchView, setSearchView] = useState(false); // The "管理 Agent" view: lists/deletes the current user's AgentKit runtimes. const [manageAgents, setManageAgents] = useState(false); @@ -904,6 +930,19 @@ export default function App() { registerConnections(c); return c; }); + const [agentLibraryLoading, setAgentLibraryLoading] = useState(false); + const [agentLibraryError, setAgentLibraryError] = useState(""); + const [libraryRuntimeIds, setLibraryRuntimeIds] = useState | null>( + null, + ); + const [runtimeUpdateTarget, setRuntimeUpdateTarget] = useState<{ + runtimeId: string; + name: string; + region: string; + currentVersion?: number | null; + } | null>(null); + const [focusedDeploymentTaskId, setFocusedDeploymentTaskId] = useState(""); + const [focusedWorkspaceAgentId, setFocusedWorkspaceAgentId] = useState(""); // Shown when the user clicks the breadcrumb root to leave a create mode; // warns that the in-progress draft will be discarded. const [confirmLeave, setConfirmLeave] = useState(false); @@ -911,6 +950,110 @@ export default function App() { const restoredRef = useRef(false); const defaultViewAppliedRef = useRef(false); + const saveWorkspaceDraft = useCallback( + ( + id: string, + draft: AgentDraft, + deploymentTarget?: WorkspaceAgentDraft["deploymentTarget"], + ) => { + if (!id || !userId) return; + setSavedAgentDrafts((current) => { + const nextItem: WorkspaceAgentDraft = { + id, + draft, + updatedAt: Date.now(), + deploymentTarget, + }; + const next = [nextItem, ...current.filter((item) => item.id !== id)]; + localStorage.setItem(workspaceDraftsKey(userId), JSON.stringify(next)); + return next; + }); + }, + [userId], + ); + + const removeWorkspaceDraft = useCallback((id: string) => { + if (!id || !userId) return; + setSavedAgentDrafts((current) => { + const next = current.filter((item) => item.id !== id); + localStorage.setItem(workspaceDraftsKey(userId), JSON.stringify(next)); + return next; + }); + }, [userId]); + + useEffect(() => { + if (!userId) { + setSavedAgentDrafts([]); + setEditingDraftId(""); + return; + } + const nextDrafts = loadWorkspaceDrafts(userId); + setSavedAgentDrafts(nextDrafts); + const activeId = localStorage.getItem(activeWorkspaceDraftKey(userId)) || ""; + const activeDraft = nextDrafts.find((item) => item.id === activeId); + if (createView === "custom" && activeDraft) { + setEditingDraftId(activeDraft.id); + setImportedDraft(activeDraft.draft); + setRuntimeUpdateTarget(activeDraft.deploymentTarget ?? null); + } + // Restore only when identity changes; later edits are already in state. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userId]); + + useEffect(() => { + if (!userId) return; + const key = activeWorkspaceDraftKey(userId); + if (createView === "custom" && editingDraftId) { + localStorage.setItem(key, editingDraftId); + } else { + localStorage.removeItem(key); + } + }, [createView, editingDraftId, userId]); + + const refreshAgentLibrary = useCallback(async () => { + setAgentLibraryLoading(true); + setAgentLibraryError(""); + try { + const runtimes: CloudRuntime[] = []; + let nextToken = ""; + do { + const page = await getRuntimes({ + scope: "mine", + region: "all", + pageSize: 100, + nextToken, + }); + runtimes.push(...page.runtimes); + nextToken = page.nextToken; + } while (nextToken && runtimes.length < 2000); + + setLibraryRuntimeIds(new Set(runtimes.map((runtime) => runtime.runtimeId))); + const failures: string[] = []; + for (const runtime of runtimes) { + try { + await connectRuntime( + runtime.runtimeId, + runtime.name, + runtime.region, + runtime.currentVersion, + ); + } catch { + failures.push(runtime.name); + } + } + setConnections(loadConnections()); + if (failures.length > 0) { + setAgentLibraryError( + `${failures.length} 个 Runtime 暂时无法读取,请稍后重试。`, + ); + } + } catch (cause) { + setAgentLibraryError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setAgentLibraryLoading(false); + } + }, []); + // Placeholder: persisting/registering the created agent is a follow-up. function onCreate(draft: AgentDraft) { console.log("create agent draft:", draft); @@ -1003,6 +1146,44 @@ export default function App() { el.scrollTop = el.scrollHeight; }, []); + const openDeploymentDetail = useCallback((task: DeploymentTaskUpdate) => { + setCreateView(null); + setAddMenu(false); + setManageAgents(true); + setFocusedWorkspaceAgentId(""); + setFocusedDeploymentTaskId(task.id); + setError(""); + }, []); + + const finishDeployment = useCallback( + async (result: import("./ui/ProjectPreview").DeployResult) => { + if (!result.runtimeId) throw new Error("部署完成,但未返回 Runtime ID。"); + const fallbackRegion = runtimeUpdateTarget?.region ?? "cn-beijing"; + const agentId = await connectRuntime( + result.runtimeId, + result.agentName, + result.region ?? fallbackRegion, + result.version, + ); + setConnections(loadConnections()); + setLibraryRuntimeIds((current) => { + const next = new Set(current ?? []); + next.add(result.runtimeId!); + return next; + }); + setRuntimeUpdateTarget(null); + removeWorkspaceDraft(editingDraftId); + setEditingDraftId(""); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(agentId); + setCreateView(null); + setManageAgents(true); + setAppName(agentId); + setAgentInfoRefreshKey((value) => value + 1); + }, + [editingDraftId, removeWorkspaceDraft, runtimeUpdateTarget], + ); + // Resolve SSO identity first; it provides the ADK user_id. const resolveAuth = useCallback(() => { setAuthError(null); @@ -1172,17 +1353,19 @@ export default function App() { // Restore the last-used agent; otherwise land on a known-good default // (prefer a servable, conversational agent — numbered examples like // 01_quickstart are standalone scripts with no root_agent and can't load). - // Cloud mode: nothing is selected by default — the user picks a runtime - // each session (the sidebar shows the red "请选择 Agent" prompt until then). + const saved = localStorage.getItem(LS.app); + const remoteIds = connections.flatMap((connection) => + connection.apps.map((app) => remoteAppId(connection.id, app)), + ); + // Cloud mode restores the last connected Runtime when it is still + // registered locally. A missing or removed connection stays unselected. if (agentsSource === "cloud") { - setAppName(""); + setAppName(saved && remoteIds.includes(saved) ? saved : ""); return; } // Local mode: restore the last-used agent, else a known-good default // (prefer a servable, conversational agent — numbered examples like // 01_quickstart are standalone scripts with no root_agent and can't load). - const saved = localStorage.getItem(LS.app); - const remoteIds = connections.flatMap((c) => c.apps.map((a) => remoteAppId(c.id, a))); const valid = saved && (list.includes(saved) || remoteIds.includes(saved)); const fallback = ["web_search_agent", "web_demo"].find((a) => list.includes(a)) ?? @@ -1196,7 +1379,7 @@ export default function App() { // Persist the current view/agent/session so a refresh restores them. useEffect(() => { if (appName) localStorage.setItem(LS.app, appName); - }, [appName]); + }, [agentInfoRefreshKey, appName]); useEffect(() => { let cancelled = false; setSessionCapabilities(null); @@ -1231,6 +1414,7 @@ export default function App() { useEffect(() => { let cancelled = false; setAgentInfo(null); + setAgentInfoAgentId(""); setInvocation(emptyInvocation()); if (!appName) { setCapabilitiesLoading(false); @@ -1239,10 +1423,16 @@ export default function App() { setCapabilitiesLoading(true); getAgentInfo(appName) .then((info) => { - if (!cancelled) setAgentInfo(info); + if (!cancelled) { + setAgentInfo(info); + setAgentInfoAgentId(appName); + } }) .catch(() => { - if (!cancelled) setAgentInfo(null); + if (!cancelled) { + setAgentInfo(null); + setAgentInfoAgentId(""); + } }) .finally(() => { if (!cancelled) setCapabilitiesLoading(false); @@ -1250,7 +1440,7 @@ export default function App() { return () => { cancelled = true; }; - }, [appName]); + }, [agentInfoRefreshKey, appName]); useEffect(() => { if (!access) return; localStorage.setItem( @@ -1951,8 +2141,13 @@ export default function App() { const visibleCreateView = canCreateAgents ? createView : null; const showAddMenu = canCreateAgents && addMenu; const showAddAgent = canCreateAgents && addAgent; - const showManageAgents = canManageAgents && manageAgents; + const showManageAgents = manageAgents; const agentEntries = buildAgentEntries(apps, connections); + const workspaceAgentEntries = agentEntries.filter( + (entry) => + entry.runtimeId && + (libraryRuntimeIds === null || libraryRuntimeIds.has(entry.runtimeId)), + ); const labelOf = (id: string) => agentEntries.find((e) => e.id === id)?.label ?? id; // The runtime backing the current selection (if it's a cloud runtime app) — // drives the picker's side detail panel. @@ -2042,23 +2237,15 @@ export default function App() { } }; - // Selecting an agent (from the sidebar picker) starts a fresh chat; any + // Selecting an agent starts a fresh chat; any // background stream keeps persisting to its own (old) session. const selectAgent = (id: string) => { setConnections(loadConnections()); viewSidRef.current = ""; setSessionId(""); + if (id === appName) setAgentInfoRefreshKey((value) => value + 1); setAppName(id); }; - const connectManagedRuntime = async (runtime: ManagedRuntime) => { - const agentId = await connectRuntime( - runtime.runtimeId, - runtime.name, - runtime.region, - ); - selectAgent(agentId); - }; - return (
{ - if (!canManageAgents) { - setError("当前账号没有管理 Agent 的权限。"); - return; - } if (sandboxSession) exitSandboxSession(); viewSidRef.current = ""; setSessionId(""); @@ -2152,6 +2335,7 @@ export default function App() { setSearchView(false); setManageAgents(true); setError(""); + void refreshAgentLibrary(); }} onPickSession={(id) => { setCreateView(null); @@ -2284,8 +2468,7 @@ export default function App() { showModeSelector={ !sandboxSession && turns.length === 0 && - skillJob === null && - canCreateAgents + skillJob === null } temporaryEnabled={newChatCapabilities.temporaryEnabled} skillCreateEnabled={newChatCapabilities.skillCreateEnabled} @@ -2336,7 +2519,7 @@ export default function App() { : searchView ? "搜索" : showManageAgents - ? "管理 Agent" + ? "智能体" : visibleCreateView ? undefined : conversationTitle @@ -2406,9 +2589,77 @@ export default function App() { )} {showManageAgents ? ( - void refreshAgentLibrary()} + onSelectAgent={selectAgent} + onCreateAgent={() => { + if (!canCreateAgents) { + setError("当前账号没有添加 Agent 的权限。"); + return; + } + setManageAgents(false); + setAddMenu(false); + setImportedDraft(null); + setRuntimeUpdateTarget(null); + setEditingDraftId(`draft-${Date.now().toString(36)}`); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setCreateView("custom"); + setError(""); + }} + onUpdateAgent={(nextDraft) => { + if (!canManageAgents && !canCreateAgents) { + setError("当前账号没有管理 Agent 的权限。"); + return; + } + if (!currentConn?.runtimeId) { + setError("仅支持更新已部署的云端智能体。"); + return; + } + setManageAgents(false); + setImportedDraft(nextDraft); + const nextDraftId = `runtime-${currentConn.runtimeId}`; + setEditingDraftId(nextDraftId); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setRuntimeUpdateTarget({ + runtimeId: currentConn.runtimeId, + name: currentConn.name, + region: currentConn.region ?? "cn-beijing", + currentVersion: currentConn.currentVersion, + }); + saveWorkspaceDraft(nextDraftId, nextDraft, { + runtimeId: currentConn.runtimeId, + name: currentConn.name, + region: currentConn.region ?? "cn-beijing", + currentVersion: currentConn.currentVersion, + }); + setCreateView("custom"); + setError(""); + }} + onEditDraft={(item) => { + setManageAgents(false); + setImportedDraft(item.draft); + setEditingDraftId(item.id); + setRuntimeUpdateTarget(item.deploymentTarget ?? null); + setFocusedDeploymentTaskId(""); + setFocusedWorkspaceAgentId(""); + setCreateView("custom"); + setError(""); + }} /> ) : showAddMenu ? ( ) : visibleCreateView === "custom" ? ( setCreateView("menu")} onCreate={onCreate} onAgentAdded={onAgentAdded} features={features} + onDraftChange={(nextDraft) => + saveWorkspaceDraft( + editingDraftId, + nextDraft, + runtimeUpdateTarget ?? undefined, + ) + } onDeploymentTaskChange={updateDeploymentTask} + onDeploymentStarted={openDeploymentDetail} + deploymentTarget={runtimeUpdateTarget ?? undefined} + onDeploymentComplete={finishDeployment} /> ) : visibleCreateView === "template" ? ( setCreateView("menu")} onCreate={onCreate} /> diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index c6eac674..9c058170 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -583,6 +583,7 @@ export interface AgentNode { id?: string; name: string; description: string; + instruction?: string; type: AgentNodeType; model: string; tools: string[]; @@ -627,6 +628,8 @@ export interface AgentInfo { model: string; tools: string[]; skills: AgentSkill[]; + /** False when an older Agent Server omits Skill introspection entirely. */ + skillsPreviewSupported: boolean; subAgents: string[]; /** Optional for compatibility with Agent Servers released before this field. */ components?: AgentComponent[]; @@ -634,6 +637,8 @@ export interface AgentInfo { searchSources?: AgentSearchCapability[]; /** Recursive typed tree; only the local server provides it. */ graph?: AgentNode; + /** Complete sanitized builder state exposed by newly generated Agents. */ + draft?: AgentDraft; } export interface SessionCapabilityItem { @@ -838,17 +843,30 @@ async function fetchAgentInfo(app: string, ep: AdkEndpoint): Promise const res = await apiFetch(`/web/agent-info/${app}`, {}, ep); if (!res.ok) throw new Error(`agent-info failed: ${res.status}`); const info = (await res.json()) as Partial; + if (!info.draft) { + try { + const draftRes = await apiFetch(`/web/agent-draft/${app}`, {}, ep); + if (draftRes.ok) { + const payload = (await draftRes.json()) as { draft?: AgentDraft }; + info.draft = payload.draft; + } + } catch { + // Older or non-Studio Agents do not expose editable builder metadata. + } + } return { name: info.name ?? app, description: info.description ?? "", type: info.type, model: info.model ?? "", tools: info.tools ?? [], + skillsPreviewSupported: Array.isArray(info.skills), skills: info.skills ?? [], subAgents: info.subAgents ?? [], components: info.components ?? [], searchSources: info.searchSources ?? [], graph: info.graph, + draft: info.draft, }; } @@ -1042,6 +1060,7 @@ export interface DeployAgentkitResult { runtimeId?: string; consoleUrl?: string; region?: string; + version?: number | null; feishuChannel?: { enabled: boolean; transport: string; @@ -1085,6 +1104,8 @@ export async function deployAgentkitProject( }, opts?: { taskId?: string; + runtimeId?: string; + description?: string; onStage?: (s: DeployStage) => void; im?: { feishu?: { @@ -1116,6 +1137,8 @@ export async function deployAgentkitProject( files, config, taskId, + runtimeId: opts?.runtimeId, + description: opts?.description, im: opts?.im, envs: opts?.envs, }), @@ -1167,6 +1190,7 @@ export async function deployAgentkitProject( runtimeId: final.runtimeId, consoleUrl: final.consoleUrl, region: final.region, + version: final.version, feishuChannel: final.feishuChannel, }; } @@ -1194,6 +1218,7 @@ export interface ManagedRuntime { createdAt: string; author?: string; region: string; + currentVersion?: number | null; } /** List AgentKit runtimes the server authorizes this user to manage. */ @@ -1407,6 +1432,8 @@ export interface CloudRuntime { status: string; region: string; author: string; + createdAt?: string; + currentVersion?: number | null; /** True when this runtime was deployed by the current user (veadk:author). */ isMine: boolean; } @@ -1503,6 +1530,7 @@ export interface RuntimeDetail { mcpToolsetId: string; artifactUrl: string; artifactType: string; + networkTypes: string[]; } /** Fetch a runtime's control-plane detail (config/status/envs). */ diff --git a/frontend/src/adk/connections.ts b/frontend/src/adk/connections.ts index fb2bc2ea..c124e8f6 100644 --- a/frontend/src/adk/connections.ts +++ b/frontend/src/adk/connections.ts @@ -20,6 +20,7 @@ export interface RemoteConnection { * set, `base`/`apiKey` are unused and the apikey stays server-side. */ runtimeId?: string; region?: string; + currentVersion?: number | null; apps: string[]; /** Optional app ID -> friendly name mapping (e.g., "a_1" -> "a_1-4zkzsezc") */ appLabels?: Record; @@ -32,6 +33,9 @@ export interface AgentEntry { app: string; // real ADK app name remote: boolean; host?: string; // remote host, for display + runtimeId?: string; + region?: string; + currentVersion?: number | null; } const STORAGE_KEY = "veadk_agentkit_connections"; @@ -91,6 +95,7 @@ export function addRuntimeConnection( region: string, apps: string[], appLabels?: Record, + currentVersion?: number | null, ): RemoteConnection { const conn: RemoteConnection = { id: `rt_${runtimeId}`, @@ -99,6 +104,7 @@ export function addRuntimeConnection( region, apps, appLabels, + currentVersion, }; const list = [...loadConnections().filter((c) => c.runtimeId !== runtimeId), conn]; persist(list); @@ -111,6 +117,7 @@ export async function connectRuntime( runtimeId: string, name: string, region: string, + currentVersion?: number | null, ): Promise { let apps: string[] | null; try { @@ -126,7 +133,14 @@ export async function connectRuntime( throw new Error("该 Runtime 暂不支持连接,请确认服务已正常运行。"); } const labels = Object.fromEntries(apps.map((app) => [app, name])); - const connection = addRuntimeConnection(runtimeId, name, region, apps, labels); + const connection = addRuntimeConnection( + runtimeId, + name, + region, + apps, + labels, + currentVersion, + ); return remoteAppId(connection.id, apps[0]); } @@ -188,6 +202,9 @@ export function buildAgentEntries( app, remote: true, host: c.runtimeId ? c.name : hostOf(c.base ?? ""), + runtimeId: c.runtimeId, + region: c.region, + currentVersion: c.currentVersion, }; }), ); diff --git a/frontend/src/assets/feishu-logo.svg b/frontend/src/assets/feishu-logo.svg new file mode 100644 index 00000000..4b305987 --- /dev/null +++ b/frontend/src/assets/feishu-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/create/AgentBuildCanvas.css b/frontend/src/create/AgentBuildCanvas.css new file mode 100644 index 00000000..7df1a276 --- /dev/null +++ b/frontend/src/create/AgentBuildCanvas.css @@ -0,0 +1,720 @@ +.abc-root { + flex: 0 1 52%; + min-width: 460px; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border-right: 1px solid hsl(var(--border)); + background: hsl(var(--cw-workspace-warm)); +} + +.abc-head { + position: relative; + z-index: 2; + flex: 0 0 auto; + height: var(--cw-workbench-toolbar-height); + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 10px 16px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--panel) / 0.92); + backdrop-filter: blur(14px); +} + +.abc-head-copy { + min-width: 0; +} + +.abc-head-copy > strong { + color: hsl(var(--cw-workspace-ink)); + font-size: 15px; + font-weight: 680; + letter-spacing: -0.025em; +} + +.abc-head-actions { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 6px; +} + +.abc-head-actions button { + min-height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + padding: 0 10px; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 600; + transition: border-color 140ms ease, background-color 140ms ease; +} + +.abc-head-actions button:hover:not(:disabled) { + border-color: hsl(var(--cw-workspace-accent) / 0.35); + background: hsl(var(--cw-workspace-accent-soft)); +} + +.abc-head-actions button:focus-visible { + outline: 2px solid hsl(var(--cw-workspace-accent) / 0.5); + outline-offset: 2px; +} + +.abc-head-actions button:disabled { + cursor: not-allowed; + opacity: 0.38; +} + +.abc-canvas { + flex: 1; + min-height: 0; + background: + radial-gradient(circle at 52% 44%, hsl(32 44% 62% / 0.07), transparent 42%), + hsl(42 32% 97%); +} + +.abc-canvas .react-flow__pane { + cursor: grab; +} + +.abc-canvas .react-flow__pane:active { + cursor: grabbing; +} + +.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: 1px solid hsl(var(--abc-type-border)); + border-radius: 13px; + background: hsl(var(--panel)); + box-shadow: 0 10px 30px hsl(var(--foreground) / 0.055); + 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.48); + box-shadow: 0 13px 34px hsl(var(--foreground) / 0.08); +} + +.abc-node.is-selected { + border-color: hsl(var(--abc-type-tone) / 0.86); + box-shadow: + 0 0 0 3px hsl(var(--abc-type-tone) / 0.1), + 0 14px 38px hsl(var(--foreground) / 0.09); +} + +.abc-node.is-llm { + grid-template-columns: minmax(0, 1fr); + background: hsl(var(--panel)); +} + +.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-canvas .react-flow__node-group { + padding: 0; + border: 0; + border-radius: 18px; + background: transparent; +} + +.abc-group { + --abc-type-tone: 213 40% 40%; + --abc-type-soft: 214 45% 96%; + --abc-type-border: 213 32% 62%; + position: relative; + width: 100%; + height: 100%; + box-sizing: border-box; + overflow: hidden; + border: 1.5px solid hsl(var(--abc-type-border) / 0.72); + 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; +} + +.abc-group.is-selected { + border-color: hsl(var(--abc-type-tone) / 0.86); + box-shadow: + 0 0 0 4px hsl(var(--abc-type-tone) / 0.1), + 0 16px 46px hsl(var(--foreground) / 0.08); +} + +.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-group.is-sequential { + --abc-type-tone: 213 40% 40%; + --abc-type-soft: 214 45% 96%; + --abc-type-border: 213 32% 62%; +} + +.abc-group.is-loop { + --abc-type-tone: 151 34% 34%; + --abc-type-soft: 148 32% 94%; + --abc-type-border: 151 28% 55%; +} + +.abc-group-head { + position: relative; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + padding: 10px 16px; + border-bottom: 1px solid hsl(var(--border) / 0.75); +} + +.abc-group-head > span:first-child { + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + text-align: center; +} + +.abc-group-head strong { + color: hsl(var(--abc-type-tone)); + font-size: 13px; + letter-spacing: -0.02em; +} + +.abc-group-head small { + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 9.5px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.abc-group-head em { + position: absolute; + top: 50%; + right: 16px; + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--background) / 0.82); + color: hsl(var(--muted-foreground)); + font-size: 9px; + font-style: normal; + font-weight: 600; + transform: translateY(-50%); +} + +.abc-group-add { + position: absolute; + right: 24px; + bottom: 24px; + left: 24px; + 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; + transition: + border-color 150ms ease, + background-color 150ms ease, + color 150ms ease; +} + +.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-group-add:focus-visible { + outline: 2px solid hsl(var(--abc-type-tone) / 0.5); + outline-offset: 2px; +} + +.abc-group-add svg { + width: 14px; + height: 14px; +} + +.abc-node.is-contained-in-parallel .abc-handle { + opacity: 0; +} + +.abc-node-icon { + width: 38px; + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 10px; + background: hsl(var(--abc-type-soft)); + color: hsl(var(--abc-type-tone)); +} + +.abc-node-icon svg { + width: 17px; + height: 17px; +} + +.abc-node-copy { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; + padding-right: 18px; +} + +.abc-node-delete { + position: absolute; + z-index: 3; + top: 7px; + right: 7px; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--panel) / 0.96); + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.08); + color: hsl(var(--muted-foreground)); + cursor: pointer; + opacity: 0; + pointer-events: none; + transform: translateY(-2px) scale(0.92); + transition: + opacity 120ms ease, + transform 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +.abc-node:hover > .abc-node-delete, +.abc-node:focus-within > .abc-node-delete, +.abc-group:hover > .abc-node-delete, +.abc-group:focus-within > .abc-node-delete, +.abc-node-delete:focus-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0) scale(1); +} + +.abc-node-delete:hover { + border-color: hsl(var(--destructive) / 0.32); + color: hsl(var(--destructive)); +} + +.abc-node-delete:focus-visible { + outline: 2px solid hsl(var(--destructive) / 0.34); + outline-offset: 2px; +} + +.abc-node-delete svg { + width: 12px; + height: 12px; +} + +.abc-group > .abc-node-delete { + top: 19px; + right: 12px; +} + +.abc-group > .abc-node-delete + .abc-handle { + z-index: 4; +} + +.abc-group:has(> .abc-node-delete) .abc-group-head em { + right: 46px; +} + +.abc-loop-handle { + left: 50% !important; + opacity: 0; + 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-node-meta small { + color: hsl(var(--muted-foreground)); + font-size: 8.5px; + font-weight: 550; + letter-spacing: 0; +} + +.abc-node-copy > strong { + overflow: hidden; + font-size: 13px; + letter-spacing: -0.015em; + text-overflow: ellipsis; + 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-terminal { + width: 96px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid hsl(var(--border)); + border-radius: 999px; + background: hsl(var(--cw-workspace-ink)); + box-shadow: 0 8px 24px hsl(var(--foreground) / 0.1); + color: white; + font-size: 10.5px; + font-weight: 650; + letter-spacing: 0.03em; +} + +.abc-handle { + width: 7px !important; + height: 7px !important; + border: 2px solid hsl(var(--background)) !important; + background: hsl(220 9% 38%) !important; +} + +.abc-group.is-sequential > .abc-handle { + background: hsl(213 40% 40%) !important; +} + +.abc-group.is-parallel > .abc-handle { + background: hsl(40 43% 38%) !important; +} + +.abc-group.is-loop > .abc-handle, +.abc-node.is-contained-in-loop .abc-loop-handle { + background: hsl(151 34% 34%) !important; +} + +.abc-canvas .react-flow__edge-path { + transition: stroke-width 120ms ease; +} + +.abc-canvas .react-flow__edge:hover .react-flow__edge-path { + stroke-width: 2.2; +} + +.abc-edge-tools { + position: absolute; + z-index: 1002; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 3px; + padding: 0; + border-radius: 999px; + pointer-events: all; +} + +.abc-canvas .react-flow__edgelabel-renderer { + z-index: 1002; +} + +.abc-edge-hover-path { + fill: none; + stroke: transparent; + stroke-width: 22px; + pointer-events: stroke; +} + +.abc-edge-label { + position: absolute; + bottom: calc(100% + 1px); + left: 50%; + padding: 2px 5px; + border-radius: 5px; + background: hsl(var(--background) / 0.92); + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 600; + transform: translateX(-50%); + white-space: nowrap; +} + +.abc-edge-add { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid hsl(var(--cw-workspace-accent) / 0.28); + border-radius: 50%; + background: hsl(var(--background)); + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.1); + color: hsl(var(--cw-workspace-accent)); + cursor: pointer; + opacity: 0; + transform: scale(0.82); + transition: + opacity 140ms ease, + transform 140ms ease, + border-color 140ms ease; +} + +.abc-edge-tools.is-visible .abc-edge-add, +.abc-edge-tools:hover .abc-edge-add, +.abc-edge-add:focus-visible { + border-color: hsl(var(--cw-workspace-accent) / 0.68); + opacity: 1; + transform: scale(1); +} + +.abc-edge-add.is-boundary:hover { + border-color: hsl(var(--cw-workspace-accent) / 0.82); + box-shadow: + 0 0 0 3px hsl(var(--cw-workspace-accent) / 0.09), + 0 6px 16px hsl(var(--foreground) / 0.12); +} + +.abc-edge-add:focus-visible { + outline: 2px solid hsl(var(--cw-workspace-accent) / 0.5); + outline-offset: 2px; +} + +.abc-edge-add svg { + width: 11px; + height: 11px; +} + +@media (hover: none) { + .abc-edge-add { + opacity: 0.88; + transform: scale(1); + } + + .abc-node-delete { + opacity: 1; + pointer-events: auto; + transform: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .abc-node-delete, + .abc-edge-add { + transition: none; + } +} + +.abc-canvas .react-flow__controls { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 9px; + box-shadow: 0 8px 24px hsl(var(--foreground) / 0.08); +} + +.abc-canvas .react-flow__controls-button { + border-bottom-color: hsl(var(--border)); + background: hsl(var(--panel)); + color: hsl(var(--foreground)); +} + +.abc-minimap { + width: 168px !important; + height: 104px !important; + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 9px; + background: hsl(var(--panel)) !important; + box-shadow: 0 8px 24px hsl(var(--foreground) / 0.06); +} + +.abc-minimap-node .abc-minimap-shell, +.abc-minimap-node .abc-minimap-icon-mark, +.abc-minimap-node .abc-minimap-group-divider { + vector-effect: non-scaling-stroke; +} + +.abc-minimap-node-agent .abc-minimap-shell { + fill: hsl(var(--panel)); + stroke: hsl(220 9% 38% / 0.72); + stroke-width: 1.4px; +} + +.abc-minimap-node-agent.is-a2a .abc-minimap-shell { + fill: hsl(211 48% 96%); + stroke: hsl(202 42% 48% / 0.78); +} + +.abc-minimap-agent-icon { + fill: hsl(214 20% 94%); +} + +.abc-minimap-node-agent.is-a2a .abc-minimap-agent-icon { + fill: hsl(202 46% 90%); +} + +.abc-minimap-icon-mark { + fill: none; + stroke: hsl(213 18% 38%); + stroke-width: 1.25px; +} + +.abc-minimap-icon-eye { + fill: hsl(213 18% 38%); +} + +.abc-minimap-copy-line { + fill: hsl(var(--muted-foreground) / 0.34); +} + +.abc-minimap-copy-line.is-primary { + fill: hsl(var(--foreground) / 0.7); +} + +.abc-minimap-node-terminal .abc-minimap-shell { + fill: hsl(var(--cw-workspace-ink)); + stroke: hsl(var(--panel)); + stroke-width: 1.5px; + vector-effect: non-scaling-stroke; +} + +.abc-minimap-terminal-dot { + fill: hsl(var(--panel) / 0.82); +} + +.abc-minimap-node-group .abc-minimap-shell { + fill: hsl(var(--panel) / 0.32); + stroke: hsl(213 40% 40% / 0.78); + stroke-width: 1.5px; +} + +.abc-minimap-node-group.is-parallel .abc-minimap-shell { + stroke: hsl(40 43% 38% / 0.82); +} + +.abc-minimap-node-group.is-sequential .abc-minimap-shell { + stroke: hsl(213 40% 40% / 0.82); +} + +.abc-minimap-node-group.is-loop .abc-minimap-shell { + stroke: hsl(151 34% 34% / 0.84); +} + +.abc-minimap-group-divider { + stroke: hsl(var(--border)); + stroke-width: 1px; +} + +.abc-minimap-group-title { + fill: hsl(var(--muted-foreground) / 0.42); +} + +.abc-minimap-node.is-selected .abc-minimap-shell { + stroke-width: 2.5px; +} + +.abc-minimap-node-agent.is-selected .abc-minimap-shell { + stroke: hsl(220 9% 20%); +} + +.abc-minimap-node-group.is-sequential.is-selected .abc-minimap-shell { + stroke: hsl(213 40% 32%); +} + +.abc-minimap-node-group.is-parallel.is-selected .abc-minimap-shell { + stroke: hsl(40 43% 30%); +} + +.abc-minimap-node-group.is-loop.is-selected .abc-minimap-shell { + stroke: hsl(151 34% 27%); +} + +@media (max-width: 1080px) { + .abc-root { + min-width: 360px; + } + +} + +@media (max-width: 860px) { + .abc-root { + flex: none; + width: 100%; + min-width: 0; + height: 480px; + border-right: 0; + border-bottom: 1px solid hsl(var(--border)); + } + + .abc-head { + min-height: 56px; + padding: 9px 12px; + } + + .abc-minimap { + display: none; + } + +} + +@media (max-width: 520px) { + .abc-root { + height: 430px; + } + + .abc-head-actions button { + min-height: 30px; + padding-inline: 8px; + } +} diff --git a/frontend/src/create/AgentBuildCanvas.tsx b/frontend/src/create/AgentBuildCanvas.tsx new file mode 100644 index 00000000..7cdd21f3 --- /dev/null +++ b/frontend/src/create/AgentBuildCanvas.tsx @@ -0,0 +1,1148 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Background, + BaseEdge, + Controls, + EdgeLabelRenderer, + getSmoothStepPath, + Handle, + MarkerType, + MiniMap, + Position, + ReactFlow, + ReactFlowProvider, + useEdgesState, + useNodesState, + useReactFlow, + type Edge, + type EdgeProps, + type MiniMapNodeProps, + type Node, + type NodeProps, +} from "@xyflow/react"; +import dagre from "@dagrejs/dagre"; +import { + ArrowRightLeft, + Bot, + Globe, + ListOrdered, + Plus, + Repeat, + Trash2, +} from "lucide-react"; +import type { AgentDraft } from "./types"; +import "@xyflow/react/dist/style.css"; +import "./AgentBuildCanvas.css"; + +type NodePath = number[]; +type AgentType = NonNullable; + +const PATTERN_COPY: Record< + AgentType, + { label: string; description: string; icon: typeof Bot } +> = { + 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"; + path?: NodePath; + agent?: AgentDraft; + title: string; + pattern?: AgentType; + description?: string; + childCount?: number; + containedIn?: AgentType; + layoutWidth?: number; + layoutHeight?: number; +}; + +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 GROUP_MIN_WIDTH = 310; +const GROUP_PADDING = 24; +const GROUP_GAP = 40; +const GROUP_ADD_HEIGHT = 40; +const GROUP_ADD_GAP = 18; +const LOOP_EDGE_SPACE = 58; +const MINIMAP_ENABLED = false; + +const isContainerType = (type: AgentType) => + type === "sequential" || type === "parallel" || type === "loop"; + +function measureAgent(agent: AgentDraft): { width: number; height: number } { + const type = agent.agentType ?? "llm"; + if (!isContainerType(type)) { + return { width: NODE_WIDTH, height: NODE_HEIGHT }; + } + const sizes = agent.subAgents.map(measureAgent); + const widestChild = sizes.length + ? Math.max(...sizes.map((size) => size.width)) + : 0; + const tallestChild = sizes.length + ? Math.max(...sizes.map((size) => size.height)) + : 0; + const addZoneGap = sizes.length + ? type === "loop" + ? LOOP_EDGE_SPACE + : GROUP_ADD_GAP + : 0; + if (type === "parallel") { + return { + width: Math.max( + GROUP_MIN_WIDTH, + widestChild + GROUP_PADDING * 2, + ), + height: + GROUP_HEADER_HEIGHT + + GROUP_PADDING + + sizes.reduce((sum, size) => sum + size.height, 0) + + GROUP_GAP * Math.max(0, sizes.length - 1) + + addZoneGap + + GROUP_ADD_HEIGHT + + GROUP_PADDING, + }; + } + return { + width: Math.max( + GROUP_MIN_WIDTH, + sizes.reduce((sum, size) => sum + size.width, 0) + + GROUP_GAP * Math.max(0, sizes.length - 1) + + GROUP_PADDING * 2, + ), + height: + GROUP_HEADER_HEIGHT + + GROUP_PADDING + + tallestChild + + addZoneGap + + GROUP_ADD_HEIGHT + + GROUP_PADDING, + }; +} + +function pathId(path: NodePath): string { + return path.length === 0 ? "agent-root" : `agent-${path.join("-")}`; +} + +function samePath(a: NodePath, b: NodePath): boolean { + return a.length === b.length && a.every((value, index) => value === b[index]); +} + +function structureKey(root: AgentDraft): string { + const visit = (node: AgentDraft): unknown => [ + node.agentType ?? "llm", + node.subAgents.map(visit), + ]; + return JSON.stringify(visit(root)); +} + +type CanvasEdgeData = { + insert?: { parentPath: NodePath; index: number }; + rootInsert?: "before" | "after"; + loop?: boolean; + tone?: AgentType; +}; + +type CanvasEdge = Edge; + +function makeEdge( + source: string, + target: string, + label?: string, + options?: { + loop?: boolean; + tone?: AgentType; + insert?: CanvasEdgeData["insert"]; + rootInsert?: CanvasEdgeData["rootInsert"]; + }, +): CanvasEdge { + const edgeColor = + options?.tone === "sequential" + ? "hsl(213 40% 40%)" + : 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, + label, + type: "insertStep", + data: options + ? { + insert: options.insert, + rootInsert: options.rootInsert, + loop: options.loop, + tone: options.tone, + } + : undefined, + animated: options?.loop, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 16, + height: 16, + color: edgeColor, + }, + style: { + stroke: edgeColor, + strokeWidth: 1.5, + }, + labelStyle: { + fill: "hsl(215 14% 42%)", + fontSize: 10, + fontWeight: 600, + }, + labelBgStyle: { + fill: "hsl(var(--background))", + fillOpacity: 0.92, + }, + }; +} + +function buildCanvasGraph(root: AgentDraft): { + nodes: CanvasNode[]; + edges: CanvasEdge[]; +} { + const nodes: CanvasNode[] = [ + { + id: "terminal-input", + type: "terminal", + position: { x: 0, y: 0 }, + data: { kind: "terminal", title: "用户请求" }, + selectable: false, + draggable: false, + }, + { + id: "terminal-output", + type: "terminal", + position: { x: 0, y: 0 }, + data: { kind: "terminal", title: "最终回复" }, + selectable: false, + draggable: false, + }, + ]; + const edges: CanvasEdge[] = []; + + function addContainedNode( + agent: AgentDraft, + path: NodePath, + parentId: string, + position: { x: number; y: number }, + containedIn: AgentType, + ): string { + const type = agent.agentType ?? "llm"; + const id = pathId(path); + if (isContainerType(type)) { + addGroupNode(agent, path, parentId, position, containedIn); + return id; + } + nodes.push({ + id, + type: "agent", + parentId, + extent: "parent", + position, + data: { + kind: "agent", + path, + agent, + title: + type === "a2a" + ? "远程智能体" + : agent.name.trim() || (path.length === 0 ? "主 Agent" : "未命名步骤"), + pattern: type, + description: agent.description.trim() || PATTERN_COPY[type].description, + childCount: agent.subAgents.length, + containedIn, + }, + }); + return id; + } + + function addGroupNode( + agent: AgentDraft, + path: NodePath, + parentId?: string, + position = { x: 0, y: 0 }, + containedIn?: AgentType, + ): string { + const type = agent.agentType ?? "sequential"; + const id = pathId(path); + const size = measureAgent(agent); + nodes.push({ + id, + type: "group", + parentId, + extent: parentId ? "parent" : undefined, + position, + style: { width: size.width, height: size.height }, + data: { + kind: "agent", + path, + agent, + title: agent.name.trim() || PATTERN_COPY[type].label, + pattern: type, + description: PATTERN_COPY[type].description, + childCount: agent.subAgents.length, + containedIn, + layoutWidth: size.width, + layoutHeight: size.height, + }, + }); + + const childSizes = agent.subAgents.map(measureAgent); + let cursor = GROUP_PADDING; + const childIds = agent.subAgents.map((child, index) => { + const childSize = childSizes[index]; + const childPosition = + type === "parallel" + ? { + x: (size.width - childSize.width) / 2, + y: GROUP_HEADER_HEIGHT + cursor, + } + : { x: cursor, y: GROUP_HEADER_HEIGHT + GROUP_PADDING }; + cursor += + (type === "parallel" ? childSize.height : childSize.width) + GROUP_GAP; + return addContainedNode( + child, + [...path, index], + id, + childPosition, + type, + ); + }); + + 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, + insert: { parentPath: path, index: index + 1 }, + }), + ); + } + if (type === "loop" && childIds.length > 1) { + edges.push( + makeEdge(childIds[childIds.length - 1], childIds[0], "继续循环", { + loop: true, + tone: "loop", + }), + ); + } + } + return id; + } + + const addTopLevelNode = (agent: AgentDraft, path: NodePath): string[] => { + const type = agent.agentType ?? "llm"; + const id = pathId(path); + if (isContainerType(type)) { + addGroupNode(agent, path); + return [id]; + } + nodes.push({ + id, + type: "agent", + position: { x: 0, y: 0 }, + data: { + kind: "agent", + path, + agent, + title: + type === "a2a" + ? "远程智能体" + : agent.name.trim() || (path.length === 0 ? "主 Agent" : "未命名步骤"), + pattern: type, + description: agent.description.trim() || PATTERN_COPY[type].description, + childCount: agent.subAgents.length, + }, + }); + 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; + }; + + if (root.agentType === "sequential" && root.subAgents.length > 0) { + let previousExits = ["terminal-input"]; + root.subAgents.forEach((child, index) => { + const childPath = [index]; + const childId = pathId(childPath); + const exits = addTopLevelNode(child, childPath); + previousExits.forEach((source, sourceIndex) => { + edges.push( + makeEdge(source, childId, index === 0 ? undefined : "然后", { + rootInsert: index === 0 ? "before" : undefined, + insert: + index > 0 && sourceIndex === 0 + ? { parentPath: [], index } + : undefined, + }), + ); + }); + previousExits = exits; + }); + previousExits.forEach((source, index) => { + edges.push( + makeEdge( + source, + "terminal-output", + undefined, + index === 0 ? { rootInsert: "after" } : undefined, + ), + ); + }); + } else { + const rootId = pathId([]); + const exits = addTopLevelNode(root, []); + edges.push( + makeEdge("terminal-input", rootId, undefined, { rootInsert: "before" }), + ); + exits.forEach((exitId, index) => + edges.push( + makeEdge( + exitId, + "terminal-output", + undefined, + index === 0 ? { rootInsert: "after" } : undefined, + ), + ), + ); + } + return layoutGraph(nodes, edges); +} + +function layoutGraph(nodes: CanvasNode[], edges: CanvasEdge[]) { + const graph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + graph.setGraph({ + rankdir: "LR", + ranksep: 50, + nodesep: 34, + edgesep: 14, + marginx: 24, + marginy: 24, + }); + const topLevelIds = new Set( + nodes.filter((node) => !node.parentId).map((node) => node.id), + ); + nodes.filter((node) => !node.parentId).forEach((node) => { + const terminal = node.data.kind === "terminal"; + graph.setNode(node.id, { + width: terminal + ? TERMINAL_WIDTH + : node.data.layoutWidth ?? NODE_WIDTH, + height: terminal + ? TERMINAL_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)); + 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 width = terminal + ? TERMINAL_WIDTH + : node.data.layoutWidth ?? NODE_WIDTH; + const height = terminal + ? TERMINAL_HEIGHT + : node.data.layoutHeight ?? NODE_HEIGHT; + return { + ...node, + position: { x: position.x - width / 2, y: position.y - height / 2 }, + }; + }), + edges, + }; +} + +type CanvasActions = { + onAdd: (path: NodePath) => void; + onInsert: (parentPath: NodePath, index: number) => void; + onInsertRoot: (position: "before" | "after") => void; + onDelete: (path: NodePath) => void; +}; + +const CanvasActionsContext = createContext(null); + +function InsertStepEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + markerEnd, + style, + label, + data, +}: 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, + }); + return ( + <> + + {actions && (data?.insert || data?.rootInsert) && ( + setShowInsert(true)} + onPointerLeave={() => setShowInsert(false)} + /> + )} + {(label || (actions && (data?.insert || data?.rootInsert))) && ( + +
setShowInsert(true)} + onPointerLeave={() => setShowInsert(false)} + > + {label && {label}} + {actions && (data?.insert || data?.rootInsert) && ( + + )} +
+
+ )} + + ); +} + +function AgentCanvasNode({ data, selected }: NodeProps) { + const actions = useContext(CanvasActionsContext); + const type = data.pattern ?? "llm"; + const copy = PATTERN_COPY[type]; + const Icon = copy.icon; + return ( +
+ + {type !== "llm" && ( + + )} + + + {copy.label} + {!!data.childCount && {data.childCount} 个步骤} + + {data.title} + {data.description} + + {actions && data.path !== undefined && ( + + )} + + {data.containedIn === "loop" && ( + <> + + + + )} +
+ ); +} + +function AgentGroupNode({ data, selected }: NodeProps) { + const actions = useContext(CanvasActionsContext); + const type = data.pattern ?? "sequential"; + const copy = PATTERN_COPY[type]; + const addLabel = + type === "parallel" + ? "添加一个同时处理的步骤" + : type === "loop" + ? "添加循环步骤" + : "添加下一个步骤"; + return ( +
+ +
+ + {copy.label} + + {type === "parallel" + ? "框内步骤同时开始,全部完成后再继续" + : copy.description} + + + {data.childCount ?? 0} 个步骤 +
+ {actions && ( + + )} + {actions && data.path !== undefined && ( + + )} + + {data.containedIn === "loop" && ( + <> + + + + )} +
+ ); +} + +function TerminalNode({ data }: NodeProps) { + return ( +
+ + {data.title} + +
+ ); +} + +const NODE_TYPES = { + agent: AgentCanvasNode, + group: AgentGroupNode, + terminal: TerminalNode, +}; + +const EDGE_TYPES = { + insertStep: InsertStepEdge, +}; + +function CanvasMiniMapNode({ + id, + x, + y, + width, + height, + className, + selected, + onClick, +}: MiniMapNodeProps) { + const isGroup = className.includes("abc-minimap-node-group"); + const isTerminal = className.includes("abc-minimap-node-terminal"); + const isLlm = className.includes("is-llm"); + const isExternal = className.includes("is-a2a"); + const radius = Math.min(18, height / 4); + const nodeClassName = `${className}${selected ? " is-selected" : ""}`; + + if (isTerminal) { + return ( + onClick(event, id) : undefined} + > + + + + ); + } + + if (isGroup) { + const headerHeight = Math.min(42, Math.max(18, height * 0.22)); + return ( + onClick(event, id) : undefined} + > + + + + + ); + } + + const iconSize = isLlm ? 0 : Math.min(36, height * 0.5, width * 0.18); + const sidePadding = Math.max(10, width * 0.08); + const iconX = x + sidePadding; + const iconY = y + (height - iconSize) / 2; + const textX = isLlm + ? x + sidePadding + : iconX + iconSize + Math.max(8, width * 0.05); + const textWidth = Math.max(10, x + width - textX - sidePadding); + const lineHeight = Math.max(4, height * 0.08); + + return ( + onClick(event, id) : undefined} + > + + {!isLlm && } + {!isLlm && (isExternal ? ( + <> + + + + ) : ( + <> + + + + + ))} + + + + ); +} + +export interface AgentBuildCanvasProps { + draft: AgentDraft; + selectedPath: NodePath; + onSelect: (path: NodePath) => void; + onAdd: (path: NodePath) => void; + onInsert: (parentPath: NodePath, index: number) => void; + onInsertRoot: (position: "before" | "after") => void; + onDelete: (path: NodePath) => void; + onReset: () => void; + /** Show the graph without any structure-changing actions. */ + readOnly?: boolean; + /** Allow pan and zoom while remaining read-only. */ + interactivePreview?: boolean; +} + +function AgentBuildCanvasInner({ + draft, + selectedPath, + onSelect, + onAdd, + onInsert, + onInsertRoot, + onDelete, + onReset, + readOnly = false, + interactivePreview = false, +}: AgentBuildCanvasProps) { + const initialGraph = useMemo(() => buildCanvasGraph(draft), []); + const [nodes, setNodes, onNodesChange] = useNodesState( + initialGraph.nodes, + ); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialGraph.edges); + const lastStructure = useRef(structureKey(draft)); + const canvasRef = useRef(null); + const { fitView } = useReactFlow(); + const currentGraph = useMemo(() => buildCanvasGraph(draft), [draft]); + const [compactCanvas, setCompactCanvas] = useState(() => + window.matchMedia("(max-width: 860px)").matches, + ); + const fitOptions = useMemo( + () => + readOnly + ? { padding: 0.16, minZoom: 0.05, maxZoom: 0.9 } + : compactCanvas + ? { padding: 0.08, minZoom: 0.35, maxZoom: 0.9 } + : { padding: 0.14, minZoom: 0.42, maxZoom: 1.1 }, + [compactCanvas, readOnly], + ); + const fitAfterLayout = useCallback(() => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => void fitView(fitOptions)); + }); + }, [fitOptions, fitView]); + + useEffect(() => { + const query = window.matchMedia("(max-width: 860px)"); + const handleChange = (event: MediaQueryListEvent) => + setCompactCanvas(event.matches); + query.addEventListener("change", handleChange); + return () => query.removeEventListener("change", handleChange); + }, []); + + useEffect(() => { + const nextStructure = structureKey(draft); + const structureChanged = nextStructure !== lastStructure.current; + lastStructure.current = nextStructure; + setEdges(currentGraph.edges); + setNodes((current) => { + const currentPositions = new Map( + current.map((node) => [node.id, node.position] as const), + ); + return currentGraph.nodes.map((node) => ({ + ...node, + position: + !structureChanged && currentPositions.get(node.id) + ? currentPositions.get(node.id)! + : node.position, + selected: + node.data.kind === "agent" && + !!node.data.path && + samePath(node.data.path, selectedPath), + })); + }); + if (structureChanged) { + fitAfterLayout(); + } + }, [currentGraph, draft, fitAfterLayout, selectedPath, setEdges, setNodes]); + + useEffect(() => { + fitAfterLayout(); + }, [compactCanvas, fitAfterLayout]); + + useEffect(() => { + if (!readOnly || !canvasRef.current) return; + const observer = new ResizeObserver(() => fitAfterLayout()); + observer.observe(canvasRef.current); + fitAfterLayout(); + return () => observer.disconnect(); + }, [fitAfterLayout, readOnly]); + + const autoLayout = useCallback(() => { + const next = buildCanvasGraph(draft); + setNodes( + next.nodes.map((node) => ({ + ...node, + selected: + node.data.kind === "agent" && + !!node.data.path && + samePath(node.data.path, selectedPath), + })), + ); + setEdges(next.edges); + fitAfterLayout(); + }, [draft, fitAfterLayout, selectedPath, setEdges, setNodes]); + + const canvasActions = useMemo( + () => + readOnly ? null : { onAdd, onInsert, onInsertRoot, onDelete }, + [onAdd, onDelete, onInsert, onInsertRoot, readOnly], + ); + return ( + +
+
+
+ 执行流程 +
+ {!readOnly && ( +
+ + +
+ )} +
+
+ + nodes={nodes} + edges={edges} + nodeTypes={NODE_TYPES} + edgeTypes={EDGE_TYPES} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + onNodeClick={(_, node) => { + if (!readOnly && node.data.kind === "agent" && node.data.path) { + onSelect(node.data.path); + } + }} + nodesDraggable={!readOnly} + nodesConnectable={false} + nodesFocusable={!readOnly} + elementsSelectable={!readOnly} + edgesFocusable={false} + edgesReconnectable={false} + panOnDrag={!readOnly || interactivePreview} + zoomOnDoubleClick={interactivePreview} + zoomOnPinch={!readOnly || interactivePreview} + zoomOnScroll={!readOnly || interactivePreview} + fitView + fitViewOptions={fitOptions} + minZoom={readOnly ? 0.05 : 0.35} + maxZoom={1.6} + proOptions={{ hideAttribution: true }} + > + + {(!readOnly || interactivePreview) && ( + + )} + {MINIMAP_ENABLED && ( + + [ + "abc-minimap-node", + node.type === "group" + ? "abc-minimap-node-group" + : node.data.kind === "terminal" + ? "abc-minimap-node-terminal" + : "abc-minimap-node-agent", + node.data.pattern ? `is-${node.data.pattern}` : "", + ] + .filter(Boolean) + .join(" ") + } + /> + )} + +
+
+
+ ); +} + +export function AgentBuildCanvas(props: AgentBuildCanvasProps) { + return ( + + + + ); +} diff --git a/frontend/src/create/CustomCreate.css b/frontend/src/create/CustomCreate.css index b3c0531f..10508907 100644 --- a/frontend/src/create/CustomCreate.css +++ b/frontend/src/create/CustomCreate.css @@ -4,17 +4,164 @@ .cw-root { --cw-workbench-toolbar-height: 64px; + --cw-workspace-ink: 222 24% 13%; + --cw-workspace-accent: 162 44% 32%; + --cw-workspace-accent-soft: 156 34% 92%; + --cw-workspace-warm: 42 28% 96%; flex: 1; min-height: 0; display: flex; flex-direction: column; height: 100%; color: hsl(var(--foreground)); + background: hsl(var(--cw-workspace-warm)); +} + +/* ---------- workspace lifecycle navigation ---------- */ +.cw-workspace-header { + position: relative; + z-index: 12; + flex: 0 0 auto; + min-height: 74px; + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(420px, 560px) minmax(180px, 1fr); + align-items: center; + gap: 20px; + padding: 10px 16px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--panel) / 0.96); +} +.cw-workspace-identity { + min-width: 0; +} +.cw-workspace-identity > strong { + min-width: 0; + overflow: hidden; + color: hsl(var(--cw-workspace-ink)); + font-size: 16px; + font-weight: 700; + letter-spacing: -0.025em; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-workspace-stepper { + grid-column: 2; + min-width: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + align-items: start; +} +.cw-workspace-stepper button { + position: relative; + z-index: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 0 8px; + border: 0; + background: transparent; + color: hsl(var(--muted-foreground)); + cursor: pointer; + font: inherit; + text-align: center; + transition: color 150ms ease; +} +.cw-workspace-stepper button:not(:last-child)::after { + content: ""; + position: absolute; + z-index: -1; + top: 14px; + left: calc(50% + 17px); + width: calc(100% - 34px); + height: 1px; + background: hsl(var(--border)); +} +.cw-workspace-stepper button.is-complete:not(:last-child)::after { + background: hsl(var(--cw-workspace-ink)); +} +.cw-workspace-stepper button:hover:not(:disabled), +.cw-workspace-stepper button.is-active, +.cw-workspace-stepper button.is-complete { + color: hsl(var(--cw-workspace-ink)); +} +.cw-workspace-stepper button:focus-visible { + outline: none; +} +.cw-workspace-stepper button:focus-visible .cw-workspace-step-marker { + outline: 2px solid hsl(var(--cw-workspace-ink) / 0.28); + outline-offset: 3px; +} +.cw-workspace-stepper button:disabled { + cursor: wait; + opacity: 0.62; +} +.cw-workspace-step-marker { + width: 29px; + height: 29px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid hsl(var(--border)); + border-radius: 50%; + background: hsl(var(--panel)); + color: hsl(var(--muted-foreground)); + box-shadow: 0 0 0 4px hsl(var(--panel)); + font-size: 11px; + font-weight: 650; + transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease; +} +.cw-workspace-stepper button:hover:not(:disabled) .cw-workspace-step-marker { + border-color: hsl(var(--cw-workspace-ink) / 0.35); +} +.cw-workspace-stepper button.is-active .cw-workspace-step-marker, +.cw-workspace-stepper button.is-complete .cw-workspace-step-marker { + border-color: hsl(var(--cw-workspace-ink)); + background: hsl(var(--cw-workspace-ink)); + color: white; +} +.cw-workspace-step-marker .cw-i { + width: 13px; + height: 13px; +} +.cw-workspace-stepper button > strong { + font-size: 11.5px; + font-weight: 620; +} +@media (prefers-reduced-motion: reduce) { + .cw-workspace-stepper button, + .cw-workspace-step-marker { + transition: none; + } +} +.cw-workspace-main { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + overflow: hidden; + background: hsl(var(--background)); +} +.cw-workspace-alert { + position: absolute; + z-index: 30; + top: 94px; + right: 18px; + max-width: min(420px, calc(100% - 36px)); + padding: 10px 13px; + border: 1px solid hsl(var(--destructive) / 0.2); + border-radius: 9px; + background: hsl(var(--background)); + box-shadow: 0 12px 36px hsl(var(--foreground) / 0.12); + color: hsl(var(--destructive)); + font-size: 12.5px; } /* ---------- master-detail editor: left tree + right form ---------- */ .cw-editor { flex: 1; + width: 100%; min-height: 0; display: flex; align-items: stretch; @@ -140,7 +287,10 @@ /* Right side: a fixed agent-type bar on top + a scrolling form/nav area. */ .cw-detail { - flex: 1; + position: relative; + flex: 1 1 48%; + width: auto; + max-width: 680px; min-width: 0; min-height: 0; display: flex; @@ -171,7 +321,50 @@ scrollbar-gutter: stable both-edges; /* Left padding matches the tree's gap-from-divider so the form mirrors the Agent-structure items across the divider. */ - padding: 24px 16px 48px; + padding: 24px 16px 96px; +} +.cw-build-next { + position: absolute; + right: 20px; + bottom: 20px; + z-index: 8; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 42px; + padding: 0 17px; + border: 1px solid hsl(var(--cw-workspace-ink)); + border-radius: 10px; + background: hsl(var(--cw-workspace-ink)); + box-shadow: 0 10px 28px hsl(var(--cw-workspace-ink) / 0.2); + color: white; + cursor: pointer; + font: inherit; + font-size: 13px; + font-weight: 620; + letter-spacing: -0.01em; + transition: + background-color 150ms ease, + border-color 150ms ease, + box-shadow 150ms ease, + transform 120ms ease; +} +.cw-build-next:hover { + border-color: hsl(var(--cw-workspace-ink) / 0.88); + background: hsl(var(--cw-workspace-ink) / 0.88); + box-shadow: 0 12px 32px hsl(var(--cw-workspace-ink) / 0.24); +} +.cw-build-next:active { + transform: translateY(1px); +} +.cw-build-next:focus-visible { + outline: 2px solid hsl(var(--cw-workspace-ink) / 0.32); + outline-offset: 3px; +} +.cw-build-next .cw-i { + width: 15px; + height: 15px; } .cw-detail-inner { max-width: 780px; @@ -244,11 +437,6 @@ padding: 10px 16px; border-bottom: 1px solid hsl(var(--border)); } -.cw-debug-head-actions { - display: flex; - align-items: center; - gap: 6px; -} .cw-debug-collapse, .cw-debug-expand { display: inline-flex; @@ -297,7 +485,6 @@ font-weight: 650; color: hsl(var(--foreground)); } -.cw-debug-deploy, .cw-debug-start { display: inline-flex; align-items: center; @@ -317,35 +504,12 @@ opacity 0.12s, transform 0.1s; } -.cw-debug-deploy { - gap: 4px; - font-size: 13px; -} -.cw-debug-deploy .cw-i { - width: 13px; - height: 13px; - transition: transform 0.18s ease; -} -.cw-debug-deploy:hover:not(:disabled) .cw-i:not(.cw-spin) { - transform: translateX(2px); -} -@media (prefers-reduced-motion: reduce) { - .cw-debug-deploy .cw-i { - transition: none; - } - .cw-debug-deploy:hover:not(:disabled) .cw-i:not(.cw-spin) { - transform: none; - } -} -.cw-debug-deploy:hover:not(:disabled), .cw-debug-start:hover:not(:disabled) { opacity: 0.86; } -.cw-debug-deploy:active:not(:disabled), .cw-debug-start:active:not(:disabled) { transform: scale(0.96); } -.cw-debug-deploy:disabled, .cw-debug-start:disabled { opacity: 0.45; cursor: default; @@ -360,15 +524,6 @@ font-size: 12px; line-height: 1.45; } -.cw-debug-deploy-error { - margin: 0 16px 12px; - padding: 8px 10px; - border-radius: 8px; - background: hsl(var(--destructive) / 0.08); - color: hsl(var(--destructive)); - font-size: 12px; - line-height: 1.45; -} .cw-debug-stage { position: relative; flex: 1; @@ -642,6 +797,568 @@ cursor: default; } +/* ---------- validation workspace ---------- */ +.cw-validation-workspace { + position: relative; + flex: 1; + min-width: 0; + min-height: 0; + display: grid; + grid-template-columns: 238px minmax(0, 1fr); + background: hsl(var(--background)); +} +.cw-optimization-panel { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + padding: 22px 16px 16px; + border-right: 1px solid hsl(var(--border)); + background: hsl(var(--cw-workspace-warm)); +} +.cw-optimization-head { + display: flex; + flex-direction: column; + gap: 5px; + padding: 0 4px 18px; +} +.cw-optimization-head > span { + color: hsl(var(--cw-workspace-ink)); + font-size: 16px; + font-weight: 700; + letter-spacing: -0.02em; +} +.cw-optimization-head > small { + color: hsl(var(--muted-foreground)); + font-size: 11px; + line-height: 1.45; +} +.cw-optimization-list { + display: flex; + flex-direction: column; + gap: 8px; +} +.cw-optimization-option { + position: relative; + width: 100%; + display: flex; + align-items: flex-start; + gap: 9px; + padding: 11px; + border: 1px solid hsl(var(--border) / 0.82); + border-radius: 11px; + background: hsl(var(--panel) / 0.62); + color: hsl(var(--foreground)); + cursor: pointer; + transition: + background-color 140ms ease, + border-color 140ms ease, + box-shadow 140ms ease; +} +.cw-optimization-option:hover { + border-color: hsl(var(--cw-workspace-ink) / 0.24); + background: hsl(var(--panel)); +} +.cw-optimization-option.is-disabled { + cursor: not-allowed; + opacity: 0.62; +} +.cw-optimization-option.is-disabled:hover { + border-color: hsl(var(--border) / 0.82); + background: hsl(var(--panel) / 0.62); +} +.cw-optimization-option:has(input:focus-visible) { + outline: 2px solid hsl(var(--cw-workspace-ink) / 0.34); + outline-offset: 2px; +} +.cw-optimization-option input { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + opacity: 0; + pointer-events: none; +} +.cw-optimization-check { + width: 17px; + height: 17px; + flex: 0 0 17px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 1px; + border: 1px solid hsl(var(--foreground) / 0.24); + border-radius: 5px; + background: hsl(var(--panel)); + color: white; +} +.cw-optimization-check .cw-i { + width: 11px; + height: 11px; + stroke-width: 2.4; +} +.cw-optimization-copy { + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} +.cw-optimization-copy strong { + color: hsl(var(--cw-workspace-ink)); + font-size: 12.5px; + font-weight: 650; +} +.cw-optimization-copy small { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + line-height: 1.45; +} +.cw-validation-content { + min-width: 0; + min-height: 0; + display: flex; + overflow: hidden; + background: + radial-gradient(circle at 84% 12%, hsl(var(--cw-workspace-accent) / 0.045), transparent 30%), + hsl(var(--background)); +} +.cw-debug-next { + position: absolute; + right: 20px; + bottom: 24px; + z-index: 20; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 42px; + padding: 0 17px; + border: 1px solid hsl(var(--cw-workspace-ink)); + border-radius: 10px; + background: hsl(var(--cw-workspace-ink)); + box-shadow: 0 10px 28px hsl(var(--cw-workspace-ink) / 0.2); + color: white; + cursor: pointer; + font: inherit; + font-size: 13px; + font-weight: 620; + letter-spacing: -0.01em; + transition: + background-color 150ms ease, + box-shadow 150ms ease, + transform 120ms ease; +} +.cw-debug-next:hover { + background: hsl(var(--cw-workspace-ink) / 0.88); + box-shadow: 0 12px 32px hsl(var(--cw-workspace-ink) / 0.24); +} +.cw-debug-next:active { + transform: translateY(1px); +} +.cw-debug-next:focus-visible { + outline: 2px solid hsl(var(--cw-workspace-ink) / 0.32); + outline-offset: 3px; +} +.cw-debug-next .cw-i { + width: 15px; + height: 15px; +} +.cw-debug.is-standalone { + flex: 1; + width: 100%; + min-width: 0; + border-left: 0; + background: transparent; +} +.cw-debug.is-standalone .cw-debug-head { + height: 58px; + padding-inline: 22px; + background: hsl(var(--panel) / 0.7); +} +.cw-debug.is-standalone .cw-debug-title { + font-size: 15px; +} +.cw-debug.is-standalone .cw-debug-body { + padding: 22px clamp(18px, 5vw, 72px) 28px; +} +.cw-debug.is-standalone .cw-debug-chat { + width: min(100%, 840px); + margin: 0 auto; +} +.cw-debug.is-standalone .cw-debug-chat-empty { + min-height: 260px; + border: 1px dashed hsl(var(--border)); + border-radius: 16px; + background: hsl(var(--panel) / 0.56); +} +.cw-debug.is-standalone .cw-debug-composer { + padding: 12px 210px 20px clamp(18px, 5vw, 72px); + background: transparent; +} +.cw-debug.is-standalone .cw-debug-composerbox { + width: min(100%, 840px); + min-height: 48px; + margin: 0 auto; + border-color: hsl(var(--foreground) / 0.14); + border-radius: 14px; + box-shadow: 0 12px 32px hsl(var(--foreground) / 0.06); +} +.cw-debug.is-standalone .cw-debug-overlay { + background: hsl(var(--background) / 0.68); +} +.cw-debug.is-standalone .cw-debug-overlay-content { + width: min(100%, 390px); + padding: 28px; + border-radius: 16px; +} +.cw-validation-prototype { + flex: 1; + min-width: 0; + min-height: 0; + overflow-y: auto; + padding: clamp(26px, 4vw, 56px); +} +.cw-validation-page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 24px; + margin: 0 auto 28px; + max-width: 1040px; +} +.cw-eyebrow { + color: hsl(var(--cw-workspace-accent)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.15em; +} +.cw-validation-page-head h2 { + margin: 5px 0 4px; + color: hsl(var(--cw-workspace-ink)); + font-size: clamp(24px, 3vw, 34px); + font-weight: 720; + letter-spacing: -0.045em; +} +.cw-validation-page-head p { + margin: 0; + color: hsl(var(--muted-foreground)); + font-size: 13px; +} +.cw-prototype-action { + min-height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 0 14px; + border: 1px solid hsl(var(--cw-workspace-ink)); + border-radius: 8px; + background: hsl(var(--cw-workspace-ink)); + color: hsl(var(--background)); + font: inherit; + font-size: 12px; + font-weight: 650; +} +.cw-prototype-action:disabled { + cursor: not-allowed; + opacity: 0.72; +} +.cw-dataset-summary, +.cw-variant-grid, +.cw-metric-board, +.cw-prototype-table, +.cw-run-list, +.cw-prototype-note { + width: min(100%, 1040px); + margin-inline: auto; +} +.cw-dataset-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-bottom: 14px; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--panel)); + box-shadow: 0 10px 32px hsl(var(--foreground) / 0.035); +} +.cw-dataset-summary > div { + display: flex; + flex-direction: column; + gap: 4px; + padding: 17px 20px; +} +.cw-dataset-summary > div + div { + border-left: 1px solid hsl(var(--border)); +} +.cw-dataset-summary strong { + color: hsl(var(--cw-workspace-ink)); + font-size: 22px; + letter-spacing: -0.04em; +} +.cw-dataset-summary span { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.cw-prototype-table { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--panel)); +} +.cw-prototype-row { + min-height: 54px; + display: grid; + grid-template-columns: minmax(180px, 1.3fr) minmax(100px, 0.7fr) minmax(170px, 1fr) 82px; + align-items: center; + gap: 16px; + padding: 10px 16px; + border-top: 1px solid hsl(var(--border)); + color: hsl(var(--muted-foreground)); + font-size: 11.5px; +} +.cw-prototype-row.is-head { + min-height: 38px; + border-top: 0; + background: hsl(var(--secondary) / 0.42); + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.cw-prototype-row > strong { + color: hsl(var(--foreground)); + font-size: 12px; + font-weight: 600; +} +.cw-status-pill, +.cw-run-status, +.cw-run-kind { + justify-self: start; + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--secondary)); + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 600; +} +.cw-variant-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 14px; +} +.cw-variant-card { + position: relative; + overflow: hidden; + padding: 20px; + border: 1px solid hsl(var(--border)); + border-radius: 14px; + background: hsl(var(--panel)); + box-shadow: 0 12px 34px hsl(var(--foreground) / 0.04); +} +.cw-variant-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 3px; + background: hsl(var(--foreground) / 0.22); +} +.cw-variant-card.is-candidate::before { + background: hsl(var(--cw-workspace-accent)); +} +.cw-variant-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 28px; + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-weight: 650; + letter-spacing: 0.06em; + text-transform: uppercase; +} +.cw-variant-head small { + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--secondary)); + font-size: 9.5px; + letter-spacing: 0; + text-transform: none; +} +.cw-variant-card > strong { + color: hsl(var(--cw-workspace-ink)); + font-size: 17px; + letter-spacing: -0.025em; +} +.cw-variant-card > p { + min-height: 42px; + margin: 7px 0 22px; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.6; +} +.cw-variant-card dl { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + margin: 0; +} +.cw-variant-card dl > div { + padding: 9px 10px; + border-radius: 8px; + background: hsl(var(--secondary) / 0.5); +} +.cw-variant-card dt { + color: hsl(var(--muted-foreground)); + font-size: 9.5px; +} +.cw-variant-card dd { + margin: 3px 0 0; + color: hsl(var(--foreground)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 10.5px; +} +.cw-metric-board { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--panel)); +} +.cw-metric-head, +.cw-metric-row { + display: grid; + grid-template-columns: minmax(170px, 1fr) 100px 100px 88px; + align-items: center; + gap: 12px; + padding: 11px 16px; +} +.cw-metric-head { + grid-template-columns: auto auto minmax(0, 1fr); + min-height: 48px; + border-bottom: 1px solid hsl(var(--border)); +} +.cw-metric-head .cw-i { + width: 15px; + color: hsl(var(--cw-workspace-accent)); +} +.cw-metric-head strong { + font-size: 12px; +} +.cw-metric-head span { + justify-self: end; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.cw-metric-row { + min-height: 44px; + color: hsl(var(--muted-foreground)); + font-size: 11px; +} +.cw-metric-row + .cw-metric-row { + border-top: 1px solid hsl(var(--border) / 0.7); +} +.cw-metric-row strong { + color: hsl(var(--foreground)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; +} +.cw-metric-row em { + color: hsl(var(--cw-workspace-accent)); + font-size: 10.5px; + font-style: normal; + font-weight: 650; +} +.cw-run-list { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--panel)); +} +.cw-run-row { + min-height: 72px; + display: grid; + grid-template-columns: 36px minmax(220px, 1fr) 70px 110px 72px; + align-items: center; + gap: 12px; + padding: 11px 16px; +} +.cw-run-row + .cw-run-row { + border-top: 1px solid hsl(var(--border)); +} +.cw-run-icon { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 9px; + background: hsl(var(--cw-workspace-accent-soft)); + color: hsl(var(--cw-workspace-accent)); +} +.cw-run-icon .cw-i { + width: 14px; + height: 14px; +} +.cw-run-row > div { + min-width: 0; +} +.cw-run-row strong { + color: hsl(var(--foreground)); + font-size: 12px; +} +.cw-run-row p { + margin: 3px 0 0; + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-run-row time { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.cw-run-status.is-running { + background: hsl(var(--cw-workspace-accent-soft)); + color: hsl(var(--cw-workspace-accent)); +} +.cw-prototype-note { + display: flex; + align-items: center; + gap: 7px; + margin-top: 14px; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; +} +.cw-prototype-note .cw-i { + width: 13px; + height: 13px; +} +.cw-publish-loading { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 7px; + color: hsl(var(--muted-foreground)); + font-size: 12px; +} +.cw-publish-loading .cw-i { + width: 22px; + height: 22px; + margin-bottom: 5px; + color: hsl(var(--cw-workspace-accent)); +} +.cw-publish-loading strong { + color: hsl(var(--foreground)); + font-size: 14px; +} + /* ---------- header ---------- */ .cw-header { flex-shrink: 0; @@ -1996,6 +2713,8 @@ display: flex; } .cw-typeradio--row { + --cw-type-tone: 220 9% 24%; + --cw-type-soft: 0 0% 100%; position: relative; flex-direction: row; flex-wrap: nowrap; @@ -2017,9 +2736,10 @@ bottom: 4px; left: 4px; width: var(--cw-agent-type-slider-width); - border: 1px solid hsl(var(--border) / 0.72); + border: 1px solid hsl(var(--cw-type-tone) / 0.28); border-radius: 7px; - background: hsl(var(--background)); + background: hsl(var(--cw-type-soft)); + box-shadow: 0 1px 3px hsl(var(--cw-type-tone) / 0.08); transform: translateX(var(--cw-active-type-offset)); transition: transform 0.24s cubic-bezier(0.22, 1, 0.36, 1); } @@ -2086,7 +2806,32 @@ } .cw-typeradio-item.is-on { background: transparent; - color: hsl(var(--foreground)); + color: hsl(var(--cw-type-tone)); +} + +.cw-typeradio--row[data-active-type="llm"] { + --cw-type-tone: 220 9% 24%; + --cw-type-soft: 0 0% 100%; +} + +.cw-typeradio--row[data-active-type="sequential"] { + --cw-type-tone: 213 40% 38%; + --cw-type-soft: 214 45% 96%; +} + +.cw-typeradio--row[data-active-type="parallel"] { + --cw-type-tone: 40 43% 36%; + --cw-type-soft: 43 52% 94%; +} + +.cw-typeradio--row[data-active-type="loop"] { + --cw-type-tone: 151 34% 32%; + --cw-type-soft: 148 32% 94%; +} + +.cw-typeradio--row[data-active-type="a2a"] { + --cw-type-tone: 213 18% 38%; + --cw-type-soft: 214 20% 94%; } .cw-typeradio-item:has(.cw-typeradio-input:focus-visible) { box-shadow: inset 0 0 0 2px hsl(var(--ring) / 0.45); @@ -2797,6 +3542,11 @@ /* ---------- responsive ---------- */ @media (max-width: 1280px) { + .cw-workspace-header { + grid-template-columns: minmax(160px, 1fr) minmax(420px, 520px) minmax(160px, 1fr); + gap: 16px; + padding-inline: 16px; + } .cw-debug { width: 280px; } @@ -2806,16 +3556,22 @@ } @media (max-width: 1080px) { + .cw-workspace-header { + grid-template-columns: 150px minmax(420px, 1fr) 150px; + } .cw-editor { - flex-wrap: wrap; - overflow-y: auto; + flex-wrap: nowrap; + overflow: hidden; } .cw-tree { - height: 260px; + height: auto; } .cw-detail { - height: min(720px, calc(100dvh - 120px)); - min-height: 560px; + flex-basis: 48%; + width: auto; + max-width: 560px; + height: auto; + min-height: 0; } .cw-debug { flex: 0 0 100%; @@ -2850,6 +3606,41 @@ } @media (max-width: 860px) { + .cw-workspace-header { + min-height: 112px; + grid-template-columns: minmax(0, 1fr); + gap: 8px; + padding: 10px 12px; + } + .cw-workspace-stepper { + grid-column: 1; + width: 100%; + max-width: 480px; + justify-self: center; + } + .cw-validation-workspace { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); + } + .cw-optimization-panel { + max-height: 250px; + padding: 14px 12px; + overflow-y: auto; + border-right: 0; + border-bottom: 1px solid hsl(var(--border)); + } + .cw-optimization-head { + padding-bottom: 12px; + } + .cw-optimization-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .cw-debug.is-standalone .cw-debug-composer { + padding-right: 12px; + padding-bottom: 72px; + padding-left: 12px; + } .cw-editor { flex-direction: column; flex-wrap: nowrap; @@ -2873,7 +3664,11 @@ padding-inline: 12px; } .cw-detail-scroll { - padding: 20px 12px 40px; + padding: 20px 12px 90px; + } + .cw-build-next { + right: 14px; + bottom: 14px; } .cw-debug { flex: none; @@ -2896,6 +3691,20 @@ } @media (max-width: 700px) { + .cw-workspace-stepper button { + padding-inline: 4px; + } + .cw-optimization-list { + grid-template-columns: minmax(0, 1fr); + } + .cw-debug-next { + right: 14px; + bottom: 14px; + } + .cw-dataset-summary > div + div { + border-top: 1px solid hsl(var(--border)); + border-left: 0; + } .cw-typeradio-item { padding-inline: 6px; } diff --git a/frontend/src/create/CustomCreate.tsx b/frontend/src/create/CustomCreate.tsx index 10058ca4..d34712a5 100644 --- a/frontend/src/create/CustomCreate.tsx +++ b/frontend/src/create/CustomCreate.tsx @@ -69,6 +69,7 @@ import { displayDescription } from "./displayText"; import { localPickerMatches } from "./localPickerSearch"; import { draftToYaml } from "./configYaml"; import type { AgentProject } from "./project"; +import { AgentBuildCanvas } from "./AgentBuildCanvas"; import type { SkillSource } from "./skills/types"; import { SkillHubPicker } from "./SkillHubPicker"; import { LocalPicker } from "./LocalPicker"; @@ -83,6 +84,7 @@ import { } from "./vikingKnowledgebases"; import { ProjectPreview, + type DeployResult, type DeploymentTaskUpdate, } from "../ui/ProjectPreview"; import { Blocks, ThinkingPlaceholder } from "../ui/Blocks"; @@ -280,13 +282,24 @@ const AGENT_TYPE_BAR_LABELS: Record< NonNullable, string > = { - llm: "LLM 智能体", - sequential: "顺序型智能体", - parallel: "并行型智能体", - loop: "循环型智能体", + llm: "智能体", + sequential: "分步协作", + parallel: "同时处理", + loop: "循环执行", a2a: "远程智能体", }; +const AGENT_TYPE_DESCRIPTIONS: Record< + NonNullable, + string +> = { + llm: "理解任务并完成一个具体工作", + sequential: "内部步骤按照顺序依次执行", + parallel: "内部步骤同时工作,完成后统一汇总", + loop: "重复执行内部步骤,直到满足停止条件", + a2a: "调用已经存在的远程 Agent", +}; + const A2A_REGISTRY_ENV_TO_FIELD = { REGISTRY_SPACE_ID: "registrySpaceId", REGISTRY_TOP_K: "registryTopK", @@ -1336,6 +1349,57 @@ function addChild(root: AgentDraft, path: NodePath): AgentDraft { })); } +function insertChild( + root: AgentDraft, + parentPath: NodePath, + index: number, +): AgentDraft { + return updateNode(root, parentPath, (n) => { + const subAgents = n.subAgents.slice(); + subAgents.splice(index, 0, emptyDraft()); + return { ...n, subAgents }; + }); +} + +function nextAvailableAgentName(root: AgentDraft, preferred: string): string { + const names = new Set(); + const visit = (node: AgentDraft) => { + if (node.name) names.add(node.name); + node.subAgents.forEach(visit); + }; + visit(root); + + if (!names.has(preferred)) return preferred; + let suffix = 2; + while (names.has(`${preferred}_${suffix}`)) suffix += 1; + return `${preferred}_${suffix}`; +} + +function insertAtRootBoundary( + root: AgentDraft, + position: "before" | "after", +): AgentDraft { + const index = position === "before" ? 0 : root.subAgents.length; + if (root.agentType === "sequential") { + return insertChild(root, [], index); + } + + const originalName = agentNameProblem(root.name) === null + ? `${root.name}_step` + : "original_step"; + const original = { + ...root, + name: nextAvailableAgentName(root, originalName), + }; + const added = emptyDraft(); + return { + ...root, + agentType: "sequential", + subAgents: + position === "before" ? [added, original] : [original, added], + }; +} + function removeNode(root: AgentDraft, path: NodePath): AgentDraft { if (path.length === 0) return root; // the root is never removable const parentPath = path.slice(0, -1); @@ -1500,7 +1564,7 @@ function collectDeploymentEnv(root: AgentDraft): RuntimeEnvConfiguration { /* ---------------------------------------------------------------- * * Left structure tree: one selectable, editable node (recursive). * ---------------------------------------------------------------- */ -function TreeNode({ +export function TreeNode({ root, path, selectedPath, @@ -1684,6 +1748,7 @@ function TreeNode({ type DebugPhase = "idle" | "building" | "starting" | "ready" | "sending" | "error"; +type WorkspaceMode = "build" | "validate" | "publish"; interface DebugMessage { role: "user" | "assistant"; content: string; @@ -1700,6 +1765,13 @@ function codegenDraft(draft: AgentDraft): AgentDraft { }; } +function workspaceAgentName(draft: AgentDraft): string { + const rootName = draft.name.trim(); + if (rootName) return rootName; + if (draft.agentType !== "sequential") return ""; + return draft.subAgents.find((agent) => agent.name.trim())?.name.trim() ?? ""; +} + function debugRuntimeDraft(draft: AgentDraft): AgentDraft { const runtimeEnv = collectDeploymentEnv(draft); const values = { @@ -1725,6 +1797,7 @@ function debugSnapshotKey(draft: AgentDraft): string { } function DebugPanel({ + standalone = false, enabled, disabledReason, phase, @@ -1735,14 +1808,12 @@ function DebugPanel({ messages, input, error, - deploying, - deployError, onInput, onSend, onRestart, onIgnoreChanges, - onDeploy, }: { + standalone?: boolean; enabled: boolean; disabledReason: string; phase: DebugPhase; @@ -1753,13 +1824,10 @@ function DebugPanel({ messages: DebugMessage[]; input: string; error: string | null; - deploying: boolean; - deployError: string; onInput: (v: string) => void; onSend: () => void; onRestart: () => void; onIgnoreChanges: () => void; - onDeploy: () => void; }) { const [collapsed, setCollapsed] = useState(false); const ready = phase === "ready" || phase === "sending"; @@ -1770,7 +1838,7 @@ function DebugPanel({ enabled && (phase === "building" || phase === "starting"); const showStaleOverlay = Boolean(run && stale && !showProgressOverlay); - if (collapsed) { + if (collapsed && !standalone) { return (
-
- + {!standalone && ( + + )} + {standalone ? "快速调试" : "调试"}
@@ -1825,12 +1882,6 @@ function DebugPanel({ )} - {deployError && ( -
- {deployError} -
- )} -
{!enabled ? ( @@ -2002,6 +2053,87 @@ function DebugPanel({ ); } +const WORKSPACE_MODES: Array<{ + id: WorkspaceMode; + label: string; +}> = [ + { id: "build", label: "构建" }, + { id: "validate", label: "调试" }, + { id: "publish", label: "发布" }, +]; + +const DEBUG_OPTIMIZATIONS = [ + { + id: "context", + label: "上下文优化", + description: "压缩历史对话,保留与当前任务相关的信息", + }, + { + id: "grounding", + label: "幻觉抑制", + description: "对不确定内容要求依据,并明确表达未知", + }, + { + id: "tools", + label: "工具调用优化", + description: "减少重复调用,优先复用可信的工具结果", + }, + { + id: "latency", + label: "响应加速", + description: "缓存稳定上下文,降低重复推理开销", + }, +] as const; + +function WorkspaceHeader({ + mode, + agentName, + busy, + onChange, +}: { + mode: WorkspaceMode; + agentName: string; + busy: boolean; + onChange: (mode: WorkspaceMode) => void; +}) { + const activeIndex = WORKSPACE_MODES.findIndex((item) => item.id === mode); + return ( +
+
+ {agentName || "未命名 Agent"} +
+ +
+ ); +} + /* ================================================================ * * Main component * ================================================================ */ @@ -2012,6 +2144,19 @@ interface CustomCreateProps extends CreateModeProps { features?: UiFeatures; /** Publish deploy progress into the persistent app header. */ onDeploymentTaskChange?: (task: DeploymentTaskUpdate) => void; + /** Existing Runtime target when editing an Agent from the library. */ + deploymentTarget?: { + runtimeId: string; + name: string; + region: string; + currentVersion?: number | null; + }; + /** Called after an existing Runtime has been updated and released. */ + onDeploymentComplete?: (result: DeployResult) => void | Promise; + /** Called once the persistent deployment task has been created. */ + onDeploymentStarted?: (task: DeploymentTaskUpdate) => void; + /** Persists the live builder state as a resumable library draft. */ + onDraftChange?: (draft: AgentDraft) => void; } export function CustomCreate({ @@ -2021,17 +2166,31 @@ export function CustomCreate({ initialDraft, features, onDeploymentTaskChange, + deploymentTarget, + onDeploymentComplete, + onDeploymentStarted, + onDraftChange, }: CustomCreateProps) { void onCreate; // outcome is the in-pane project preview, not a navigation void onBack; // no footer nav in the single-scroll layout; back lives in app chrome const [draft, setDraft] = useState( () => initialDraft ?? emptyDraft(), ); + const onDraftChangeRef = useRef(onDraftChange); + useEffect(() => { + onDraftChangeRef.current = onDraftChange; + }, [onDraftChange]); + useEffect(() => { + onDraftChangeRef.current?.(draft); + }, [draft]); + const [workspaceMode, setWorkspaceMode] = useState("build"); const [showErrors, setShowErrors] = useState(false); const [validationPulse, setValidationPulse] = useState(0); const [project, setProject] = useState(null); const [building, setBuilding] = useState(false); - const [deployRegion, setDeployRegion] = useState("cn-beijing"); + const [deployRegion, setDeployRegion] = useState( + deploymentTarget?.region ?? "cn-beijing", + ); const debugEnabled = features?.generatedAgentTestRun === true; const debugDisabledReason = features?.generatedAgentTestRunDisabledReason || @@ -2191,6 +2350,38 @@ export function CustomCreate({ if (select) setSelectedPath(select); }; + const addCanvasStep = (path: NodePath) => { + const parent = getNode(draft, path); + if (!nodeAcceptsChildren(parent) || path.length >= MAX_TREE_DEPTH) return; + const next = addChild(draft, path); + const childIndex = getNode(next, path).subAgents.length - 1; + applyTree(next, [...path, childIndex]); + }; + + const insertCanvasStep = (parentPath: NodePath, index: number) => { + const parent = getNode(draft, parentPath); + if ( + !nodeAcceptsChildren(parent) || + parentPath.length >= MAX_TREE_DEPTH + ) { + return; + } + const safeIndex = Math.max(0, Math.min(index, parent.subAgents.length)); + const next = insertChild(draft, parentPath, safeIndex); + applyTree(next, [...parentPath, safeIndex]); + }; + + const insertCanvasRootStep = (position: "before" | "after") => { + const wasSequential = draft.agentType === "sequential"; + const next = insertAtRootBoundary(draft, position); + const selectedIndex = position === "before" + ? 0 + : wasSequential + ? next.subAgents.length - 1 + : 1; + applyTree(next, [selectedIndex]); + }; + const clearRootAgent = () => { if ( !window.confirm("清空根 Agent 的全部配置和子 Agent?此操作无法撤销。") @@ -2203,6 +2394,14 @@ export function CustomCreate({ setAdvancedConfigOpen(false); }; + const deleteCanvasStep = (path: NodePath) => { + if (path.length === 0) { + clearRootAgent(); + return; + } + applyTree(removeNode(draft, path), path.slice(0, -1)); + }; + // Root-only rich sections read these off the root draft directly. const builtinTools = node.builtinTools ?? []; const mcpTools = node.mcpTools ?? []; @@ -2399,30 +2598,19 @@ export function CustomCreate({ } }; - const finish = async () => { + const openPublishPreview = async () => { setBuildErr(""); - if (!requireCompleteDraft()) return; - // NOTE: do NOT call onCreate() here — it navigates away from the create - // view. The generated project preview below IS the outcome of this step. - + if (!requireCompleteDraft()) { + setWorkspaceMode("build"); + return; + } setBuilding(true); try { - // Network settings are deployment-only and are not part of the codegen - // API schema. Keep them in the local draft while sending only the - // channel flag needed to generate the project. - const proj = await generateAgentProject(codegenDraft(draft)); - await cleanupDebugRun(); - setDebugPhase("idle"); - setDebugProjectName(""); - setDebugLogs([]); - setDebugMessages([]); - setDebugInput(""); - setDebugError(null); - setProject(proj); - } catch (err) { - setBuildErr( - `打开部署页失败:${err instanceof Error ? err.message : String(err)}`, - ); + const generated = await generateAgentProject(codegenDraft(draft)); + setProject(generated); + setWorkspaceMode("publish"); + } catch (error) { + setBuildErr(error instanceof Error ? error.message : String(error)); } finally { setBuilding(false); } @@ -2528,89 +2716,55 @@ export function CustomCreate({ } }; - // ---------------------------------------------------------------- - // Preview mode: takes over the whole pane, hiding the wizard chrome. - // ---------------------------------------------------------------- - if (project) { - const handleDeploy = async ( - proj: AgentProject, - onStage?: (s: DeployStage) => void, - options?: Parameters[3], - ) => { - const net = draft.deployment?.network; - const network = - net && net.mode && net.mode !== "public" - ? { - mode: net.mode, - vpc_id: net.vpcId, - subnet_ids: net.subnetIds, - enable_shared_internet_access: net.enableSharedInternetAccess, - } - : undefined; - return deployAgentkitProject( - proj.name, - proj.files, - { region: deployRegion, projectName: "default", network }, - { ...options, onStage }, - ); - }; - - return ( -
-
- { - const nextDraft: AgentDraft = { - ...draft, - deployment: { - ...(draft.deployment ?? { feishuEnabled: false }), - feishuEnabled, - }, - }; - const nextProject = await generateAgentProject(codegenDraft(nextDraft)); - setDraft(nextDraft); - setProject(nextProject); - }} - deploymentEnv={deploymentEnv.specs} - deploymentEnvValues={{ - ...draft.deployment?.envValues, - ...deploymentEnv.fixedValues, - }} - onDeploymentEnvChange={patchDeploymentEnv} - network={draft.deployment?.network} - onNetworkChange={(network) => - setDraft((current) => ({ - ...current, - deployment: { - ...(current.deployment ?? { feishuEnabled: false }), - network, - }, - })) - } - deployRegion={deployRegion} - onDeployRegionChange={setDeployRegion} - onBack={() => setProject(null)} - onExportYaml={() => - downloadText( - `${draft.name || "agent"}.yaml`, - draftToYaml(draft), - "text/yaml", - ) - } - /> -
-
+ const handleDeploy = async ( + proj: AgentProject, + onStage?: (s: DeployStage) => void, + options?: Parameters[3], + ) => { + const net = draft.deployment?.network; + const network = + net && net.mode && net.mode !== "public" + ? { + mode: net.mode, + vpc_id: net.vpcId, + subnet_ids: net.subnetIds, + enable_shared_internet_access: net.enableSharedInternetAccess, + } + : undefined; + return deployAgentkitProject( + proj.name, + proj.files, + { + region: deploymentTarget?.region ?? deployRegion, + projectName: "default", + network, + }, + { + ...options, + onStage, + runtimeId: deploymentTarget?.runtimeId, + description: draft.description, + }, ); - } + }; + + const openValidation = () => { + if (!requireCompleteDraft()) return; + setWorkspaceMode("validate"); + }; + + const handleWorkspaceChange = (nextMode: WorkspaceMode) => { + if (nextMode === "publish") { + if (project) setWorkspaceMode("publish"); + else openPublishPreview(); + return; + } + if (nextMode === "validate") { + openValidation(); + return; + } + setWorkspaceMode(nextMode); + }; const Section = sectionImpl.current; @@ -2618,22 +2772,30 @@ export function CustomCreate({ return (
-
- {/* Left: the Agent structure tree (select / add / remove / reorder). */} - + + {buildErr && ( +
+ {buildErr} +
+ )} +
+ {workspaceMode === "build" && ( +
+ {/* Right: the form for the currently-selected node. The agent-type bar is fixed on top (outside the scroll area); the form (left) + step nav (right) scroll below it. */} @@ -2644,7 +2806,8 @@ export function CustomCreate({
@@ -2683,9 +2851,7 @@ export function CustomCreate({ onChange={() => selectAgentType(t.id)} /> - {AGENT_TYPE_BAR_LABELS[t.id].replace("智能体", "")} - - 智能体 + {AGENT_TYPE_BAR_LABELS[t.id]} {remoteTypeDisabled && ( - 远程 Agent 仅可作为子 Agent + 远程智能体只能作为子步骤使用 )} @@ -2714,7 +2880,8 @@ export function CustomCreate({ <>
) : ( - 遵循 Google ADK 命名规则,且在 Agent - 结构中保持唯一。 + 遵循 Google ADK 命名规则,且在执行流程中保持唯一。 )}