diff --git a/frontend/README.md b/frontend/README.md index 7563b6ef3..a1eb32735 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 @@ -145,6 +157,14 @@ server that `veadk frontend` launches — no separate backend. its generated internal proxy mounts AgentKit A2A center agents dynamically from the center ID, recall count, region, and OpenAPI endpoint. Remote names, descriptions, and capabilities come from the returned Agent Cards. + +Custom model endpoints used by comparison debugging require an endpoint-specific +temporary API Key and an exact server-side hostname allowlist. Studio operators +configure comma-separated hostnames with +`VEADK_STUDIO_DEBUG_MODEL_HOST_ALLOWLIST`; URLs, wildcards, and inherited +subdomains are not accepted. The endpoint must still use HTTPS and resolve to a +public address. + - **Code-package deployment**: upload a ZIP project from the add-Agent menu, inspect or edit its files in the existing code browser, then choose the region and public/VPC network before deploying it to AgentKit. The package @@ -432,6 +452,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..1b2c0a65a 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, @@ -27,7 +28,6 @@ import { Layers, Loader2, Plus, - RefreshCw, Rocket, Shapes, Sparkles, @@ -105,8 +105,10 @@ import { import { Blocks, ThinkingPlaceholder } from "../ui/Blocks"; import { DeploymentErrorMessage } from "../ui/DeploymentErrorMessage"; import { StudioConfirmDialog } from "../ui/StudioConfirmDialog"; +import { SecretVisibilityIcon } from "../ui/icons/SecretVisibilityIcon"; import { TraceDrawer } from "../ui/TraceDrawer"; import { isImeCompositionEvent } from "../ui/composerKeyboard"; +import type { A2uiAction, A2uiComponent } from "../a2ui/types"; import { createGeneratedAgentTestRun, createGeneratedAgentTestSession, @@ -139,6 +141,59 @@ import { } from "../adk/cloudProvider"; import { applyEvent, emptyAcc, type Block } from "../blocks"; import { customModelCredentialRequirements } from "./customModelCredentials"; +import { + invalidateTransientModelCredentials, + type TransientModelCredentialState, + validateModelConnection, +} from "./comparison/modelCredentials"; +import { + invalidateDebugVariantCredentials, + previewDebugChangeSummary, + removeDebugChange, + semanticDebugConfigurationKey, + summarizeDebugChanges, + switchPrimaryDebugChange, + updateDebugVariantConfiguration as preserveDebugVariantEvidence, +} from "./comparison/debugVariantState"; +import { DebugStreamRegistry } from "./comparison/debugStreamRegistry"; +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 +325,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 +1454,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 +1578,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 +2014,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 +2030,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 +2139,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 { @@ -2105,172 +2206,1212 @@ function debugSnapshotKey(draft: AgentDraft): string { function debugVariantSnapshot( draftSnapshot: string, - variant: Pick< - DebugVariant, - "modelName" | "description" | "instruction" | "optimizations" - >, + semanticConfigurationKey: string, ): string { return JSON.stringify({ draftSnapshot, - modelName: variant.modelName, - description: variant.description, - instruction: variant.instruction, - optimizations: variant.optimizations, + semanticConfigurationKey, }); } 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 semanticDebugConfigurationKey( + effectiveDebugChanges(draft, variant), + ); } -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 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" && ( +