Menu

Change language

GitHub repo

Prompt Ab Testing Comparator

blockai

Side-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

$npx uipkge-ng@latest add prompt-ab-testing-comparator
Manifest:https://uipkge.dev/r/angular/prompt-ab-testing-comparator.jsonInstalls to:src/app/components/blocks/prompt-ab-testing-comparator/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

TestCase
interface 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 &amp; Dimension Delta Matrix</h4>
                  <span class="text-muted-foreground text-xs">Evaluated on AST parse &amp; 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

Raw manifest:https://uipkge.dev/r/angular/prompt-ab-testing-comparator.json