diff --git a/src/lib/langs/en.json b/src/lib/langs/en.json index 1c0f2772..be11ddbe 100644 --- a/src/lib/langs/en.json +++ b/src/lib/langs/en.json @@ -303,6 +303,12 @@ "Reset": "Reset", "Agent Testing": "Agent Testing", "Test Suites": "Test Suites", + "suite": "suite", + "suites": "suites", + "case": "case", + "cases": "cases", + "run": "run", + "runs": "runs", "Test Suite": "Test Suite", "Test Cases": "Test Cases", "Test Case": "Test Case", @@ -350,6 +356,7 @@ "Hide details": "Hide details", "Clear selection": "Clear selection", "All Agents": "All Agents", + "Search agents": "Search agents", "Select Agent": "Select Agent", "Select all cases": "Select all cases", "Select case {name}": "Select case {name}", diff --git a/src/lib/langs/zh.json b/src/lib/langs/zh.json index 078783bf..0f2e8187 100644 --- a/src/lib/langs/zh.json +++ b/src/lib/langs/zh.json @@ -497,6 +497,12 @@ "Reset": "重置", "Agent Testing": "Agent 测试", "Test Suites": "测试套件", + "suite": "个套件", + "suites": "个套件", + "case": "个用例", + "cases": "个用例", + "run": "次运行", + "runs": "次运行", "Test Suite": "测试套件", "Test Cases": "测试用例", "Test Case": "测试用例", @@ -544,6 +550,7 @@ "Hide details": "收起详情", "Clear selection": "清除选择", "All Agents": "全部 Agent", + "Search agents": "搜索 Agent", "Select Agent": "选择 Agent", "Select all cases": "全选用例", "Select case {name}": "选择用例 {name}", diff --git a/src/lib/styles/app.scss b/src/lib/styles/app.scss index ba33f436..13c28868 100644 --- a/src/lib/styles/app.scss +++ b/src/lib/styles/app.scss @@ -15,6 +15,7 @@ @import "pages/instruction"; @import "pages/knowledge-base"; @import "pages/agent"; +@import "pages/agent-test"; @import "pages/settings"; @import "layout/vertical"; diff --git a/src/lib/styles/pages/_agent-test.scss b/src/lib/styles/pages/_agent-test.scss new file mode 100644 index 00000000..9a19511e --- /dev/null +++ b/src/lib/styles/pages/_agent-test.scss @@ -0,0 +1,880 @@ +/* + * Agent Testing page styles — the shared component layer for every page under + * src/routes/page/agent-test. + * + * The four pages were originally written against Bootstrap 5 (.card, .row, + * .btn-*, .modal-*, .form-control, .badge …), which this app does not ship — + * the whole feature rendered as unstyled document flow. Rather than inline the + * same 15-utility Tailwind string on every one of ~500 elements, the recurring + * pieces are named once here behind an `ats-` prefix, exactly as _chat.scss and + * _knowledge-base.scss do for their own pages. One-off spacing and layout stays + * in the markup as Tailwind utilities. + * + * `ats-`, not `at-`: _agent.scss already owns `at-` for the agent detail tabs + * (.at-pane / .at-hide), and two features sharing one prefix is exactly the + * collision the prefix convention exists to prevent. + * + * Palette, radii and shadows are matched to the already-migrated pages + * (roles / users / task / setting) so Agent Testing does not read as a + * different product: rounded-2xl cards on a white/gray-800 ground, a 1px + * gray-100/gray-700 divider between card sections, and uppercase 11px + * table headers. + * + * Status tones (`.ats-tone-success`, `.ats-tone-danger`, …) are spelled + * out as real classes on purpose. The pages pick a tone at runtime from + * `statusColor()`, and a constructed `bg-{tone}` string is invisible to + * Tailwind's source scanner — it only emitted `bg-secondary` at all because + * some other file happened to name it literally. Declaring the tones here + * removes that dependency entirely. + */ + +/* ======================================================================== + * Tone tokens + * ======================================================================== + * `statusColor()` in $lib/helpers/utils/agent-test.js returns one of these + * six names; `ats-tone-*` maps the name onto a single `--ats-tone` custom + * property that the badge / dot / alert rules below all read from. + */ +.ats-tone-primary { --ats-tone: var(--color-primary); } +.ats-tone-secondary { --ats-tone: var(--color-secondary); } +.ats-tone-success { --ats-tone: var(--color-success); } +.ats-tone-info { --ats-tone: var(--color-info); } +.ats-tone-warning { --ats-tone: var(--color-warning); } +.ats-tone-danger { --ats-tone: var(--color-danger); } +.ats-tone-dark { --ats-tone: var(--color-dark); } + +/* ======================================================================== + * Card shell (replaces .card / .card-body / .card-title / .border-bottom) + * ======================================================================== */ + +.ats-card { + border-radius: 1rem; + background-color: rgb(255 255 255); + box-shadow: + 0 20px 25px -5px rgb(15 23 42 / 0.1), + 0 8px 10px -6px rgb(15 23 42 / 0.1); + outline: 1px solid rgb(0 0 0 / 0.05); + outline-offset: -1px; + + .dark & { + background-color: rgb(31 41 55); + outline-color: rgb(255 255 255 / 0.1); + } +} + +/* A single band inside the card. `ats-card-section` carries the padding; + `ats-card-divider` adds the hairline that separated Bootstrap's stacked + `.card-body.border-bottom` blocks. */ +.ats-card-section { + padding: 1rem 1.5rem; +} + +.ats-card-divider { + border-bottom: 1px solid rgb(243 244 246); + + .dark & { + border-bottom-color: rgb(55 65 81); + } +} + +/* The body band is roomier than the header/filter bands, matching the + `p-4 sm:p-6` the roles and users pages use for their table area. */ +.ats-card-body { + padding: 1rem; + + @media (min-width: 640px) { + padding: 1.5rem; + } +} + +.ats-card-title { + margin: 0; + font-size: 1rem; + font-weight: 600; + line-height: 1.4; + color: var(--color-dark); + + .dark & { + color: rgb(243 244 246); + } +} + +.ats-card-subtitle { + margin: 0; + font-size: 0.75rem; + color: var(--color-muted); +} + +/* Round tinted glyph that opens the card header on every migrated page. */ +.ats-card-icon { + display: flex; + flex: none; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 999px; + background-color: color-mix(in srgb, var(--color-primary) 10%, transparent); + color: var(--color-primary); + + & i { + font-size: 1.25rem; + line-height: 1; + } +} + +/* ======================================================================== + * Buttons (replaces .btn and every .btn-* variant) + * ======================================================================== */ + +.ats-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + height: 2.5rem; + padding: 0 1rem; + border: 1px solid transparent; + border-radius: 0.375rem; + background-color: transparent; + font-size: 0.875rem; + font-weight: 500; + line-height: 1; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, + color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease; + + & i { + font-size: 1rem; + line-height: 1; + } + + &:active:not(:disabled) { + transform: translateY(1px); + } + + &:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +/* Compact variant (was .btn-sm). */ +.ats-btn-sm { + height: 2rem; + padding: 0 0.625rem; + font-size: 0.8125rem; + + & i { + font-size: 0.9375rem; + } +} + +/* Square icon-only button, sized to line up with .ats-btn-sm in a row of + table actions (was .btn.btn-sm with a single inside). */ +.ats-btn-icon { + width: 2rem; + height: 2rem; + padding: 0; +} + +/* ---- Solid variants ---- */ + +.ats-btn-primary { + background-color: var(--color-primary); + border-color: var(--color-primary); + color: rgb(255 255 255); + box-shadow: 0 1px 2px rgb(15 23 42 / 0.08); + + &:hover:not(:disabled) { + background-color: var(--color-primary-hover); + border-color: var(--color-primary-hover); + } +} + +.ats-btn-secondary { + background-color: rgb(255 255 255); + border-color: rgb(229 231 235); + color: rgb(55 65 81); + + &:hover:not(:disabled) { + background-color: rgb(243 244 246); + border-color: rgb(209 213 219); + color: rgb(17 24 39); + } + + .dark & { + background-color: rgb(55 65 81); + border-color: rgb(75 85 99); + color: rgb(229 231 235); + + &:hover:not(:disabled) { + background-color: rgb(75 85 99); + border-color: rgb(107 114 128); + } + } +} + +.ats-btn-warning { + background-color: var(--color-warning); + border-color: var(--color-warning); + color: rgb(255 255 255); + box-shadow: 0 1px 2px rgb(15 23 42 / 0.08); + + &:hover:not(:disabled) { + background-color: color-mix(in srgb, var(--color-warning), black 12%); + border-color: color-mix(in srgb, var(--color-warning), black 12%); + } +} + +/* ---- Soft variants: tinted fill that fills in solid on hover ---- + (was .btn-soft-primary / -secondary / -info / -danger) */ + +.ats-btn-soft { + background-color: color-mix(in srgb, var(--ats-tone) 12%, transparent); + color: var(--ats-tone); + + &:hover:not(:disabled) { + background-color: var(--ats-tone); + color: rgb(255 255 255); + } + + &:focus-visible { + outline-color: var(--ats-tone); + } +} + +/* ---- Outline variant (was .btn-outline-danger) ---- */ + +.ats-btn-outline { + border-color: var(--ats-tone); + color: var(--ats-tone); + + &:hover:not(:disabled) { + background-color: var(--ats-tone); + color: rgb(255 255 255); + } + + &:focus-visible { + outline-color: var(--ats-tone); + } +} + +/* ---- Inline text button (was .btn-link) ---- */ + +.ats-btn-link { + height: auto; + padding: 0.125rem 0.25rem; + border: 0; + border-radius: 0.25rem; + color: var(--color-primary); + font-size: 0.8125rem; + + &:hover:not(:disabled) { + background-color: color-mix(in srgb, var(--color-primary) 10%, transparent); + text-decoration: underline; + } +} + +/* ======================================================================== + * Form controls (replaces .form-label / .form-control / .form-select / + * .form-text / .form-check / .form-switch) + * ======================================================================== */ + +.ats-label { + display: block; + margin-bottom: 0.375rem; + font-size: 0.8125rem; + font-weight: 500; + color: rgb(55 65 81); + + .dark & { + color: rgb(209 213 219); + } +} + +/* One rule for text inputs and textareas. `height` is deliberately left to the + element: inputs get a fixed 2.5rem, textareas size to their `rows`. + + There is no select rule here on purpose. Every dropdown on these pages is the + shared Select component, which carries its own trigger styling -- matched to + this same 2.5rem / 0.375rem / gray-200 look, so the two line up inside a grid + row. Reintroducing a native changeAgentFilter(e)} /> -
+
-
+
-
+
{#if loadErrorText} - -
- {/if} - -
-
-
-
-
{$_('Case Results')} ({results.length})
+ {/if} + +
+
+
+ + + +
+
{$_('Case Results')}
+

+ {results.length} {results.length === 1 ? $_('case') : $_('cases')} +

+
-
- {#if results.length === 0} -

+

+
+ {#if results.length === 0} +
+

{#if run.error} {$_('Nothing ran -- see the reason above.')} {:else} {isFinished ? $_('This run produced no case results.') : $_('No case results yet.')} {/if}

- {:else} - {#each results as result (result.id)} - {@const isExpanded = expandedIds.includes(result.id)} -
-
-
- {$_(result.status)} - {result.caseName} - {#if result.model} - - {result.model} - {/if} - {formatDuration(result.durationMs)} - {#if result.conversationId} - {result.conversationId} - {/if} -
- +
+ {:else} + {#each results as result (result.id)} + {@const isExpanded = expandedIds.includes(result.id)} +
+
+
+ {$_(result.status)} + {result.caseName} + {#if result.model} + + + {result.model} + + {/if} + {formatDuration(result.durationMs)} + {#if result.conversationId} + {result.conversationId} + {/if}
+ +
- {#if result.error} -
- -
- {/if} + {#if result.error} + + {/if} - {#if isExpanded} -
- {#each result.turns as turn (turn.index)} -
-
{$_('Turn')} {turn.index + 1}
-
-
{$_('User')}
-
{turn.userMessage}
-
-
-
{$_('Agent output')}
-
{turn.output || '--'}
-
- {#if turn.assertions?.length > 0} - {@render assertionTable(turn.assertions)} - {:else} -

{$_('No assertions on this turn.')}

- {/if} + {#if isExpanded} +
+ {#each result.turns as turn (turn.index)} +
+
{$_('Turn')} {turn.index + 1}
+
+
{$_('User')}
+
{turn.userMessage}
- {/each} - - {#if result.assertions?.length > 0} -
-
{$_('Case Assertions')}
- {@render assertionTable(result.assertions)} +
+
{$_('Agent output')}
+
{turn.output || '--'}
- {/if} - -
-
{$_('Observed Tool Calls')} ({result.observedToolCalls?.length || 0})
- {#if result.observedToolCalls?.length > 0} -
- - - - - - - - - - - - {#each result.observedToolCalls as call, i (i)} - - - - - - - - {/each} - -
{$_('Turn')}{$_('Function')}{$_('Outcome')}{$_('Args')}{$_('Result')}
{call.turnIndex + 1}{call.functionName} - {#if call.outcome === 'Blocked'} - - {$_('Blocked')} - - {:else} - {$_(call.outcome)} - {/if} - {call.argsJson || '--'}{call.resultContent || '--'}
-
+ {#if turn.assertions?.length > 0} + {@render assertionTable(turn.assertions)} {:else} -

{$_('The agent called no tools during this case.')}

+

{$_('No assertions on this turn.')}

{/if}
+ {/each} + + {#if result.assertions?.length > 0} +
+
{$_('Case Assertions')}
+ {@render assertionTable(result.assertions)} +
+ {/if} + +
+
+ {$_('Observed Tool Calls')} ({result.observedToolCalls?.length || 0}) +
+ {#if result.observedToolCalls?.length > 0} +
+ + + + + + + + + + + + {#each result.observedToolCalls as call, i (i)} + + + + + + + + {/each} + +
{$_('Turn')}{$_('Function')}{$_('Outcome')}{$_('Args')}{$_('Result')}
{call.turnIndex + 1}{call.functionName} + {#if call.outcome === 'Blocked'} + + {$_('Blocked')} + + {:else} + {$_(call.outcome)} + {/if} + {call.argsJson || '--'}{call.resultContent || '--'}
+
+ {:else} +

+ {$_('The agent called no tools during this case.')} +

+ {/if}
- {/if} -
- {/each} - {/if} -
+
+ {/if} +
+ {/each} + {/if}