From 5b0f5f6ba21ab10f8af40edb7c2b71c1869ccb3b Mon Sep 17 00:00:00 2001 From: caishencheng Date: Thu, 13 Aug 2026 13:25:36 +0800 Subject: [PATCH 1/2] feat(studio): add multidimensional comparison debugging --- frontend/README.md | 48 +- frontend/src/a2ui/Surface.tsx | 8 +- .../src/a2ui/components/Button/Button.tsx | 1 + frontend/src/a2ui/registry.ts | 1 + frontend/src/adk/client.ts | 12 +- frontend/src/create/CustomCreate.css | 1441 +++++++- frontend/src/create/CustomCreate.tsx | 3128 ++++++++++++++--- .../create/comparison/ComparisonDrawer.tsx | 137 + .../comparison/ComparisonSessionControls.tsx | 82 + .../comparison/ComparisonTraceDrawer.tsx | 228 ++ .../create/comparison/comparisonHistory.ts | 83 + .../comparison/comparisonSessionRuntime.ts | 49 + .../comparison/comparisonSessionState.ts | 96 + .../comparison/comparisonSessionViewModel.ts | 99 + .../create/comparison/debugVariantState.ts | 242 ++ .../src/create/comparison/draftComparison.ts | 172 + .../src/create/comparison/modelCredentials.ts | 67 + .../src/create/comparison/traceAlignment.ts | 163 + frontend/src/ui/Blocks.tsx | 21 +- frontend/src/ui/StudioConfirmDialog.tsx | 42 + frontend/tests/comparisonHistory.test.mjs | 62 + .../tests/comparisonSessionControls.test.mjs | 119 + .../tests/comparisonSessionRuntime.test.mjs | 89 + .../tests/comparisonSessionState.test.mjs | 112 + .../tests/comparisonSessionViewModel.test.mjs | 216 ++ frontend/tests/comparisonTraceDrawer.test.mjs | 19 + frontend/tests/comparisonWorkspaceUx.test.mjs | 168 + .../tests/debugErrorPresentation.test.mjs | 9 - frontend/tests/debugVariantState.test.mjs | 308 ++ frontend/tests/deploymentEnv.test.mjs | 4 +- frontend/tests/generatedAgentTrace.test.mjs | 4 - frontend/tests/interactionReadOnly.test.mjs | 69 + frontend/tests/markdownPromptEditor.test.mjs | 220 +- frontend/tests/modelCredentials.test.mjs | 120 + frontend/tests/multiAgentComparison.test.mjs | 226 ++ .../tests/studioConfirmDialogFocus.test.mjs | 106 + frontend/tests/traceAlignment.test.mjs | 83 + .../test_generated_agent_backend_codegen.py | 45 + ...enerated_agent_backend_codegen_extended.py | 248 +- veadk/cli/cli_frontend.py | 263 +- veadk/cli/generated_agent_codegen.py | 171 +- veadk/cli/generated_agent_security.py | 37 +- ...Bi.js => MarkdownPromptEditor-CiSEA1Ez.js} | 2 +- ...{index-UUZFCGLU.css => index-B-fiQjyX.css} | 2 +- veadk/webui/assets/index-CIF5u1Yb.js | 1182 ------- veadk/webui/assets/index-CxFjk9KS.js | 1182 +++++++ veadk/webui/index.html | 4 +- 47 files changed, 9130 insertions(+), 2060 deletions(-) create mode 100644 frontend/src/create/comparison/ComparisonDrawer.tsx create mode 100644 frontend/src/create/comparison/ComparisonSessionControls.tsx create mode 100644 frontend/src/create/comparison/ComparisonTraceDrawer.tsx create mode 100644 frontend/src/create/comparison/comparisonHistory.ts create mode 100644 frontend/src/create/comparison/comparisonSessionRuntime.ts create mode 100644 frontend/src/create/comparison/comparisonSessionState.ts create mode 100644 frontend/src/create/comparison/comparisonSessionViewModel.ts create mode 100644 frontend/src/create/comparison/debugVariantState.ts create mode 100644 frontend/src/create/comparison/draftComparison.ts create mode 100644 frontend/src/create/comparison/modelCredentials.ts create mode 100644 frontend/src/create/comparison/traceAlignment.ts create mode 100644 frontend/tests/comparisonHistory.test.mjs create mode 100644 frontend/tests/comparisonSessionControls.test.mjs create mode 100644 frontend/tests/comparisonSessionRuntime.test.mjs create mode 100644 frontend/tests/comparisonSessionState.test.mjs create mode 100644 frontend/tests/comparisonSessionViewModel.test.mjs create mode 100644 frontend/tests/comparisonTraceDrawer.test.mjs create mode 100644 frontend/tests/comparisonWorkspaceUx.test.mjs create mode 100644 frontend/tests/debugVariantState.test.mjs create mode 100644 frontend/tests/interactionReadOnly.test.mjs create mode 100644 frontend/tests/modelCredentials.test.mjs create mode 100644 frontend/tests/multiAgentComparison.test.mjs create mode 100644 frontend/tests/studioConfirmDialogFocus.test.mjs create mode 100644 frontend/tests/traceAlignment.test.mjs rename veadk/webui/assets/{MarkdownPromptEditor-DAkD-tBi.js => MarkdownPromptEditor-CiSEA1Ez.js} (99%) rename veadk/webui/assets/{index-UUZFCGLU.css => index-B-fiQjyX.css} (72%) delete mode 100644 veadk/webui/assets/index-CIF5u1Yb.js create mode 100644 veadk/webui/assets/index-CxFjk9KS.js diff --git a/frontend/README.md b/frontend/README.md index 7563b6ef3..3183e1541 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -114,7 +114,13 @@ server that `veadk frontend` launches — no separate backend. - **Custom-agent workbench**: configure an agent with a rich Markdown system-prompt editor (including heading and list shortcuts), then debug with expandable, copyable runner error details, per-result Trace inspection, and - review. In-progress drafts are stored only in the current browser and scoped + review. Debugging is optional: creators can enter comparison debugging or + publish directly after architecture configuration. Comparison results keep + the baseline and candidates side by side, while one focused configuration + panel preserves edits when switching Agent or dimension. The baseline exposes + a read-only model, prompt, and Skills summary; candidate model changes support + Model ID, Provider, API Base, and temporary API Key values. In-progress drafts + are stored only in the current browser and scoped to the signed-in user. MCP tokens are converted to Runtime environment variables: generated source retains only the `${ENV_NAME}` reference, while YAML and browser drafts preserve the corresponding environment value. @@ -122,7 +128,13 @@ server that `veadk frontend` launches — no separate backend. environment values visible to users who can view the Agent. Entering a replacement Token overrides the previous value. Long descriptions and prompts scroll within bounded editors, while the sidebar stays pinned to the - viewport. On narrow desktop windows, the structure, configuration, and debug + viewport. Changing the group set or any candidate model, prompt, credential, + or Skill configuration makes the previous comparison Session read-only. + Studio keeps the old transcript visible until the user confirms a new Session, + then starts all valid groups with one new comparison ID without replaying prior + inputs. If any group fails to start, Studio cleans the newly staged runs and + preserves the previous evidence. On narrow desktop windows, the structure, + configuration, and debug panels stack vertically instead of squeezing the form. The deployment page pairs an inspectable Agent topology with a vertically aligned action rail for YAML export, source download, and the code browser/editor dialog, while keeping @@ -432,6 +444,38 @@ Deleting a draft attachment deletes its object. Deleting a session deletes all media scoped to that session from either backend. Because `/tmp` may be cleared at any time, use TOS when attachments must survive process or host replacement. +## Multi-scheme debug comparison + +The Custom Agent editor can compare its read-only baseline with up to three +candidate schemes in one debug workspace. A candidate may change multiple +Agents and dimensions together. The supported dimensions are system prompt, +model, and skills. Model changes treat Model ID, provider, and API Base as one +atomic configuration. + +Model API keys are temporary debug credentials. A key entered while creating a +Custom Agent may be left blank so the Studio server resolves its configured +credential. A candidate with a custom API Base must provide a temporary key. +The browser keeps these keys outside the persisted draft and comparison +history; the server injects them only into the selected debug environment. +Applying a candidate copies configuration and in-memory credential state, but +never writes a key into generated source, the Draft, or local storage. + +All running schemes receive the same initial text input. Interactive A2UI +actions are broadcast only when every running scheme exposes the same action; +otherwise the affected runs are marked as input-diverged. Changing any scheme +makes the previous Session read-only. Starting a new Session requires explicit +confirmation, starts all schemes atomically, and never replays prior inputs. If +any environment fails to start, Studio cleans the newly staged runs and keeps +the previous Session evidence available. + +The workspace reports first visible text latency, total latency, completed tool +calls, and total tokens. Cost is shown as unavailable until a shared accounting +contract exists. Trace alignment uses exact invocation or tool-call identifiers +and leaves unmatched events explicit. Human verdicts and reasons are stored in +the local comparison record; applying a candidate requires an explicit +`Adopt candidate` verdict. Scenario evaluation remains disabled until its API is +available. + ## Skills and sub-agents Type `/` in the composer to search skills mounted on the selected agent. Type diff --git a/frontend/src/a2ui/Surface.tsx b/frontend/src/a2ui/Surface.tsx index 55d92887e..a867a5ecd 100644 --- a/frontend/src/a2ui/Surface.tsx +++ b/frontend/src/a2ui/Surface.tsx @@ -79,12 +79,18 @@ function Fallback({ node }: { node: A2uiComponent }) { export interface SurfaceViewProps { surface: SurfaceState; onAction: (action: A2uiAction | undefined, node: A2uiComponent) => void; + readOnly?: boolean; } /** Render a single surface starting at its root component. */ -export function SurfaceView({ surface, onAction }: SurfaceViewProps) { +export function SurfaceView({ + surface, + onAction, + readOnly = false, +}: SurfaceViewProps) { const ctx: RenderContext = { surface, + readOnly, resolve: (v: DynamicValue) => resolveValue(v, surface.dataModel), resolveString: (v: DynamicValue) => resolveStr(v, surface.dataModel), dispatchAction: onAction, diff --git a/frontend/src/a2ui/components/Button/Button.tsx b/frontend/src/a2ui/components/Button/Button.tsx index d9016aba9..7aa159eba 100644 --- a/frontend/src/a2ui/components/Button/Button.tsx +++ b/frontend/src/a2ui/components/Button/Button.tsx @@ -9,6 +9,7 @@ export function Button({ node, ctx }: ComponentRendererProps) { className={`a2ui-button a2ui-button--${variant}`} data-a2ui-id={node.id} data-a2ui-component={node.component} + disabled={ctx.readOnly} onClick={() => ctx.dispatchAction(node.action as A2uiAction | undefined, node)} > {ctx.render(node.child as string)} diff --git a/frontend/src/a2ui/registry.ts b/frontend/src/a2ui/registry.ts index 42a78bf90..6948de538 100644 --- a/frontend/src/a2ui/registry.ts +++ b/frontend/src/a2ui/registry.ts @@ -11,6 +11,7 @@ import type { A2uiAction, A2uiComponent, DynamicValue, SurfaceState } from "./ty export interface RenderContext { surface: SurfaceState; + readOnly: boolean; /** Render a child component by id. */ render: (id: string | undefined) => ReactNode; /** Resolve a dynamic value against the surface data model. */ diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index 2f85841e8..14ff8effb 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -2959,15 +2959,21 @@ export async function generateAgentDraftFromRequirement( export async function createGeneratedAgentTestRun( draft: AgentDraft, - runtime?: { runtimeId: string; region: string }, + options: { + runtime?: { runtimeId: string; region: string }; + modelCredentials?: Array<{ agentPath: number[]; apiKey: string }>; + comparisonId?: string; + } = {}, ): Promise { const res = await apiFetch("/web/generated-agent-test-runs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ draft, - runtimeId: runtime?.runtimeId, - runtimeRegion: runtime?.region, + runtimeId: options.runtime?.runtimeId, + runtimeRegion: options.runtime?.region, + modelCredentials: options.modelCredentials ?? [], + comparisonId: options.comparisonId ?? "", }), }); if (!res.ok) { diff --git a/frontend/src/create/CustomCreate.css b/frontend/src/create/CustomCreate.css index b6c7fcbbd..dc1b4827c 100644 --- a/frontend/src/create/CustomCreate.css +++ b/frontend/src/create/CustomCreate.css @@ -19,7 +19,7 @@ background: hsl(var(--background)); } .cw-root.is-validate { - --cw-workspace-width: min(88%, 1440px); + --cw-workspace-width: min(1260px, calc(100% - 32px)); } .cw-root.is-publish { --cw-workspace-width: min(80%, 1180px); @@ -69,6 +69,14 @@ padding: 12px 0 4px; background: transparent; } +.cw-root.is-validate .cw-workspace-footer { + min-height: 64px; + gap: 6px; + margin-bottom: 0; + padding: 8px 0 6px; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--background)); +} .cw-workspace-nav-actions { width: 100%; display: grid; @@ -102,6 +110,15 @@ .cw-workspace-nav-actions.has-assistant > .cw-workspace-nav-button:last-child { grid-area: next; } +.cw-workspace-nav-actions.has-assistant > .cw-workspace-build-actions { + grid-area: next; +} +.cw-workspace-build-actions { + grid-column: 3; + display: flex; + justify-content: flex-end; + gap: 8px; +} .cw-workspace-ai-slot { min-width: 0; } @@ -1137,65 +1154,202 @@ min-width: 0; min-height: 0; display: grid; - grid-template-rows: minmax(0, 1fr) auto; + grid-template-areas: + "safety" + "toolbar" + "session" + "tabs" + "stage" + "composer"; + grid-template-rows: auto auto auto auto minmax(0, 1fr) auto; overflow: hidden; background: hsl(var(--background)); } +.cw-comparison-safety { + grid-area: safety; + margin: 8px var(--cw-workspace-gutter) 0; + padding: 10px 12px; + border: 1px solid hsl(38 82% 48% / 0.3); + border-radius: 8px; + background: hsl(38 92% 50% / 0.07); + color: hsl(var(--foreground)); + font-size: 13px; + line-height: 1.5; +} +.cw-comparison-safety-head { + min-height: 28px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} +.cw-comparison-safety-head > span { + display: inline-flex; + align-items: center; + gap: 8px; +} +.cw-comparison-safety p { + margin: 6px 0 0; + color: hsl(var(--foreground) / 0.78); +} +.cw-comparison-collapse-trigger { + min-height: 28px; + padding: 0 7px; + border: 0; + border-radius: 6px; + background: transparent; + color: hsl(var(--foreground) / 0.68); + cursor: pointer; + font: inherit; + font-size: 12px; +} +.cw-comparison-collapse-trigger:hover { + background: hsl(38 92% 50% / 0.1); + color: hsl(var(--foreground)); +} +.cw-comparison-toolbar { + grid-area: toolbar; + min-width: 0; + min-height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin: 8px var(--cw-workspace-gutter) 0; + padding: 8px 0; +} +.cw-comparison-toolbar > div:first-child { + min-width: 0; + display: grid; + gap: 2px; +} +.cw-comparison-toolbar > div:first-child strong { + font-size: 14px; + font-weight: 650; +} +.cw-comparison-toolbar > div:first-child span { + color: hsl(var(--muted-foreground)); + font-size: 12px; +} +.cw-comparison-toolbar-actions { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; +} +.cw-comparison-toolbar-actions .cw-btn { + min-height: 32px; + padding-inline: 10px; + font-size: 12px; +} +.cw-comparison-toolbar-actions .is-primary { + border-color: hsl(var(--primary)); + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} +.cw-comparison-toolbar-actions .is-danger { + color: hsl(var(--destructive)); +} +.cw-comparison-session-action { + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; +} +.cw-comparison-session-action > span { + max-width: 280px; + color: hsl(var(--destructive)); + font-size: 12px; + line-height: 1.4; + text-align: right; +} +.cw-comparison-session-alert { + grid-area: session; + min-width: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin: 0 var(--cw-workspace-gutter) 8px; + padding: 12px 16px; + border: 1px solid hsl(38 82% 48% / 0.3); + border-radius: 8px; + background: hsl(38 92% 50% / 0.07); + color: hsl(var(--foreground)); + font-size: 13px; + line-height: 1.5; +} +.cw-comparison-session-alert-copy, +.cw-comparison-session-warning-copy, +.cw-comparison-session-failure { + min-width: 0; + display: grid; + gap: 4px; +} +.cw-comparison-session-alert-copy { + gap: 8px; +} +.cw-comparison-session-alert strong { + font-weight: 650; +} +.cw-comparison-session-alert p { + margin: 0; + color: hsl(var(--foreground) / 0.78); + font-size: 12px; +} +.cw-comparison-session-failure strong { + color: hsl(var(--destructive)); +} +.cw-comparison-session-alert > .cw-comparison-session-action { + flex: 0 0 auto; +} +.cw-comparison-session-alert .cw-btn { + min-height: 34px; +} +.cw-comparison-tabs { + grid-area: tabs; + display: none; +} .cw-ab-stage { + grid-area: stage; position: relative; flex: 1; min-width: 0; min-height: 0; - overflow-x: hidden; + overflow-x: auto; overflow-y: auto; padding: 8px var(--cw-workspace-gutter); } .cw-ab-grid { min-height: 100%; display: grid; - grid-template-columns: repeat(var(--cw-ab-column-count), minmax(0, 1fr)); - grid-auto-rows: minmax(420px, 1fr); + grid-template-columns: repeat(var(--cw-ab-column-count), minmax(280px, 1fr)); + min-width: calc(var(--cw-ab-column-count) * 280px); + grid-auto-rows: minmax(0, 1fr); align-items: stretch; gap: 12px; } -.cw-ab-card { - min-width: 0; - min-height: 420px; - display: flex; - flex-direction: column; - perspective: 1400px; -} -.cw-ab-card-inner { - position: relative; - width: 100%; - min-height: 420px; - flex: 1; - transform-style: preserve-3d; - transition: transform 440ms cubic-bezier(0.22, 1, 0.36, 1); -} -.cw-ab-card-inner.is-flipped { - transform: rotateY(180deg); +.cw-ab-card:first-child { + position: sticky; + left: 0; + z-index: 2; } -.cw-ab-card-face { - position: absolute; - inset: 0; +.cw-ab-card { min-width: 0; + min-height: 0; display: flex; flex-direction: column; overflow: hidden; - border: 1px dashed hsl(var(--foreground) / 0.2); - border-radius: 16px; + border: 1px solid hsl(var(--border)); + border-radius: 12px; background: hsl(var(--background)); - backface-visibility: hidden; - -webkit-backface-visibility: hidden; transition: border-color 160ms ease, background-color 160ms ease; } -.cw-ab-card-back { - transform: rotateY(180deg); - overflow-x: hidden; - overflow-y: auto; -} .cw-ab-card-head { + position: sticky; + top: 0; + z-index: 2; min-height: 54px; display: flex; align-items: center; @@ -1217,10 +1371,176 @@ max-width: 150px; overflow: hidden; color: hsl(var(--muted-foreground)); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-comparison-baseline-config { + flex: 0 0 auto; + display: grid; + gap: 7px; + max-height: 46%; + margin: 0 10px 8px; + padding: 10px; + overflow-y: auto; + border: 1px solid hsl(var(--border)); + border-radius: 10px; + background: hsl(var(--muted) / 0.2); +} +.cw-comparison-baseline-config h3, +.cw-comparison-baseline-config p { + margin: 0; +} +.cw-comparison-baseline-config h3 { + font-size: 12px; + font-weight: 680; +} +.cw-comparison-baseline-config > p { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + line-height: 1.45; +} +.cw-comparison-baseline-config details { + padding: 7px 8px; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background)); +} +.cw-comparison-baseline-config summary { + cursor: pointer; + font-size: 11px; + font-weight: 650; +} +.cw-comparison-baseline-config dl { + display: grid; + gap: 4px; + margin: 8px 0 0; +} +.cw-comparison-baseline-config dl > div { + display: grid; + grid-template-columns: 74px minmax(0, 1fr); + gap: 7px; + font-size: 10.5px; +} +.cw-comparison-baseline-config dt { + color: hsl(var(--muted-foreground)); +} +.cw-comparison-baseline-config dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; +} +.cw-comparison-baseline-text { + display: grid; + gap: 3px; + margin-top: 8px; font-size: 10.5px; +} +.cw-comparison-baseline-text p { + max-height: 5.6em; + overflow: auto; + color: hsl(var(--muted-foreground)); + line-height: 1.4; + white-space: pre-wrap; +} +.cw-comparison-baseline-config { + max-height: none; + gap: 8px; + margin: 0 12px 10px; + padding: 10px 12px; + overflow: visible; + border-radius: 8px; + background: hsl(var(--muted) / 0.22); +} +.cw-comparison-baseline-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.cw-comparison-baseline-agent { + min-width: 0; + display: grid; + grid-template-columns: minmax(92px, 0.7fr) minmax(0, 1.3fr); + gap: 2px 10px; + padding: 8px 0; + border-top: 1px solid hsl(var(--border)); +} +.cw-comparison-baseline-agent strong, +.cw-comparison-baseline-agent span, +.cw-comparison-baseline-agent small { + min-width: 0; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.cw-comparison-baseline-agent strong { + grid-row: 1 / span 2; + align-self: center; + font-size: 12px; +} +.cw-comparison-baseline-agent span { + font-size: 12px; +} +.cw-comparison-baseline-agent small { + color: hsl(var(--muted-foreground)); + font-size: 11px; +} +.cw-comparison-status { + width: fit-content; + display: inline-flex; + align-items: center; + min-height: 20px; + padding: 0 7px; + border-radius: 999px; + background: hsl(var(--secondary)); + color: hsl(var(--muted-foreground)); + font-size: 10px; + font-style: normal; + font-weight: 600; +} +.cw-comparison-status.is-ready { + background: hsl(153 42% 92%); + color: hsl(153 48% 27%); +} +.cw-comparison-status.is-starting, +.cw-comparison-status.is-sending { + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); +} +.cw-comparison-status.is-error { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} +.cw-comparison-status.is-stale, +.cw-comparison-status.is-canceled { + background: hsl(38 92% 50% / 0.12); + color: hsl(30 82% 36%); +} +.cw-comparison-status.is-read-only { + background: hsl(38 92% 50% / 0.12); + color: hsl(30 82% 36%); +} +.cw-comparison-repair-badge { + width: fit-content; + padding: 2px 6px; + border-radius: 999px; + background: hsl(var(--cw-workspace-accent-soft)); + color: hsl(var(--cw-workspace-accent)); + font-size: 9px; + font-style: normal; + font-weight: 650; +} +.cw-comparison-diverged-badge { + width: fit-content; + padding: 2px 6px; + border-radius: 999px; + background: hsl(38 92% 50% / 0.13); + color: hsl(30 82% 36%); + font-size: 9px; + font-style: normal; + font-weight: 650; +} .cw-ab-card-actions { flex: 0 0 auto; display: flex; @@ -1261,150 +1581,822 @@ cursor: default; opacity: 0.45; } -.cw-ab-remove { - width: 28px; - padding: 0; - background: transparent; +.cw-ab-remove { + width: 28px; + padding: 0; + background: transparent; +} +.cw-comparison-config-panel { + min-inline-size: 0; + min-height: 100%; + display: grid; + grid-template-columns: minmax(180px, 220px) minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr); + overflow: hidden; + margin: 0; + padding: 0; + border: 0; + border-radius: 0; + background: hsl(var(--background)); +} +.cw-comparison-agent-nav { + min-height: 0; + display: flex; + flex-direction: column; + gap: 6px; + padding: 16px 12px; + overflow-y: auto; + border-right: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.22); +} +.cw-comparison-agent-nav > strong { + margin: 0 6px 4px; + color: hsl(var(--muted-foreground)); + font-size: 11px; + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.cw-comparison-agent-nav button { + min-height: 38px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0 10px; + overflow: hidden; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: hsl(var(--muted-foreground)); + cursor: pointer; + font: inherit; + font-size: 13px; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-comparison-agent-nav button > span:first-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} +.cw-comparison-agent-change-count { + flex: 0 0 auto; + color: hsl(var(--muted-foreground)); + font-size: 11px; + font-weight: 500; +} +.cw-comparison-agent-nav button:hover:not(:disabled) { + background: hsl(var(--background)); + color: hsl(var(--foreground)); +} +.cw-comparison-agent-nav button.is-active { + border-color: hsl(var(--border)); + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-weight: 650; +} +.cw-comparison-config-main { + min-width: 0; + min-height: 0; + padding: 20px 24px; + overflow-y: auto; +} +.cw-comparison-dimension-tabs { + width: fit-content; + display: flex; + gap: 4px; + margin-bottom: 14px; + padding: 3px; + border-radius: 10px; + background: hsl(var(--muted) / 0.58); +} +.cw-comparison-dimension-tabs button { + min-height: 34px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 14px; + border: 0; + border-radius: 7px; + background: transparent; + color: hsl(var(--muted-foreground)); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 600; +} +.cw-comparison-dimension-tabs button[aria-selected="true"] { + background: hsl(var(--background)); + color: hsl(var(--foreground)); + box-shadow: 0 1px 3px hsl(var(--foreground) / 0.1); +} +.cw-comparison-dimension-status { + padding: 2px 5px; + border-radius: 999px; + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); + font-size: 10px; + font-weight: 600; + line-height: 1.2; +} +.cw-comparison-config-problem { + margin: 0 0 12px; + padding: 9px 11px; + border: 1px solid hsl(var(--destructive) / 0.28); + border-radius: 8px; + background: hsl(var(--destructive) / 0.06); + color: hsl(var(--destructive)); + font-size: 12px; +} +.cw-comparison-change-summary { + display: grid; + gap: 10px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid hsl(var(--border)); +} +.cw-comparison-change-summary > h3 { + margin: 0; + font-size: 13px; + font-weight: 650; +} +.cw-comparison-change-groups { + display: grid; + gap: 10px; +} +.cw-comparison-change-group { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 10px; + background: hsl(var(--panel)); +} +.cw-comparison-change-group h4 { + min-height: 34px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 0; + padding: 7px 10px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.22); + font-size: 12px; + font-weight: 650; +} +.cw-comparison-change-group h4 > span:last-child { + flex: 0 0 auto; + color: hsl(var(--muted-foreground)); + font-size: 11px; + font-weight: 500; +} +.cw-comparison-change-group ul { + display: grid; + margin: 0; + padding: 0; + list-style: none; +} +.cw-comparison-change-group li { + min-height: 54px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 9px 10px; +} +.cw-comparison-change-group li + li { + border-top: 1px solid hsl(var(--border)); +} +.cw-comparison-change-group li.is-active { + background: hsl(var(--primary) / 0.05); + box-shadow: inset 3px 0 0 hsl(var(--primary)); +} +.cw-comparison-change-detail { + min-width: 0; + display: grid; + gap: 2px; +} +.cw-comparison-change-detail strong { + color: hsl(var(--foreground)); + font-size: 12px; + font-weight: 650; +} +.cw-comparison-change-detail > span { + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 11.5px; + line-height: 1.5; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-comparison-change-actions { + display: flex; + align-items: center; + gap: 8px; +} +.cw-comparison-change-editing { + color: hsl(var(--primary)); + font-size: 11px; + font-weight: 650; +} +.cw-comparison-locked-note { + margin: 14px 0 0; + color: hsl(var(--muted-foreground)); + font-size: 12px; +} +.cw-ab-config-head-actions .cw-ab-config-remove { + width: 32px; + height: 32px; +} +.cw-ab-config-done { + min-height: 32px; + padding: 0 11px; + border: 0; + border-radius: 8px; + background: hsl(var(--foreground)); + color: hsl(var(--background)); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 650; +} +.cw-ab-config-done:disabled { + background: hsl(var(--secondary) / 0.82); + color: hsl(var(--muted-foreground) / 0.68); + cursor: not-allowed; +} +.cw-ab-config { + flex: 0 0 auto; + display: grid; + grid-template-columns: minmax(0, 1fr); + align-content: start; + gap: 12px; + padding: 16px 0 20px; + background: hsl(var(--background)); +} +.cw-ab-config > label, +.cw-ab-config fieldset { + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; + margin: 0; + padding: 0; + border: 0; +} +.cw-ab-config > label > span, +.cw-ab-config legend { + color: hsl(var(--muted-foreground)); + font-size: 13px; + font-weight: 650; +} +.cw-ab-config legend { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.cw-ab-config legend em { + padding: 3px 7px; + border-radius: 999px; + background: hsl(var(--secondary)); + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + font-style: normal; + font-weight: 550; +} +.cw-ab-config input[type="text"], +.cw-ab-config > label > input, +.cw-ab-config > label > textarea, +.cw-ab-config > label > select { + width: 100%; + min-height: 36px; + padding: 8px 11px; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font: inherit; + font-size: 13px; +} +.cw-comparison-skills { + display: grid; + gap: 8px; +} +.cw-comparison-skills > span { + color: hsl(var(--foreground)); + font-size: 12px; + font-weight: 600; +} +.cw-comparison-skills > small { + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.5; +} +.cw-comparison-drawer-scrim { + position: fixed; + inset: 0; + z-index: 1100; + display: flex; + justify-content: flex-end; + background: hsl(var(--foreground) / 0.22); + backdrop-filter: blur(2px); + animation: cw-comparison-drawer-fade 140ms ease-out; +} +.cw-comparison-drawer { + width: min(720px, calc(100vw - 40px)); + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + border-left: 1px solid hsl(var(--border)); + background: hsl(var(--background)); + box-shadow: -16px 0 48px hsl(var(--foreground) / 0.14); + animation: cw-comparison-drawer-enter 220ms cubic-bezier(0.22, 1, 0.36, 1); +} +.cw-comparison-drawer.is-standard { + width: min(600px, calc(100vw - 40px)); +} +.cw-comparison-drawer-head { + flex: 0 0 auto; + min-height: 68px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 12px 20px 12px 24px; + border-bottom: 1px solid hsl(var(--border)); +} +.cw-comparison-drawer-head > div { + min-width: 0; +} +.cw-comparison-drawer-head h2, +.cw-comparison-drawer-head p { + margin: 0; +} +.cw-comparison-drawer-head h2 { + color: hsl(var(--foreground)); + font-size: 17px; + font-weight: 650; +} +.cw-comparison-drawer-head p { + margin-top: 4px; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.5; +} +.cw-comparison-drawer-close { + width: 32px; + height: 32px; + flex: 0 0 auto; + display: grid; + place-items: center; + padding: 0; + border: 0; + border-radius: 6px; + background: transparent; + color: hsl(var(--muted-foreground)); + cursor: pointer; +} +.cw-comparison-drawer-close svg { + width: 16px; + height: 16px; +} +.cw-comparison-drawer-close:hover:not(:disabled) { + background: hsl(var(--secondary)); + color: hsl(var(--foreground)); +} +.cw-comparison-drawer-close:focus-visible, +.cw-comparison-drawer-actions button:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 2px; +} +.cw-comparison-drawer-body { + flex: 1; + min-width: 0; + min-height: 0; + overflow: auto; +} +.cw-comparison-drawer-actions { + flex: 0 0 64px; + display: flex; + align-items: center; + padding: 0 20px 0 24px; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--background)); +} +.cw-comparison-drawer-footer { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.cw-comparison-drawer-footer > span { + display: inline-flex; + align-items: center; + gap: 8px; +} +.cw-comparison-drawer-actions button { + min-height: 34px; + padding: 0 14px; + 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; +} +.cw-comparison-drawer-actions button.is-primary { + border-color: hsl(var(--primary)); + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} +.cw-comparison-drawer-actions .cw-comparison-danger-action { + border-color: transparent; + color: hsl(var(--destructive)); +} +.cw-comparison-drawer-actions button:hover:not(:disabled) { + background: hsl(var(--secondary)); +} +.cw-comparison-drawer-actions button.is-primary:hover:not(:disabled) { + background: hsl(var(--primary) / 0.9); +} +.cw-comparison-drawer-actions button:disabled { + cursor: not-allowed; + opacity: 0.48; +} +@keyframes cw-comparison-drawer-fade { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes cw-comparison-drawer-enter { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} +@media (prefers-reduced-motion: reduce) { + .cw-comparison-drawer-scrim, + .cw-comparison-drawer { + animation: none; + } +} +.cw-comparison-baseline-detail { + display: grid; + gap: 28px; + padding: 24px; +} +.cw-comparison-baseline-detail section { + display: grid; + gap: 16px; +} +.cw-comparison-baseline-detail h3 { + position: relative; + margin: 0; + padding-left: 11px; + font-size: 14px; + font-weight: 650; + line-height: 22px; +} +.cw-comparison-baseline-detail h3::before { + position: absolute; + top: 4px; + bottom: 4px; + left: 0; + width: 3px; + border-radius: 2px; + background: hsl(var(--primary)); + content: ""; +} +.cw-comparison-baseline-detail dl { + display: grid; + gap: 12px; + margin: 0; +} +.cw-comparison-baseline-detail dl > div { + display: grid; + grid-template-columns: 158px minmax(0, 1fr); + gap: 16px; + align-items: start; + font-size: 13px; + line-height: 1.6; +} +.cw-comparison-baseline-detail dt { + color: hsl(var(--muted-foreground)); + text-align: right; +} +.cw-comparison-baseline-detail dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; +} +.cw-comparison-baseline-detail .is-long dd { + max-height: 180px; + overflow: auto; + white-space: pre-wrap; +} +.cw-comparison-history-table-wrap { + min-width: 0; + padding: 20px 24px 28px; + overflow: auto; +} +.cw-comparison-history-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + font-size: 12px; +} +.cw-comparison-history-table th, +.cw-comparison-history-table td { + min-width: 0; + padding: 10px 12px; + overflow: hidden; + border-bottom: 1px solid hsl(var(--border)); + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-comparison-history-table th { + background: hsl(var(--secondary) / 0.54); + color: hsl(var(--muted-foreground)); + font-weight: 600; +} +.cw-comparison-history-table th:first-child { + width: 178px; +} +.cw-comparison-history-table th:nth-child(4), +.cw-comparison-history-table th:nth-child(5) { + width: 90px; +} +.cw-comparison-drawer-empty { + min-height: 240px; + display: grid; + place-items: center; + padding: 32px; + color: hsl(var(--muted-foreground)); + font-size: 13px; + text-align: center; +} +.cw-comparison-trace-content { + display: grid; + min-height: 0; + gap: 18px; + padding: 16px 18px 24px; + overflow: auto; +} +.cw-comparison-trace-section { + display: grid; + gap: 8px; +} +.cw-comparison-trace-section h3 { + margin: 0; + font-size: 13px; + font-weight: 650; +} +.cw-comparison-trace-table { + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 10px; +} +.cw-comparison-trace-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.cw-comparison-trace-row + .cw-comparison-trace-row { + border-top: 1px solid hsl(var(--border)); +} +.cw-comparison-trace-row > span { + display: grid; + min-width: 0; + gap: 3px; + padding: 9px 10px; +} +.cw-comparison-trace-row > span + span { + border-left: 1px solid hsl(var(--border)); +} +.cw-comparison-trace-row strong { + overflow: hidden; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-comparison-trace-row small, +.cw-comparison-trace-row em { + color: hsl(var(--muted-foreground)); + font-size: 11.5px; + font-style: normal; +} +.cw-comparison-trace-row.is-head { + background: hsl(var(--secondary) / 0.5); + color: hsl(var(--muted-foreground)); + font-size: 12px; + font-weight: 650; +} +.cw-comparison-trace-row .is-unmatched { + background: hsl(var(--secondary) / 0.18); +} +.cw-comparison-verdict { + display: grid; + gap: 8px; + padding: 10px 12px; + border-top: 1px solid hsl(var(--border)); +} +.cw-comparison-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1px; + margin: 0; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--border)); +} +.cw-comparison-metrics > div { + min-width: 0; + padding: 8px; + background: hsl(var(--background)); +} +.cw-comparison-metrics dt { + color: hsl(var(--muted-foreground)); + font-size: 11.5px; +} +.cw-comparison-metrics dd { + margin: 2px 0 0; + overflow: hidden; + color: hsl(var(--foreground)); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-comparison-diff-summary { + flex: 0 0 auto; + min-width: 0; + display: grid; + gap: 8px; + margin: 0 10px 8px; + padding: 10px 12px; + border: 1px solid hsl(var(--border)); + border-radius: 10px; + background: hsl(var(--muted) / 0.2); } -.cw-ab-config-head { - min-height: 68px; +.cw-comparison-diff-head, +.cw-comparison-diff-group > h4 { + min-width: 0; display: flex; align-items: center; justify-content: space-between; - gap: 12px; - padding: 14px 16px 10px; - background: hsl(var(--background) / 0.94); + gap: 8px; } -.cw-ab-config-head > div { - min-width: 0; - display: flex; - flex-direction: column; - gap: 3px; +.cw-comparison-diff-head h3, +.cw-comparison-diff-group > h4 { + margin: 0; } -.cw-ab-config-head strong { - font-size: 16px; - font-weight: 680; +.cw-comparison-diff-head h3 { + color: hsl(var(--foreground)); + font-size: 12.5px; + font-weight: 650; + line-height: 1.45; } -.cw-ab-config-head span { +.cw-comparison-diff-head > span, +.cw-comparison-diff-group > h4 > span:last-child { + flex: 0 0 auto; color: hsl(var(--muted-foreground)); - font-size: 12px; + font-size: 11px; + font-weight: 500; } -.cw-ab-config-head > .cw-ab-config-head-actions { - flex: 0 0 auto; - display: inline-flex; - flex-direction: row; - align-items: center; - gap: 6px; +.cw-comparison-diff-groups, +.cw-comparison-diff-group, +.cw-comparison-diff-item, +.cw-comparison-diff-item dl { + display: grid; + min-width: 0; } -.cw-ab-config-head-actions .cw-ab-config-remove { - width: 32px; - height: 32px; +.cw-comparison-diff-groups { + gap: 10px; } -.cw-ab-config-done { - min-height: 32px; - padding: 0 11px; - border: 0; - border-radius: 8px; - background: hsl(var(--foreground)); - color: hsl(var(--background)); - cursor: pointer; - font: inherit; +.cw-comparison-diff-group, +.cw-comparison-diff-item dl { + gap: 8px; +} +.cw-comparison-diff-group > h4 > span:first-child { + min-width: 0; + overflow: hidden; + color: hsl(var(--foreground)); font-size: 12px; - font-weight: 650; + font-weight: 600; + line-height: 1.45; + text-overflow: ellipsis; + white-space: nowrap; } -.cw-ab-config-done-wrap { - position: relative; - flex: 0 0 auto; - display: inline-flex; +.cw-comparison-diff-item { + gap: 6px; + padding: 8px; border-radius: 8px; + background: hsl(var(--background) / 0.76); } -.cw-ab-config-done:disabled { - background: hsl(var(--secondary) / 0.82); - color: hsl(var(--muted-foreground) / 0.68); - cursor: not-allowed; +.cw-comparison-diff-item > strong { + color: hsl(var(--foreground)); + font-size: 12px; + font-weight: 600; + line-height: 1.45; } -.cw-ab-config-head .cw-ab-config-done-tip { - position: absolute; - z-index: 8; - right: 0; - top: calc(100% + 7px); - bottom: auto; - width: max-content; - max-width: 190px; - padding: 6px 8px; - border-radius: 7px; - background: hsl(var(--foreground)); - color: #fff; +.cw-comparison-diff-item dl { + margin: 0; +} +.cw-comparison-diff-item dl > div { + min-width: 0; + display: grid; + gap: 4px; +} +.cw-comparison-diff-item dt { + overflow-wrap: anywhere; + color: hsl(var(--muted-foreground)); font-size: 11px; - font-weight: 400; + font-weight: 500; line-height: 1.4; - opacity: 0; - pointer-events: none; - transform: translateY(3px); - transition: opacity 140ms ease, transform 140ms ease; -} -.cw-ab-config-done-wrap.is-disabled:hover .cw-ab-config-done-tip, -.cw-ab-config-done-wrap.is-disabled:focus-visible .cw-ab-config-done-tip { - opacity: 1; - transform: translateY(0); } -.cw-ab-config-done-wrap:focus-visible { - outline: 2px solid hsl(var(--foreground) / 0.18); - outline-offset: 2px; +.cw-comparison-diff-item dd { + min-width: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: start; + gap: 6px; + margin: 0; } -.cw-ab-config { - flex: 0 0 auto; +.cw-comparison-diff-value { + min-width: 0; display: grid; - grid-template-columns: minmax(0, 1fr); - align-content: start; - gap: 12px; - padding: 12px 16px 16px; - background: hsl(var(--background)); + gap: 2px; } -.cw-ab-config > label, -.cw-ab-config fieldset { +.cw-comparison-diff-value > small { + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + line-height: 1.35; +} +.cw-comparison-diff-value > span:last-child { min-width: 0; - display: flex; - flex-direction: column; - gap: 6px; - margin: 0; - padding: 0; - border: 0; + overflow-wrap: anywhere; + color: hsl(var(--foreground)); + font-size: 12px; + line-height: 1.45; } -.cw-ab-config > label > span, -.cw-ab-config legend { +.cw-comparison-diff-arrow { + display: grid; + place-items: center; + padding-top: 16px; color: hsl(var(--muted-foreground)); - font-size: 13px; - font-weight: 650; + font-size: 11px; + line-height: 1; } -.cw-ab-config legend { - width: 100%; - display: flex; +.cw-comparison-diff-long { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} +.cw-comparison-diff-long:focus-visible { + -webkit-line-clamp: unset; + outline: 2px solid hsl(var(--primary) / 0.35); + outline-offset: 1px; +} +.cw-comparison-diff-toggle { + width: fit-content; + min-height: 28px; + display: inline-flex; align-items: center; - justify-content: space-between; - gap: 10px; + padding: 0; + border: 0; + background: transparent; + color: hsl(var(--primary)); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 500; } -.cw-ab-config legend em { - padding: 3px 7px; - border-radius: 999px; - background: hsl(var(--secondary)); +.cw-comparison-diff-toggle:hover { + color: hsl(var(--primary) / 0.78); +} +.cw-comparison-diff-toggle:focus-visible { + outline: 2px solid hsl(var(--primary) / 0.35); + outline-offset: 2px; +} +.cw-comparison-verdict label { + display: grid; + gap: 4px; color: hsl(var(--muted-foreground)); - font-size: 10.5px; - font-style: normal; - font-weight: 550; + font-size: 12px; } -.cw-ab-config input[type="text"], -.cw-ab-config > label > input, -.cw-ab-config > label > textarea { - width: 100%; - min-height: 40px; - padding: 8px 11px; +.cw-comparison-verdict input, +.cw-comparison-verdict select { + min-width: 0; + min-height: 32px; border: 1px solid hsl(var(--border)); - border-radius: 8px; + border-radius: 7px; + padding: 5px 8px; background: hsl(var(--background)); color: hsl(var(--foreground)); font: inherit; - font-size: 13px; } .cw-ab-config > label > textarea { min-height: 58px; @@ -1448,7 +2440,7 @@ } .cw-ab-empty { height: 100%; - min-height: 210px; + min-height: 100px; display: grid; place-items: center; color: hsl(var(--muted-foreground)); @@ -1578,18 +2570,20 @@ white-space: nowrap; } .cw-ab-composer { + grid-area: composer; position: relative; z-index: 2; min-width: 0; display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr); align-items: stretch; gap: 12px; - padding: 18px var(--cw-workspace-gutter) 18px; + padding: 12px var(--cw-workspace-gutter) 14px; + border-top: 1px solid hsl(var(--border)); background: hsl(var(--background)); } .cw-ab-composer .cw-debug-composerbox { - width: 100%; + width: min(100%, 768px); min-height: 48px; margin: 0 auto; border-color: hsl(var(--foreground) / 0.14); @@ -4340,6 +5334,62 @@ } @media (max-width: 700px) { + .cw-ab-workspace { + grid-template-rows: auto auto auto auto minmax(0, 1fr) auto; + } + .cw-comparison-tabs { + display: flex; + gap: 6px; + overflow-x: auto; + padding: 8px var(--cw-workspace-gutter) 0; + } + .cw-comparison-tabs button { + flex: 0 0 auto; + min-height: 34px; + padding: 0 12px; + border: 1px solid hsl(var(--border)); + border-radius: 999px; + background: hsl(var(--background)); + color: hsl(var(--muted-foreground)); + font: inherit; + cursor: pointer; + } + .cw-comparison-tabs button[aria-selected="true"] { + border-color: hsl(var(--foreground)); + color: hsl(var(--foreground)); + } + .cw-comparison-safety-head, + .cw-comparison-toolbar { + align-items: flex-start; + flex-direction: column; + } + .cw-comparison-toolbar-actions { + width: 100%; + overflow-x: auto; + padding-bottom: 2px; + } + .cw-comparison-toolbar-actions .cw-btn { + flex: 0 0 auto; + } + .cw-comparison-session-action { + flex: 0 0 auto; + } + .cw-comparison-session-action > span { + max-width: 220px; + text-align: left; + } + .cw-comparison-session-alert { + align-items: stretch; + flex-direction: column; + gap: 12px; + } + .cw-comparison-session-alert .cw-btn { + align-self: flex-start; + } + .cw-comparison-drawer, + .cw-comparison-drawer.is-standard { + width: min(100%, 720px); + } .cw-workspace-header h1 { text-align: center; } @@ -4349,6 +5399,13 @@ .cw-workspace-nav-button { padding-inline: 12px; } + .cw-workspace-build-actions { + gap: 6px; + } + .cw-workspace-build-actions .cw-workspace-nav-button { + padding-inline: 9px; + font-size: 12px; + } .cw-form > .cw-more-options, .cw-dependency-hint { margin-left: 0; @@ -4358,6 +5415,47 @@ } .cw-ab-grid { grid-template-columns: minmax(0, 1fr); + min-width: 0; + } + .cw-ab-card { + display: none; + } + .cw-ab-card.is-narrow-selected { + display: flex; + } + .cw-ab-card:first-child { + position: static; + } + .cw-comparison-config-panel { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto auto minmax(0, 1fr); + } + .cw-comparison-agent-nav { + flex-direction: row; + padding: 8px 10px; + overflow-x: auto; + border-right: 0; + border-bottom: 1px solid hsl(var(--border)); + } + .cw-comparison-agent-nav > strong { + display: none; + } + .cw-comparison-agent-nav button { + flex: 0 0 auto; + } + .cw-comparison-config-main { + padding: 12px; + } + .cw-comparison-dimension-tabs { + width: 100%; + } + .cw-comparison-dimension-tabs button { + min-width: 0; + flex: 1 1 0; + flex-direction: column; + justify-content: center; + gap: 2px; + padding: 4px 6px; } .cw-ab-config { grid-template-columns: minmax(0, 1fr); @@ -4374,3 +5472,50 @@ border-left: 0; } } +.cw-model-secret-input, +.cw-model-secret-configured { + display: flex; + align-items: center; + gap: 8px; +} + +.cw-model-secret-input .cw-input { + min-width: 0; + flex: 1; +} + +.cw-model-secret-toggle { + display: inline-flex; + min-width: 36px; + min-height: 36px; + align-items: center; + justify-content: center; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background)); + color: hsl(var(--muted-foreground)); + cursor: pointer; +} + +.cw-model-secret-toggle:focus-visible, +.cw-link-btn:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 2px; +} + +.cw-model-secret-configured { + min-height: 38px; + justify-content: space-between; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + padding: 0 10px; + color: hsl(var(--muted-foreground)); +} + +.cw-link-btn { + border: 0; + padding: 4px; + background: transparent; + color: hsl(var(--primary)); + cursor: pointer; +} diff --git a/frontend/src/create/CustomCreate.tsx b/frontend/src/create/CustomCreate.tsx index e067dfed4..b5b70f704 100644 --- a/frontend/src/create/CustomCreate.tsx +++ b/frontend/src/create/CustomCreate.tsx @@ -6,6 +6,7 @@ import { type ReactNode, Suspense, useEffect, + useId, useMemo, useRef, useState, @@ -23,11 +24,12 @@ import { ExternalLink, FolderUp, Globe, + Eye, + EyeOff, Info, Layers, Loader2, Plus, - RefreshCw, Rocket, Shapes, Sparkles, @@ -107,6 +109,7 @@ import { DeploymentErrorMessage } from "../ui/DeploymentErrorMessage"; import { StudioConfirmDialog } from "../ui/StudioConfirmDialog"; import { TraceDrawer } from "../ui/TraceDrawer"; import { isImeCompositionEvent } from "../ui/composerKeyboard"; +import type { A2uiAction, A2uiComponent } from "../a2ui/types"; import { createGeneratedAgentTestRun, createGeneratedAgentTestSession, @@ -139,6 +142,52 @@ import { } from "../adk/cloudProvider"; import { applyEvent, emptyAcc, type Block } from "../blocks"; import { customModelCredentialRequirements } from "./customModelCredentials"; +import { validateModelConnection } from "./comparison/modelCredentials"; +import { + previewDebugChangeSummary, + removeDebugChange, + summarizeDebugChanges, + switchPrimaryDebugChange, + updateDebugVariantConfiguration as preserveDebugVariantEvidence, +} from "./comparison/debugVariantState"; +import { + buildCandidateDraft, + applyCandidateAtomically, + effectiveComparisonOverrides, + fingerprintDraft, + firstConfigurableAgent, + listConfigurableAgents, + type AgentComparisonOverride, + type ComparisonDimension, +} from "./comparison/draftComparison"; +import { + persistComparisonRecord, + readComparisonRecords, + type ComparisonHistoryRecord, +} from "./comparison/comparisonHistory"; +import { + ComparisonTraceDrawer, + type ComparisonTraceTarget, +} from "./comparison/ComparisonTraceDrawer"; +import { ComparisonDrawer } from "./comparison/ComparisonDrawer"; +import { + beginComparisonSession, + comparisonSessionStatus, + completeComparisonSession, + createComparisonSessionState, + failComparisonSession, + markComparisonConfigurationChanged, + resetComparisonSessionState, + type ComparisonSessionState, +} from "./comparison/comparisonSessionState"; +import { + comparisonSessionViewModel, +} from "./comparison/comparisonSessionViewModel"; +import { ComparisonSessionActionSlot } from "./comparison/ComparisonSessionControls"; +import { + debugRuntimeFailureMessage, + stageComparisonRuntimes, +} from "./comparison/comparisonSessionRuntime"; import "./CustomCreate.css"; const MarkdownPromptEditor = lazy(() => import("./MarkdownPromptEditor")); @@ -270,27 +319,6 @@ function ClearAgentIcon({ className }: { className?: string }) { ); } -/** Debug-run mark: a play head breaking through two lightweight motion rails. */ -function DebugRunIcon({ className }: { className?: string }) { - return ( - - ); -} - function DebugVariantDeleteIcon({ className }: { className?: string }) { return ( void; cloudProvider: CloudProvider; }) { + const pickerId = useId(); + const dialogTitleId = `${pickerId}-title`; + const tabPanelId = `${pickerId}-panel`; const [active, setActive] = useState("local"); const [open, setOpen] = useState(false); const activeIndex = SKILL_SOURCES.findIndex((source) => source.id === active); @@ -1417,14 +1448,14 @@ function SkillsSourceTabs({ className="cw-skill-dialog" role="dialog" aria-modal="true" - aria-labelledby="cw-skill-dialog-title" + aria-labelledby={dialogTitleId} initial={{ opacity: 0, y: 10, scale: 0.985 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 6, scale: 0.99 }} transition={{ duration: 0.18, ease: "easeOut" }} >
-

