Prompt Ab Testing Comparator
blockaiSide-by-side prompt variation comparison shootout with automated LLM judge scoring, win rates, and token diffs. Features benchmark test payload, side-by-side arena with latency, cost, and judge critique, plus aggregate win-rate scorecard.
Installation
$pnpm dlx uipkge-ng@latest add prompt-ab-testing-comparator$npx uipkge-ng@latest add prompt-ab-testing-comparator$yarn dlx uipkge-ng@latest add prompt-ab-testing-comparator$bunx uipkge-ng@latest add prompt-ab-testing-comparatorManifest:
https://uipkge.dev/r/angular/prompt-ab-testing-comparator.jsonInstalls to:src/app/components/blocks/prompt-ab-testing-comparator/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
TestCaseinterface TestCase {
id: string
number: string
title: string
prompt: string
componentTarget: string
category: string
}Files installed (2)
src/app/components/blocks/prompt-ab-testing-comparator/prompt-ab-testing-comparator.component.ts41.3 kB
// Side-by-side prompt variation comparison shootout: benchmark test payload, Prompt A // (baseline) vs Prompt B (challenger) arena with latency / cost / judge critique, plus an // aggregate win-rate scorecard with rubric matrix and golden runs. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { AlertCircle, Check, CheckCircle2, Clock, Code2, Coins, Copy, Cpu, LucideAngularModule, Play, Rocket, Scale, ShieldCheck, Sparkles, Terminal, Trophy, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' interface TestCase { id: string number: string title: string prompt: string componentTarget: string category: string } const testCases: TestCase[] = [ { id: 'test-18', number: '#18', title: 'Button Component with CVA Variants', prompt: 'Generate a polymorphic Button component with cva variants in Vue 3.5 <script setup lang="ts">', componentTarget: 'Button.vue', category: 'Control / Primitive', }, { id: 'test-24', number: '#24', title: 'Card Container with Header and Action', prompt: 'Generate a compound Card component with CardHeader, CardTitle, and CardAction in Vue 3.5', componentTarget: 'Card.vue', category: 'Layout / Primitive', }, { id: 'test-31', number: '#31', title: 'Tabs Navigation with Animated Indicator', prompt: 'Create a headless accessible Tabs component with horizontal/vertical orientation support in Vue 3.5', componentTarget: 'Tabs.vue', category: 'Navigation', }, { id: 'test-42', number: '#42', title: 'Semantic Status Badge Component', prompt: 'Build a Badge component with success, warning, destructive, and outline variants in Tailwind v4', componentTarget: 'Badge.vue', category: 'Feedback / Tag', }, ] const promptAData = { id: 'prompt-a', label: 'Prompt A', variantName: 'Baseline Control', model: 'Claude 3.5 Sonnet', temperature: '0.7', maxTokens: 2048, latency: '1.42s', outputTokens: 340, cost: '$0.0018', judgeScore: 82, judgeStatus: 'Baseline Control', critique: 'Generated inline cva variants in index.ts causing potential SSR circular imports.', systemPrompt: `You are a Vue 3 component generator. Generate standard Vue SFC components with Tailwind CSS and class-variance-authority for variants.`, outputCode: `<script setup lang="ts"> import { buttonVariants } from './index' interface Props { variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' size?: 'default' | 'sm' | 'lg' | 'icon' asChild?: boolean class?: string } const props = withDefaults(defineProps<Props>(), { variant: 'default', size: 'default', }) </script> <template> <button :class="buttonVariants({ variant: props.variant, size: props.size, class: props.class })" > <slot /> </button> </template>`, } const promptBData = { id: 'prompt-b', label: 'Prompt B', variantName: 'Challenger (With Rules)', model: 'Claude 3.5 Sonnet', temperature: '0.7', maxTokens: 2048, latency: '1.28s', latencyDelta: '-0.14s (-9.8%)', outputTokens: 320, tokensDelta: '-20 tok (-5.9%)', cost: '$0.0016', costDelta: '-$0.0002 (-11.1%)', judgeScore: 96, judgeDelta: '+14 pts', judgeStatus: 'Winner · +14 pts', critique: 'Clean architectural separation with .variants.ts sidecar and strict 120-col Prettier formatting.', systemPrompt: `You are a specialized Vue 3.5 SFC generator for UIPKGE. Adhere strictly to the sidecar variant architecture: 1) ALWAYS isolate CVA variant definitions into a standalone [name].variants.ts file to eliminate Vue SSR circular-import deadlocks, 2) Use <script setup lang="ts"> with Primitive from reka-ui for polymorphic as/asChild rendering, 3) Follow 120-column Prettier formatting without semicolons.`, outputCode: `<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { Primitive } from 'reka-ui' import { cn } from '@/lib/utils' import { buttonVariants, type ButtonVariants } from './button.variants' interface Props { as?: string asChild?: boolean variant?: ButtonVariants['variant'] size?: ButtonVariants['size'] class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { as: 'button', }) </script> <template> <Primitive data-slot="button" :data-variant="variant" :data-size="size" :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)" > <slot /> </Primitive> </template>`, } const rubricDimensions = [ { id: 'ssr-safety', name: 'SSR Safety & Circular Import Guard', promptAScore: '68 / 100', promptBScore: '98 / 100', delta: '+30 pts', insight: 'Prompt B isolates variants into button.variants.ts, preventing component <-> index.ts SSR deadlocks.', }, { id: 'polymorphism', name: 'Polymorphism & Reka UI Primitive', promptAScore: '84 / 100', promptBScore: '96 / 100', delta: '+12 pts', insight: 'Prompt B wraps Reka UI Primitive with asChild and data-slot attributes for full slot delegation.', }, { id: 'typescript-typing', name: 'TypeScript HTMLAttributes & Variants', promptAScore: '82 / 100', promptBScore: '95 / 100', delta: '+13 pts', insight: "Uses HTMLAttributes['class'] and ButtonVariants['variant'] instead of generic string props.", }, { id: 'token-efficiency', name: 'Token Generation Efficiency', promptAScore: '88 / 100 (340 tok)', promptBScore: '94 / 100 (320 tok)', delta: '+6 pts (-5.9% tok)', insight: 'Direct script setup syntax eliminates 20 tokens of redundant wrapper code per generation.', }, { id: 'style-conformance', name: 'Prettier & 120-Col Print Width', promptAScore: '88 / 100', promptBScore: '97 / 100', delta: '+9 pts', insight: 'Strict adherence to single quotes, no semicolons, 2-space indentation, and sorted utility classes.', }, ] const summaryMetrics = [ { id: 'win-rate', label: 'Overall Win Rate', value: '84.0%', subvalue: '42 of 50 Tests Won', badge: 'Dominant Win', badgeVariant: 'success' as const, description: 'Prompt B outperformed or matched Prompt A in 47 / 50 benchmark cases.', highlight: true, }, { id: 'quality-lift', label: 'Quality Score Lift', value: '+18.4%', subvalue: '96.1 vs 81.2 Avg Score', badge: '+14.9 pts avg', badgeVariant: 'success' as const, description: 'Statistically significant improvement in architectural correctness.', highlight: true, }, { id: 'latency-delta', label: 'Avg Response Latency', value: '1.28s', subvalue: '-9.8% vs Baseline (1.42s)', badge: '-140ms faster', badgeVariant: 'secondary' as const, description: 'Lower token payload reduces end-to-end LLM response streaming latency.', highlight: false, }, { id: 'token-savings', label: 'Avg Token Economy', value: '320 tok', subvalue: '-5.9% Token Reduction', badge: '20 tok / call saved', badgeVariant: 'secondary' as const, description: 'Est. $20.00 / month cost savings per 100,000 component generation calls.', highlight: false, }, ] const goldenTests = [ { id: 'gt-1', name: '#01 Button with CVA Variants', category: 'Control', promptAScore: '82', promptBScore: '96', delta: '+14', winner: 'Prompt B', }, { id: 'gt-2', name: '#02 Card Container with Action', category: 'Layout', promptAScore: '79', promptBScore: '98', delta: '+19', winner: 'Prompt B', }, { id: 'gt-3', name: '#03 Tabs with Indicator Slider', category: 'Navigation', promptAScore: '80', promptBScore: '95', delta: '+15', winner: 'Prompt B', }, { id: 'gt-4', name: '#04 Semantic Status Badge', category: 'Feedback', promptAScore: '86', promptBScore: '97', delta: '+11', winner: 'Prompt B', }, { id: 'gt-5', name: '#05 Dropdown Menu Compound', category: 'Overlay', promptAScore: '81', promptBScore: '94', delta: '+13', winner: 'Prompt B', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-prompt-ab-testing-comparator, [ui-prompt-ab-testing-comparator]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div data-uipkge data-slot="prompt-ab-testing-comparator" [class]="rootClass"> <!-- Production Promotion Success Banner (if promoted) --> @if (isPromoted()) { <div class="border-success/30 bg-success/10 text-foreground dark:text-foreground flex flex-col items-start justify-between gap-3 rounded-lg border p-4 shadow-xs sm:flex-row sm:items-center" > <div class="flex items-center gap-3"> <div class="bg-success flex size-9 shrink-0 items-center justify-center rounded-md text-white shadow-xs"> <lucide-icon [img]="Rocket" class="size-4" aria-hidden="true" /> </div> <div class="space-y-0.5"> <p class="text-foreground text-sm font-semibold"> Prompt B (Challenger with Sidecar Isolation) Promoted to Production </p> <p class="text-muted-foreground text-xs"> 100% of live component generation traffic is now routed through Prompt B. Expected quality lift: +18.4%. </p> </div> </div> <span ui-badge variant="success" class="font-semibold"> Active in Production </span> </div> } <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> {{ experimentTitle }} </h2> <span ui-badge variant="outline" class="font-mono text-xs"> {{ testSampleCount }} </span> <span ui-badge variant="success" class="border-success/20 bg-success/10 text-success gap-1.5 font-medium"> <lucide-icon [img]="Sparkles" class="size-3" aria-hidden="true" /> <span>Prompt B Winner · +18.4% Quality Lift</span> </span> </div> <p class="text-muted-foreground text-xs leading-relaxed sm:text-sm"> Automated LLM Judge Shootout · Evaluator: <span class="text-foreground font-medium">Claude 3.5 Sonnet (Temp 0.0)</span> · <span class="text-foreground font-medium">50 Test Cases</span> </p> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2.5"> <button ui-button size="sm" variant="outline" class="gap-1.5 shadow-xs" [disabled]="isRunning()" (click)="runComparison()" > @if (isRunning()) { <span class="border-primary size-3.5 animate-spin rounded-full border-2 border-t-transparent"></span> } @else { <lucide-icon [img]="Play" class="size-3.5 fill-current" aria-hidden="true" /> } <span>{{ isRunning() ? 'Evaluating 50 Prompts...' : 'Run A/B Comparison' }}</span> <kbd class="bg-muted text-muted-foreground hidden items-center rounded px-1.5 py-0.5 font-mono text-xs sm:inline-flex" > ⌘↵ </kbd> </button> <button ui-button size="sm" variant="default" [disabled]="isPromoted()" class="gap-1.5 shadow-xs" (click)="promotePromptB()" > <lucide-icon [img]="Rocket" class="size-3.5" aria-hidden="true" /> <span>{{ isPromoted() ? 'Prompt B Active in Prod' : 'Promote Prompt B to Production' }}</span> </button> </div> </div> <!-- Common Input Test Prompt Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Code2" class="size-4" /> </div> <div> <h3 ui-card-title class="text-sm font-semibold">Common Input Test Prompt (Evaluation Benchmark)</h3> <p ui-card-description class="text-xs"> Standardized input payload dispatched to both prompt candidate configurations </p> </div> </div> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="font-mono text-xs"> {{ currentTestCase().number }} of 50 </span> <span ui-badge variant="secondary" class="text-xs"> {{ currentTestCase().category }} </span> </div> </div> </div> <div ui-card-content class="space-y-3 pt-0"> <!-- Test Prompt Text --> <div class="border-border bg-muted/40 text-foreground flex items-center justify-between rounded-md border p-3" > <div class="flex items-center gap-2"> <lucide-icon [img]="Terminal" class="text-primary size-4 shrink-0" /> <code class="font-mono text-xs leading-relaxed font-semibold"> "{{ currentTestCase().prompt }}" </code> </div> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-7 px-2 text-xs" (click)="copyToClipboard('test-prompt', currentTestCase().prompt)" > @if (copiedState()['test-prompt']) { <lucide-icon [img]="Check" class="text-success mr-1 size-3.5" /> } @else { <lucide-icon [img]="Copy" class="mr-1 size-3.5" /> } <span>{{ copiedState()['test-prompt'] ? 'Copied' : 'Copy' }}</span> </button> </div> <!-- Meta chips / specifications --> <div class="flex flex-wrap items-center gap-2 pt-1"> <span class="text-muted-foreground text-xs font-medium">Benchmark Spec:</span> <span class="border-border bg-muted/30 text-foreground rounded-md border px-2 py-0.5 font-mono text-xs"> target: {{ currentTestCase().componentTarget }} </span> <span class="border-border bg-muted/30 text-foreground rounded-md border px-2 py-0.5 font-mono text-xs"> framework: Vue 3.5 SFC </span> <span class="border-border bg-muted/30 text-foreground rounded-md border px-2 py-0.5 font-mono text-xs"> engine: class-variance-authority + reka-ui </span> <span class="border-border bg-muted/30 text-foreground rounded-md border px-2 py-0.5 font-mono text-xs"> isolation: zero-ssr-circular-imports </span> </div> </div> </div> <!-- 2-Column Side-by-Side Prompt Arena --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2"> <!-- Left Column: Prompt A (Baseline - Control) --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <span ui-badge variant="secondary" class="font-mono text-xs font-semibold"> {{ promptA.label }} · {{ promptA.variantName }} </span> </div> <p class="text-muted-foreground text-xs"> Model: <span class="text-foreground font-medium">{{ promptA.model }}</span> · Temp: <span class="font-mono font-medium tabular-nums">{{ promptA.temperature }}</span> </p> </div> <div class="flex items-center gap-1.5"> <span ui-badge variant="outline" class="border-border/80 font-mono text-xs"> Score: <span class="text-foreground ml-1 font-bold tabular-nums">82/100</span> </span> </div> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <!-- Tabs for Prompt A --> <ui-tabs defaultValue="output" class="w-full"> <div class="flex items-center justify-between"> <ui-tabs-list class="grid w-64 grid-cols-2"> <button ui-tabs-trigger value="output">Generated Output</button> <button ui-tabs-trigger value="prompt">System Prompt</button> </ui-tabs-list> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-7 px-2 text-xs" (click)="copyToClipboard('prompt-a-code', promptA.outputCode)" > @if (copiedState()['prompt-a-code']) { <lucide-icon [img]="Check" class="text-success mr-1 size-3.5" /> } @else { <lucide-icon [img]="Copy" class="mr-1 size-3.5" /> } <span>{{ copiedState()['prompt-a-code'] ? 'Copied' : 'Copy' }}</span> </button> </div> <ui-tabs-content value="output" class="mt-3"> <div class="relative"> <pre class="border-border bg-muted/40 text-foreground max-h-[300px] overflow-x-auto overflow-y-auto rounded-md border p-3 font-mono text-xs leading-relaxed" ><code>{{ promptA.outputCode }}</code></pre> </div> </ui-tabs-content> <ui-tabs-content value="prompt" class="mt-3"> <div class="border-border bg-muted/20 text-foreground max-h-[300px] min-h-[160px] overflow-y-auto rounded-md border p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap" > {{ promptA.systemPrompt }} </div> </ui-tabs-content> </ui-tabs> <!-- Performance Metrics Bar --> <div class="border-border bg-muted/20 grid grid-cols-3 gap-2 rounded-lg border p-3 text-center"> <div class="space-y-0.5"> <span class="text-muted-foreground flex items-center justify-center gap-1 text-xs"> <lucide-icon [img]="Clock" class="size-3" /> <span>Latency</span> </span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ promptA.latency }} </p> </div> <div class="space-y-0.5"> <span class="text-muted-foreground flex items-center justify-center gap-1 text-xs"> <lucide-icon [img]="Cpu" class="size-3" /> <span>Output Tokens</span> </span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ promptA.outputTokens }} </p> </div> <div class="space-y-0.5"> <span class="text-muted-foreground flex items-center justify-center gap-1 text-xs"> <lucide-icon [img]="Coins" class="size-3" /> <span>Cost / Call</span> </span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ promptA.cost }} </p> </div> </div> <!-- Judge Score & Critique Box --> <div class="border-border bg-card/60 space-y-2 rounded-lg border p-3.5 shadow-xs"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <lucide-icon [img]="Scale" class="text-muted-foreground size-4" /> <span class="text-foreground text-xs font-semibold">Judge Score</span> </div> <div class="flex items-center gap-2"> <span class="text-foreground font-mono text-base font-bold tabular-nums"> {{ promptA.judgeScore }} / 100 </span> <span ui-badge variant="outline" class="text-xs"> {{ promptA.judgeStatus }} </span> </div> </div> <div class="border-border/60 bg-muted/40 text-muted-foreground flex items-start gap-2 rounded-md border p-2.5 text-xs leading-relaxed" > <lucide-icon [img]="AlertCircle" class="text-warning mt-0.5 size-3.5 shrink-0" /> <span><strong>Critique:</strong> {{ promptA.critique }}</span> </div> </div> </div> </div> <!-- Right Column: Prompt B (Challenger with Rules - WINNER) --> <div ui-card class="border-success/40 bg-success/[0.015] border-success/30 dark:bg-success/10] shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <span ui-badge variant="success" class="border-success/30 bg-success/15 text-success font-mono text-xs font-semibold" > {{ promptB.label }} · {{ promptB.variantName }} </span> <span ui-badge variant="success" class="gap-1 font-semibold"> <lucide-icon [img]="Trophy" class="size-3" /> <span>{{ promptB.judgeStatus }}</span> </span> </div> <p class="text-muted-foreground text-xs"> Model: <span class="text-foreground font-medium">{{ promptB.model }}</span> · Temp: <span class="font-mono font-medium tabular-nums">{{ promptB.temperature }}</span> </p> </div> <div class="flex items-center gap-1.5"> <span ui-badge variant="success" class="border-success/30 bg-success/10 text-success font-mono text-xs font-bold" > Score: <span class="ml-1 tabular-nums">96/100</span> </span> </div> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <!-- Tabs for Prompt B --> <ui-tabs defaultValue="output" class="w-full"> <div class="flex items-center justify-between"> <ui-tabs-list class="grid w-64 grid-cols-2"> <button ui-tabs-trigger value="output">Generated Output</button> <button ui-tabs-trigger value="prompt">System Prompt</button> </ui-tabs-list> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-7 px-2 text-xs" (click)="copyToClipboard('prompt-b-code', promptB.outputCode)" > @if (copiedState()['prompt-b-code']) { <lucide-icon [img]="Check" class="text-success mr-1 size-3.5" /> } @else { <lucide-icon [img]="Copy" class="mr-1 size-3.5" /> } <span>{{ copiedState()['prompt-b-code'] ? 'Copied' : 'Copy' }}</span> </button> </div> <ui-tabs-content value="output" class="mt-3"> <div class="relative"> <pre class="border-border bg-muted/40 text-foreground max-h-[300px] overflow-x-auto overflow-y-auto rounded-md border p-3 font-mono text-xs leading-relaxed" ><code>{{ promptB.outputCode }}</code></pre> </div> </ui-tabs-content> <ui-tabs-content value="prompt" class="mt-3"> <div class="border-border bg-muted/20 text-foreground max-h-[300px] min-h-[160px] overflow-y-auto rounded-md border p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap" > {{ promptB.systemPrompt }} </div> </ui-tabs-content> </ui-tabs> <!-- Performance Metrics Bar with Deltas --> <div class="border-success/20 bg-success/[0.04] grid grid-cols-3 gap-2 rounded-lg border p-3 text-center"> <div class="space-y-0.5"> <span class="text-muted-foreground flex items-center justify-center gap-1 text-xs"> <lucide-icon [img]="Clock" class="text-success size-3" /> <span>Latency</span> </span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ promptB.latency }} </p> <p class="text-success text-success text-xs font-semibold tabular-nums"> {{ promptB.latencyDelta }} </p> </div> <div class="space-y-0.5"> <span class="text-muted-foreground flex items-center justify-center gap-1 text-xs"> <lucide-icon [img]="Cpu" class="text-success size-3" /> <span>Output Tokens</span> </span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ promptB.outputTokens }} </p> <p class="text-success text-success text-xs font-semibold tabular-nums"> {{ promptB.tokensDelta }} </p> </div> <div class="space-y-0.5"> <span class="text-muted-foreground flex items-center justify-center gap-1 text-xs"> <lucide-icon [img]="Coins" class="text-success size-3" /> <span>Cost / Call</span> </span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ promptB.cost }} </p> <p class="text-success text-success text-xs font-semibold tabular-nums"> {{ promptB.costDelta }} </p> </div> </div> <!-- Judge Score & Critique Box --> <div class="border-success/30 bg-success/[0.04] space-y-2 rounded-lg border p-3.5 shadow-xs"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <lucide-icon [img]="Trophy" class="text-success size-4" /> <span class="text-foreground text-xs font-semibold">Judge Score</span> </div> <div class="flex items-center gap-2"> <span class="text-success text-success font-mono text-base font-bold tabular-nums"> {{ promptB.judgeScore }} / 100 </span> <span ui-badge variant="success" class="text-xs font-semibold"> {{ promptB.judgeDelta }} </span> </div> </div> <div class="text-muted-foreground border-success/30 bg-success/[0.06] flex items-start gap-2 rounded-md border p-2.5 text-xs leading-relaxed" > <lucide-icon [img]="CheckCircle2" class="text-success mt-0.5 size-3.5 shrink-0" /> <span><strong>Critique:</strong> {{ promptB.critique }}</span> </div> </div> </div> </div> </div> <!-- Automated Judge Verdict Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-foreground text-base font-semibold"> Automated LLM Judge Benchmark Verdict </h3> <span ui-badge variant="success" class="border-success/20 bg-success/10 text-success font-mono text-xs"> Prompt B Won 42 of 50 Tests · 84% Win Rate </span> </div> <p ui-card-description class="mt-1 text-xs"> Direct comparison of code quality, SSR compatibility, type correctness, and token economics across 50 golden evaluation prompts. </p> </div> <div class="border-border/80 bg-muted/30 flex items-center gap-2 self-start rounded-lg border px-3 py-1.5 text-xs sm:self-auto" > <span class="text-muted-foreground">Judge Model:</span> <span class="text-foreground font-mono font-semibold">Claude 3.5 Sonnet (Temp 0.0)</span> </div> </div> </div> <div ui-card-content class="space-y-6"> <!-- 4 Primary Aggregate Metric Summary Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> @for (metric of summaryMetrics; track metric.id) { <div ui-card [class]=" cn( 'border-border bg-card/70 hover:border-border/80 shadow-xs transition-colors', metric.highlight && 'border-success/30 bg-success/[0.02] dark:bg-success/10]' ) " > <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <h4 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> {{ metric.label }} </h4> <span ui-badge [variant]="metric.badgeVariant" class="text-xs font-medium tabular-nums"> {{ metric.badge }} </span> </div> </div> <div ui-card-content class="space-y-1.5 pb-4"> <div class="flex items-baseline gap-1.5"> <span [class]=" cn( 'text-2xl font-bold tracking-tight tabular-nums sm:text-3xl', metric.highlight ? 'text-success' : 'text-foreground' ) " > {{ metric.value }} </span> @if (metric.subvalue) { <span class="text-muted-foreground text-xs font-normal tabular-nums"> ({{ metric.subvalue }}) </span> } </div> <p class="text-muted-foreground text-xs leading-relaxed"> {{ metric.description }} </p> </div> </div> } </div> <!-- Detailed Rubric Matrix Table --> <div class="space-y-3"> <div class="flex items-center justify-between"> <h4 class="text-foreground text-sm font-semibold">Evaluation Rubric & Dimension Delta Matrix</h4> <span class="text-muted-foreground text-xs">Evaluated on AST parse & runtime verification</span> </div> <div class="border-border overflow-hidden rounded-lg border"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row class="border-border hover:bg-transparent"> <th ui-table-head class="text-foreground text-xs font-semibold">Evaluation Dimension</th> <th ui-table-head class="text-foreground text-xs font-semibold">Prompt A (Baseline)</th> <th ui-table-head class="text-foreground text-xs font-semibold">Prompt B (Challenger)</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Score Delta</th> <th ui-table-head class="text-foreground text-xs font-semibold">Automated Judge Finding</th> </tr> </thead> <tbody ui-table-body> @for (rubric of rubricDimensions; track rubric.id) { <tr ui-table-row class="border-border bg-success/[0.02] hover:bg-success/[0.05] bg-success/[0.03]" > <td ui-table-cell class="py-3.5"> <div class="flex items-center gap-2"> <lucide-icon [img]="ShieldCheck" class="text-success size-4" /> <span class="text-foreground text-xs font-semibold">{{ rubric.name }}</span> </div> </td> <td ui-table-cell class="text-muted-foreground py-3.5 font-mono text-xs tabular-nums"> {{ rubric.promptAScore }} </td> <td ui-table-cell class="text-success text-success py-3.5 font-mono text-xs font-bold tabular-nums" > {{ rubric.promptBScore }} </td> <td ui-table-cell class="py-3.5 text-right"> <span ui-badge variant="success" class="text-xs font-semibold tabular-nums"> {{ rubric.delta }} </span> </td> <td ui-table-cell class="text-muted-foreground py-3.5 text-xs leading-relaxed"> {{ rubric.insight }} </td> </tr> } </tbody> </table> </div> </div> </div> <!-- Sample Golden Dataset Top Runs Table --> <div class="space-y-3"> <div class="flex items-center justify-between"> <h4 class="text-foreground text-sm font-semibold">Golden Evaluation Suite (Top Sample Runs)</h4> <span ui-badge variant="outline" class="font-mono text-xs">5 of 50 Samples Shown</span> </div> <div class="border-border overflow-hidden rounded-lg border"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row class="border-border hover:bg-transparent"> <th ui-table-head class="text-foreground text-xs font-semibold">Test Case</th> <th ui-table-head class="text-foreground text-xs font-semibold">Category</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Prompt A Score</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Prompt B Score</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Score Delta</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Winner</th> </tr> </thead> <tbody ui-table-body> @for (gt of goldenTests; track gt.id) { <tr ui-table-row class="border-border hover:bg-muted/30 transition-colors"> <td ui-table-cell class="text-foreground py-3 font-mono text-xs font-medium"> {{ gt.name }} </td> <td ui-table-cell class="py-3 text-xs"> <span ui-badge variant="secondary" class="text-xs"> {{ gt.category }} </span> </td> <td ui-table-cell class="text-muted-foreground py-3 text-right font-mono text-xs tabular-nums"> {{ gt.promptAScore }} / 100 </td> <td ui-table-cell class="text-success text-success py-3 text-right font-mono text-xs font-bold tabular-nums" > {{ gt.promptBScore }} / 100 </td> <td ui-table-cell class="py-3 text-right"> <span class="text-success text-success font-mono text-xs font-semibold tabular-nums"> {{ gt.delta }} pts </span> </td> <td ui-table-cell class="py-3 text-right"> <span ui-badge variant="success" class="text-xs font-semibold"> {{ gt.winner }} </span> </td> </tr> } </tbody> </table> </div> </div> </div> </div> </div> </div> `, }) export class UiPromptAbTestingComparatorComponent { protected readonly AlertCircle = AlertCircle protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Code2 = Code2 protected readonly Coins = Coins protected readonly Copy = Copy protected readonly Cpu = Cpu protected readonly Play = Play protected readonly Rocket = Rocket protected readonly Scale = Scale protected readonly ShieldCheck = ShieldCheck protected readonly Sparkles = Sparkles protected readonly Terminal = Terminal protected readonly Trophy = Trophy protected readonly cn = cn protected readonly promptA = promptAData protected readonly promptB = promptBData protected readonly rubricDimensions = rubricDimensions protected readonly summaryMetrics = summaryMetrics protected readonly goldenTests = goldenTests @Input() experimentTitle = 'Prompt Optimization: Vue SFC AST Component Generator' @Input() testSampleCount = '50 Evaluation Prompts' @Input('class') className?: string readonly isRunning = signal(false) readonly isPromoted = signal(false) readonly copiedState = signal<Record<string, boolean>>({}) readonly selectedTestCase = signal('test-18') private copyTimer: ReturnType<typeof setTimeout> | undefined private runTimer: ReturnType<typeof setTimeout> | undefined constructor() { const destroyRef = inject(DestroyRef) destroyRef.onDestroy(() => { clearTimeout(this.copyTimer) clearTimeout(this.runTimer) window.removeEventListener('keydown', this.keyHandler) }) window.addEventListener('keydown', this.keyHandler) } private readonly keyHandler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault() this.runComparison() } } readonly currentTestCase = computed<TestCase>(() => { return testCases.find((tc) => tc.id === this.selectedTestCase()) ?? testCases[0]! }) get rootClass(): string { return cn('w-full space-y-6', this.className) } runComparison(): void { if (this.isRunning()) return this.isRunning.set(true) clearTimeout(this.runTimer) this.runTimer = setTimeout(() => this.isRunning.set(false), 1200) } promotePromptB(): void { this.isPromoted.set(true) } copyToClipboard(key: string, text: string): void { try { navigator.clipboard?.writeText(text) } catch { // Clipboard unavailable (e.g. jsdom) — still flip the copied indicator. } this.copiedState.update((prev) => ({ ...prev, [key]: true })) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => { this.copiedState.update((prev) => ({ ...prev, [key]: false })) }, 2000) } }src/app/components/blocks/prompt-ab-testing-comparator/index.ts0.1 kB
export { UiPromptAbTestingComparatorComponent } from './prompt-ab-testing-comparator.component'
Raw manifest:https://uipkge.dev/r/angular/prompt-ab-testing-comparator.json