添加 Skill

+

添加 Skill

{active === "skillhub" && ( @@ -1541,6 +1572,9 @@ function Toggle({ * ================================================================ */ type NodePath = number[]; +const nodePathKey = (path: NodePath) => + path.length === 0 ? "root" : path.join("."); + const samePath = (a: NodePath, b: NodePath) => a.length === b.length && a.every((v, i) => v === b[i]); @@ -1974,7 +2008,13 @@ export function TreeNode({ ); } -type DebugPhase = "idle" | "starting" | "ready" | "sending" | "error"; +type DebugPhase = + | "idle" + | "starting" + | "ready" + | "sending" + | "canceled" + | "error"; type WorkspaceMode = "build" | "validate" | "publish"; interface DebugMessage { @@ -1984,13 +2024,47 @@ interface DebugMessage { error?: string; } +interface DebugVariantChange { + id: string; + modelName: string; + modelProvider: string; + modelApiBase: string; + apiKey: string; + apiKeyLocked: boolean; + apiKeyVisible: boolean; + instruction: string; + selectedSkills: SelectedSkill[]; + agentKey: string; + dimension: ComparisonDimension; +} + interface DebugVariant { id: string; name: string; modelName: string; - description: string; + modelProvider: string; + modelApiBase: string; + apiKey: string; + apiKeyLocked: boolean; + apiKeyVisible: boolean; instruction: string; - optimizations: string[]; + selectedSkills: SelectedSkill[]; + agentKey: string; + dimension: ComparisonDimension; + additionalChanges: DebugVariantChange[]; + verdict: + | "" + | "采用候选" + | "保留基线" + | "持平" + | "各有取舍" + | "证据不足"; + verdictReason: string; + ttftMs: number | null; + latencyMs: number | null; + toolCalls: number | null; + tokens: number | null; + inputDiverged: boolean; configOpen: boolean; phase: DebugPhase; runtimeSnapshot: string; @@ -2059,6 +2133,27 @@ function draftForCloudProvider( }; } +type DebugActionConfirm = + | { kind: "new-session"; variantCount: number } + | { kind: "delete"; variantId: string; variantName: string } + | { kind: "baseline-failed-apply"; variantId: string }; + +interface PreparedDebugRuntime { + runtime: { run: GeneratedAgentTestRun; sessionId: string }; + runtimeSnapshot: string; + credentialAgentPaths: number[][]; +} + +class DebugRuntimePreparationError extends Error { + constructor( + readonly stage: "run" | "session", + message: string, + ) { + super(message); + this.name = "DebugRuntimePreparationError"; + } +} + function codegenDraft(draft: AgentDraft): AgentDraft { const prepared = prepareMcpAuth(draft).draft; return { @@ -2107,170 +2202,1242 @@ function debugVariantSnapshot( draftSnapshot: string, variant: Pick< DebugVariant, - "modelName" | "description" | "instruction" | "optimizations" + | "modelName" + | "modelProvider" + | "modelApiBase" + | "instruction" + | "selectedSkills" + | "agentKey" + | "dimension" + | "additionalChanges" >, ): string { return JSON.stringify({ draftSnapshot, modelName: variant.modelName, - description: variant.description, + modelProvider: variant.modelProvider, + modelApiBase: variant.modelApiBase, instruction: variant.instruction, - optimizations: variant.optimizations, + selectedSkills: variant.selectedSkills, + agentKey: variant.agentKey, + dimension: variant.dimension, + additionalChanges: variant.additionalChanges.map(debugChangeConfiguration), }); } function debugVariantConfigurationKey( - variant: Pick< - DebugVariant, - "modelName" | "description" | "instruction" | "optimizations" - >, + draft: AgentDraft, + variant: DebugVariant, ): string { - return JSON.stringify({ - modelName: variant.modelName.trim(), - description: variant.description.trim(), - instruction: variant.instruction.trim(), - optimizations: variant.optimizations, - }); + if (variant.id === "baseline") return "[]"; + return JSON.stringify( + effectiveDebugChanges(draft, variant) + .map(debugChangeConfiguration) + .sort((left, right) => + `${left.agentKey}:${left.dimension}`.localeCompare( + `${right.agentKey}:${right.dimension}`, + ), + ), + ); } -function DebugComparisonWorkspace({ - enabled, - disabledReason, +function primaryDebugChange(variant: DebugVariant): DebugVariantChange { + return { + id: "primary", + modelName: variant.modelName, + modelProvider: variant.modelProvider, + modelApiBase: variant.modelApiBase, + apiKey: variant.apiKey, + apiKeyLocked: variant.apiKeyLocked, + apiKeyVisible: variant.apiKeyVisible, + instruction: variant.instruction, + selectedSkills: variant.selectedSkills, + agentKey: variant.agentKey, + dimension: variant.dimension, + }; +} + +function debugChangesForVariant(variant: DebugVariant): DebugVariantChange[] { + return [primaryDebugChange(variant), ...variant.additionalChanges]; +} + +function debugChangeConfiguration(change: DebugVariantChange) { + return { + modelName: change.modelName.trim(), + modelProvider: change.modelProvider.trim(), + modelApiBase: change.modelApiBase.trim(), + instruction: change.instruction.trim(), + selectedSkills: change.selectedSkills, + agentKey: change.agentKey, + dimension: change.dimension, + }; +} + +function debugVariantConfigurationProblem( + draft: AgentDraft, + variant: DebugVariant, +): string { + if (variant.id === "baseline") return ""; + const seen = new Set(); + for (const change of debugChangesForVariant(variant)) { + const key = `${change.agentKey}:${change.dimension}`; + if (seen.has(key)) return "同一 Agent 的同一维度只能配置一次"; + seen.add(key); + if (change.dimension === "model" && !change.modelName.trim()) { + return "请填写 Model ID"; + } + if (change.dimension === "instruction" && !change.instruction.trim()) { + return "请填写系统提示词"; + } + } + if (effectiveDebugChanges(draft, variant).length === 0) { + return "请至少修改一个配置项"; + } + return ""; +} + +function baselineDebugChange( + draft: AgentDraft, + baselineApiKeys: Record, + agentKey: string, + dimension: ComparisonDimension, +): DebugVariantChange | null { + const agent = listConfigurableAgents(draft).find( + (item) => item.key === agentKey, + ); + if (!agent) return null; + const target = getNode(draft, agent.path); + return { + id: `${agentKey}:${dimension}`, + modelName: target.modelName ?? "", + modelProvider: target.modelProvider ?? "", + modelApiBase: target.modelApiBase ?? "", + apiKey: baselineApiKeys[agentKey] ?? "", + apiKeyLocked: false, + apiKeyVisible: false, + instruction: target.instruction, + selectedSkills: structuredClone(target.selectedSkills ?? []), + agentKey, + dimension, + }; +} + +function debugApiKeyForAgent( + variant: DebugVariant, + agentKey: string, + baselineApiKeys: Record, +): string { + if (variant.id === "baseline") return baselineApiKeys[agentKey] ?? ""; + const modelChange = debugChangesForVariant(variant).find( + (change) => + change.agentKey === agentKey && change.dimension === "model", + ); + return modelChange?.apiKey ?? baselineApiKeys[agentKey] ?? ""; +} + +const COMPARISON_DIFF_PREVIEW_LIMIT = 3; + +interface DebugConfigurationDiffLine { + label: string; + baseline: string; + candidate: string; + long?: boolean; +} + +function debugDimensionLabel(dimension: ComparisonDimension): string { + return dimension === "model" + ? "模型" + : dimension === "instruction" + ? "系统提示词" + : "Skills"; +} + +function debugSkillListLabel(skills: SelectedSkill[]): string { + return skills.length + ? skills + .map( + (skill) => + `${skill.name || skill.folder}${skill.version ? `@${skill.version}` : ""}`, + ) + .join("、") + : "无 Skill"; +} + +function debugCredentialLabel(change: DebugVariantChange): string { + return change.apiKeyLocked || change.apiKey + ? "临时凭据已配置" + : "服务端凭据"; +} + +function debugChangeDiffLines( + baseline: DebugVariantChange, + change: DebugVariantChange, +): DebugConfigurationDiffLine[] { + if (change.dimension === "model") { + const lines: DebugConfigurationDiffLine[] = [ + { + label: "Model ID", + baseline: baseline.modelName || "Studio 默认 Model ID", + candidate: change.modelName || "Studio 默认 Model ID", + }, + { + label: "Provider", + baseline: baseline.modelProvider || "Studio 默认 Provider", + candidate: change.modelProvider || "Studio 默认 Provider", + }, + { + label: "API Base", + baseline: baseline.modelApiBase || "Studio 默认 API Base", + candidate: change.modelApiBase || "Studio 默认 API Base", + long: true, + }, + { + label: "凭据", + baseline: debugCredentialLabel(baseline), + candidate: debugCredentialLabel(change), + }, + ]; + return lines.filter((line) => line.baseline !== line.candidate); + } + if (change.dimension === "instruction") { + return [ + { + label: "提示词内容", + baseline: baseline.instruction || "空提示词", + candidate: change.instruction || "空提示词", + long: true, + }, + ]; + } + return [ + { + label: "Skills 列表", + baseline: debugSkillListLabel(baseline.selectedSkills), + candidate: debugSkillListLabel(change.selectedSkills), + long: true, + }, + ]; +} + +function debugChangeDiffText( + draft: AgentDraft, + change: DebugVariantChange, +): string { + const agent = listConfigurableAgents(draft).find( + (item) => item.key === change.agentKey, + ); + if (!agent) return "Agent 已不在当前 Draft 中"; + const baseline = getNode(draft, agent.path); + if (change.dimension === "model") { + const before = [ + baseline.modelName || "Studio 默认 Model ID", + baseline.modelProvider || "Studio 默认 Provider", + baseline.modelApiBase || "Studio 默认 API Base", + ].join(" / "); + const after = [ + change.modelName || "Studio 默认 Model ID", + change.modelProvider || "Studio 默认 Provider", + change.modelApiBase || "Studio 默认 API Base", + ].join(" / "); + return `${before} → ${after}`; + } + if (change.dimension === "instruction") { + return `系统提示词 ${baseline.instruction.length} 字 → ${change.instruction.length} 字`; + } + return `${debugSkillListLabel(baseline.selectedSkills ?? [])} → ${debugSkillListLabel(change.selectedSkills)}`; +} + +function debugVariantHasA2uiAction( + variant: DebugVariant, + actionName: string, + nodeId: string, +): boolean { + return variant.messages.some((message) => + (message.blocks ?? []).some( + (block) => + block.kind === "a2ui" && + block.messages.some((a2uiMessage) => { + const update = ( + a2uiMessage as { + updateComponents?: { components?: A2uiComponent[] }; + } + ).updateComponents; + const components = Array.isArray(update?.components) + ? update.components + : []; + return Boolean( + components.some((component) => { + const componentAction = component.action as + | A2uiAction + | undefined; + return ( + component.id === nodeId && + (componentAction?.event?.name ?? component.id) === actionName + ); + }), + ); + }), + ), + ); +} + +function draftForDebugVariant( + baseline: AgentDraft, + variant: DebugVariant, +): AgentDraft { + if (variant.id === "baseline") return baseline; + return buildCandidateDraft(baseline, debugOverridesForVariant(variant)); +} + +function debugOverridesForVariant( + variant: DebugVariant, +): AgentComparisonOverride[] { + return debugChangesForVariant(variant).map((change) => ({ + agentKey: change.agentKey, + dimensions: [change.dimension], + model: + change.dimension === "model" + ? { + modelName: change.modelName, + modelProvider: change.modelProvider, + modelApiBase: change.modelApiBase, + } + : undefined, + instruction: + change.dimension === "instruction" ? change.instruction : undefined, + selectedSkills: + change.dimension === "skills" ? change.selectedSkills : undefined, + })); +} + +function effectiveDebugChanges( + draft: AgentDraft, + variant: DebugVariant, +): DebugVariantChange[] { + if (variant.id === "baseline") return []; + const effective = new Set( + effectiveComparisonOverrides(draft, debugOverridesForVariant(variant)).flatMap( + (override) => + override.dimensions.map( + (dimension) => `${override.agentKey}:${dimension}`, + ), + ), + ); + return debugChangesForVariant(variant).filter((change) => + effective.has(`${change.agentKey}:${change.dimension}`), + ); +} + +function debugComparisonSessionProblem( + draft: AgentDraft, + variants: DebugVariant[], + baselineApiKeys: Record, +): string { + if (variants.length < 2) return "请至少添加一个对照组"; + if (variants.length > 4) return "测试组最多为 4 个"; + + for (const variant of variants) { + if (variant.configOpen) return `请先保存「${variant.name}」的配置`; + } + for (const variant of variants) { + if (variant.phase === "starting" || variant.phase === "sending") { + return `请等待「${variant.name}」完成当前操作`; + } + } + for (const variant of variants) { + const problem = debugVariantConfigurationProblem(draft, variant); + if (problem) return `请先完成「${variant.name}」的配置:${problem}`; + } + + const seenConfigurations = new Set(); + for (const variant of variants) { + const configurationKey = debugVariantConfigurationKey(draft, variant); + if (seenConfigurations.has(configurationKey)) { + return `「${variant.name}」的配置与已有测试组相同`; + } + seenConfigurations.add(configurationKey); + } + + for (const variant of variants) { + let variantDraft: AgentDraft; + try { + variantDraft = draftForDebugVariant(draft, variant); + } catch { + return `「${variant.name}」的配置已失效,请重新选择 Agent`; + } + for (const { path, key } of listConfigurableAgents(variantDraft)) { + const modelNode = getNode(variantDraft, path); + const validation = validateModelConnection({ + modelName: modelNode.modelName ?? "", + modelProvider: modelNode.modelProvider ?? "", + modelApiBase: modelNode.modelApiBase ?? "", + apiKey: debugApiKeyForAgent(variant, key, baselineApiKeys), + studioApiBase: defaultModelApiBase( + draft.cloudProvider ?? "volcengine", + ), + }); + if (!validation.ok) { + return `请先完成「${variant.name}」的模型连接配置:${validation.reason}`; + } + } + } + return ""; +} + +function DebugVariantDifferenceSummary({ + draft, + baselineApiKeys, + changes, + configurableAgents, + variantName, +}: { + draft: AgentDraft; + baselineApiKeys: Record; + changes: DebugVariantChange[]; + configurableAgents: ReturnType; + variantName: string; +}) { + const [expanded, setExpanded] = useState(false); + const titleId = useId(); + const contentId = useId(); + const groups = summarizeDebugChanges( + changes, + configurableAgents.map((agent) => agent.key), + "", + "model", + ); + const totalCount = changes.length; + const visible = expanded + ? { groups, hiddenCount: 0 } + : previewDebugChangeSummary(groups, COMPARISON_DIFF_PREVIEW_LIMIT); + + return ( +
+
+

与基准组的配置差异

+ {totalCount} 项 +
+
+ {visible.groups.map((group) => { + const agentName = + configurableAgents.find((agent) => agent.key === group.agentKey) + ?.name ?? group.agentKey; + const groupTotalCount = + groups.find((item) => item.agentKey === group.agentKey)?.changes + .length ?? group.changes.length; + return ( +
+

+ {agentName} + {groupTotalCount} 项变化 +

+ {group.changes.map(({ change }) => { + const baseline = baselineDebugChange( + draft, + baselineApiKeys, + change.agentKey, + change.dimension, + ); + if (!baseline) return null; + return ( +
+ {debugDimensionLabel(change.dimension)} +
+ {debugChangeDiffLines(baseline, change).map((line) => ( +
+
{line.label}
+
+ + 基准 + + {line.baseline} + + + + + 本组 + + {line.candidate} + + +
+
+ ))} +
+
+ ); + })} +
+ ); + })} +
+ {totalCount > COMPARISON_DIFF_PREVIEW_LIMIT ? ( + + ) : null} +
+ ); +} + +type DebugChangeField = + | "agentKey" + | "dimension" + | "modelName" + | "modelProvider" + | "modelApiBase" + | "apiKey" + | "instruction"; + +function DebugVariantConfigurationPanel({ + variant, variants, - draftSnapshot, - input, - onInput, - onSend, - onStartVariant, - onDeployVariant, - onAddVariant, + draft, + busy, + onClose, onRemoveVariant, - onToggleConfig, onCompleteConfig, + onSelectChange, + onRemoveChange, onConfigChange, - onOpenTrace, + onSkillsChange, + onToggleApiKey, }: { - enabled: boolean; - disabledReason: string; + variant: DebugVariant; variants: DebugVariant[]; - draftSnapshot: string; - input: string; - onInput: (v: string) => void; - onSend: () => void; - onStartVariant: (id: string) => void; - onDeployVariant: (id: string) => void; - onAddVariant: () => void; + draft: AgentDraft; + busy: boolean; + onClose: () => void; onRemoveVariant: (id: string) => void; - onToggleConfig: (id: string) => void; onCompleteConfig: (id: string) => void; + onSelectChange: ( + id: string, + agentKey: string, + dimension: ComparisonDimension, + ) => void; + onRemoveChange: ( + id: string, + agentKey: string, + dimension: ComparisonDimension, + ) => void; onConfigChange: ( id: string, - field: "modelName" | "description" | "instruction", + field: DebugChangeField, value: string, ) => void; - onOpenTrace: (id: string) => void; + onSkillsChange: (id: string, skills: SelectedSkill[]) => void; + onToggleApiKey: (id: string) => void; }) { - const runningVariants = variants.filter((variant) => { - if (variant.phase !== "ready") return false; - return ( - variant.runtimeSnapshot === debugVariantSnapshot(draftSnapshot, variant) - ); - }); - const sending = variants.some((variant) => variant.phase === "sending"); - const canSend = runningVariants.length > 0 && !sending; + const configurableAgents = listConfigurableAgents(draft); + const configurationKey = debugVariantConfigurationKey(draft, variant); + const variantIndex = variants.findIndex((item) => item.id === variant.id); + const duplicateConfiguration = variants.findIndex( + (item) => debugVariantConfigurationKey(draft, item) === configurationKey, + ) !== variantIndex; + const configurationProblem = debugVariantConfigurationProblem(draft, variant); + const disabledReason = + configurationProblem || + (duplicateConfiguration ? "该配置与已有测试组相同" : ""); + const summaryTitleId = useId(); + const effectiveChanges = effectiveDebugChanges(draft, variant); + const changeSummary = summarizeDebugChanges( + effectiveChanges, + configurableAgents.map((agent) => agent.key), + variant.agentKey, + variant.dimension, + ); + const changeCountByAgent = new Map( + changeSummary.map((group) => [group.agentKey, group.changes.length]), + ); + const changedDimensions = new Set( + effectiveChanges.map( + (change) => `${change.agentKey}:${change.dimension}`, + ), + ); + const dimensionLabel = (dimension: ComparisonDimension) => + dimension === "model" + ? "模型" + : dimension === "instruction" + ? "系统提示词" + : "Skills"; return ( -
-
- {!enabled ? ( -
{disabledReason}
- ) : ( -
+ + +
+ } + > +
+ + +
+
+ {(["model", "instruction", "skills"] as ComparisonDimension[]).map( + (dimension) => { + const changed = changedDimensions.has( + `${variant.agentKey}:${dimension}`, + ); + return ( + + ); + }, + )} +
+ + {disabledReason && ( +

+ {disabledReason} +

+ )} + +
+ {variant.dimension === "model" && ( + <> + + + + + + )} + {variant.dimension === "instruction" && ( +