{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "rag-pipeline-visualizer",
  "title": "Rag Pipeline Visualizer",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/rag-pipeline-visualizer/rag-pipeline-visualizer.component.ts",
      "content": "// Edit this file to change the pipeline stages, the sample chunks, the presets or the copy.\n// Everything is local sample data: wire `handleRunPipeline` to your own retrieval backend.\nimport { Component, DestroyRef, Input, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core'\nimport {\n  BookOpen,\n  Bot,\n  Check,\n  ChevronRight,\n  Code2,\n  Copy,\n  Cpu,\n  Database,\n  FileCode,\n  Filter,\n  Layers,\n  LucideAngularModule,\n  Play,\n  RefreshCw,\n  Search,\n  ShieldCheck,\n  SlidersHorizontal,\n  Sparkles,\n  Terminal,\n  ThumbsDown,\n  ThumbsUp,\n  Workflow,\n  Zap,\n  type LucideIconData,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport { UiSliderComponent } from '@/ui/slider/slider.component'\nimport { UiSwitchComponent } from '@/ui/switch/switch.component'\n\nexport interface RagPipelineStage {\n  id: number\n  name: string\n  subtitle: string\n  latency: string\n  durationMs: number\n  status: 'completed' | 'active' | 'queued' | 'bypassed'\n  icon: LucideIconData\n  tag: string\n  details: {\n    description: string\n    metrics: { label: string; value: string }[]\n    codeOrData?: { title: string; content: string }\n  }\n}\n\nexport interface RagRetrievedChunk {\n  id: string\n  citationIndex: number\n  relevanceScore: number\n  rerankBoost: number\n  cosineScore: number\n  bm25Score: number\n  documentPath: string\n  namespace: string\n  chunkOffset: string\n  tokenCount: number\n  updatedAgo: string\n  title: string\n  snippetPrefix: string\n  highlightedText: string\n  snippetSuffix: string\n  fullSnippet: string\n}\n\nconst presetQueries = [\n  'How do I configure OKLCH color palettes in Tailwind v4 with UIPKGE?',\n  'Cross-Encoder vs Bi-Encoder reranker score calibration',\n  'HyDE prompt expansion for zero-shot semantic retrieval',\n  'Pinecone HNSW index tuning: efSearch & M parameter tradeoffs',\n]\n\nconst allChunks: RagRetrievedChunk[] = [\n  {\n    id: 'chunk-1',\n    citationIndex: 1,\n    relevanceScore: 0.942,\n    rerankBoost: 0.142,\n    cosineScore: 0.8,\n    bm25Score: 0.785,\n    documentPath: 'docs/styling/tailwind-v4-oklch.md',\n    namespace: 'docs',\n    chunkOffset: 'Chunk #3 · Lines 42–78',\n    tokenCount: 384,\n    updatedAgo: '2 hours ago',\n    title: 'Tailwind v4 OKLCH Architecture & @theme Configuration',\n    snippetPrefix: 'Tailwind CSS v4 introduces native CSS-first token configuration with ',\n    highlightedText:\n      '@theme inline and OKLCH color spaces. In contrast to RGB/HSL, OKLCH ensures perceptually uniform lightness across hues',\n    snippetSuffix:\n      ', preventing contrast degradation in dark mode variants while preserving single-source token truth.',\n    fullSnippet:\n      'Tailwind CSS v4 introduces native CSS-first token configuration with `@theme inline` and OKLCH color spaces. In contrast to RGB/HSL, OKLCH ensures perceptually uniform lightness across hues, preventing contrast degradation in dark mode variants while preserving single-source token truth.',\n  },\n  {\n    id: 'chunk-2',\n    citationIndex: 2,\n    relevanceScore: 0.887,\n    rerankBoost: 0.095,\n    cosineScore: 0.792,\n    bm25Score: 0.71,\n    documentPath: 'docs/tokens/color-derivation.md',\n    namespace: 'docs',\n    chunkOffset: 'Chunk #1 · Lines 1–36',\n    tokenCount: 412,\n    updatedAgo: '1 day ago',\n    title: 'Color Tokens Derivation & Contrast Calibration',\n    snippetPrefix: 'Deriving consistent dark-mode contrasts requires ',\n    highlightedText:\n      'anchoring chroma and shifting lightness along the OKLCH L-axis. Use --color-primary: oklch(0.65 0.22 260) for vibrant interactive states',\n    snippetSuffix:\n      ' and calibrate border contrast with `--color-border: oklch(0.28 0.01 260)` for WCAG AA compliance across both frameworks.',\n    fullSnippet:\n      'Deriving consistent dark-mode contrasts requires anchoring chroma and shifting lightness along the OKLCH L-axis. Use `--color-primary: oklch(0.65 0.22 260)` for vibrant interactive states and calibrate border contrast with `--color-border: oklch(0.28 0.01 260)` for WCAG AA compliance across both frameworks.',\n  },\n  {\n    id: 'chunk-3',\n    citationIndex: 3,\n    relevanceScore: 0.824,\n    rerankBoost: 0.048,\n    cosineScore: 0.776,\n    bm25Score: 0.65,\n    documentPath: 'blog/2026/design-systems-monorepo.md',\n    namespace: 'blog',\n    chunkOffset: 'Chunk #5 · Lines 112–164',\n    tokenCount: 526,\n    updatedAgo: '3 days ago',\n    title: 'Dual-Framework Component Registry Architecture',\n    snippetPrefix: 'When architecting a dual-framework registry (Vue + React), ',\n    highlightedText:\n      'shared design tokens must compile cleanly without runtime overhead. We leverage CSS custom properties to mirror CVA component variants',\n    snippetSuffix: ' across both frameworks with zero extra runtime.',\n    fullSnippet:\n      'When architecting a dual-framework registry (Vue + React), shared design tokens must compile cleanly without runtime overhead. We leverage modern CSS custom properties and PostCSS pipelines to mirror CVA component variants across both frameworks with zero extra runtime.',\n  },\n]\n\nconst COPY_ANSWER_TEXT =\n  'To configure OKLCH color palettes in Tailwind CSS v4 with UIPKGE: 1. Define Theme Tokens in CSS (@theme inline)... 2. Calibrate Dark Mode Contrasts... 3. Maintain Dual-Framework CVA Parity...'\n\nconst CITATION_BUTTON_CLASS =\n  'bg-primary/10 py-0.2 text-primary hover:bg-primary hover:text-primary-foreground mx-1 inline-flex min-h-6 cursor-pointer items-center rounded px-1 font-mono text-xs font-bold transition-colors'\n\n/**\n * RagPipelineVisualizer -- the React block, 1:1: a RAG pipeline debugger with a header\n * (trace export + test run), a query bar with presets, Top-K slider and reranker switch,\n * a clickable 5-stage execution graph with a stage inspector, the retrieved chunks with\n * highlighted spans and scores, and the grounded answer whose citations light up their chunk.\n *\n * Icons: lucide-angular renders the <svg> inside a <lucide-icon> host. `class=\"...\"` lands on\n * both the host and the svg, so icons that carry a margin, transform or animation use\n * `[class]` (svg only) to avoid applying it twice.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-rag-pipeline-visualizer, [ui-rag-pipeline-visualizer]',\n  standalone: true,\n  // React renders the root <div> itself: keep this wrapper out of the layout.\n  host: { class: 'contents' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiCardDescriptionComponent,\n    UiCardContentComponent,\n    UiInputComponent,\n    UiProgressComponent,\n    UiSeparatorComponent,\n    UiSliderComponent,\n    UiSwitchComponent,\n  ],\n  template: `\n    <div [class]=\"rootClass\">\n      <!-- 1. Pipeline Header -->\n      <ui-card class=\"border-border bg-card shadow-xs\">\n        <ui-card-header class=\"pb-4\">\n          <div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\">\n            <div class=\"space-y-1.5\">\n              <div class=\"flex flex-wrap items-center gap-2\">\n                <div class=\"bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg\">\n                  <lucide-icon [img]=\"Workflow\" class=\"size-4.5\" />\n                </div>\n                <h2 class=\"text-foreground text-lg font-semibold tracking-tight sm:text-xl\">\n                  enterprise_knowledge_rag_v3\n                </h2>\n                <span ui-badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success\">\n                  <span class=\"bg-success mr-1.5 size-1.5 rounded-full motion-safe:animate-pulse\"></span>\n                  Pipeline Active\n                </span>\n                <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">p99 {{ totalPipelineLatency() }}</span>\n              </div>\n              <p class=\"text-muted-foreground text-sm\">\n                Production Retrieval-Augmented Generation pipeline with HyDE query rewriting, hybrid reciprocal rank\n                fusion, and cross-encoder validation.\n              </p>\n            </div>\n\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <!-- px-2.5 / px-1.5 on icon buttons: React's size gets it from has-[>svg], but here the svg sits inside <lucide-icon>. -->\n              <button\n                ui-button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"gap-1.5 px-2.5 shadow-xs\"\n                (click)=\"showTraceJson.set(!showTraceJson())\"\n              >\n                <lucide-icon [img]=\"FileCode\" class=\"size-4\" />\n                <span>{{ showTraceJson() ? 'Hide Trace' : 'Export Trace JSON' }}</span>\n              </button>\n              <button\n                ui-button\n                size=\"sm\"\n                class=\"gap-1.5 px-2.5 shadow-xs\"\n                [disabled]=\"isExecuting()\"\n                (click)=\"handleRunPipeline()\"\n              >\n                @if (isExecuting()) {\n                  <lucide-icon [img]=\"RefreshCw\" [class]=\"'size-4 motion-safe:animate-spin'\" />\n                } @else {\n                  <lucide-icon [img]=\"Play\" class=\"size-4 fill-current\" />\n                }\n                <span>{{ isExecuting() ? 'Executing Pipeline...' : 'Test Query Pipeline' }}</span>\n              </button>\n            </div>\n          </div>\n\n          <!-- Pipeline Metadata Specs Bar -->\n          <div class=\"border-border mt-4 grid grid-cols-2 gap-2 border-t pt-4 text-xs sm:grid-cols-4\">\n            <div class=\"text-muted-foreground flex items-center gap-2\">\n              <lucide-icon [img]=\"Cpu\" class=\"text-primary size-3.5 shrink-0\" />\n              <span class=\"truncate\">\n                <strong class=\"text-foreground font-medium\">Embedding:</strong> OpenAI text-embedding-3-small · 1536-dim\n              </span>\n            </div>\n            <div class=\"text-muted-foreground flex items-center gap-2\">\n              <lucide-icon [img]=\"Database\" class=\"text-primary size-3.5 shrink-0\" />\n              <span class=\"truncate\">\n                <strong class=\"text-foreground font-medium\">Vector DB:</strong> Pinecone Serverless · HNSW\n              </span>\n            </div>\n            <div class=\"text-muted-foreground flex items-center gap-2\">\n              <lucide-icon [img]=\"Layers\" class=\"text-primary size-3.5 shrink-0\" />\n              <span class=\"truncate\">\n                <strong class=\"text-foreground font-medium\">Index Space:</strong> 482,910 chunks · 4 namespaces\n              </span>\n            </div>\n            <div class=\"text-muted-foreground flex items-center gap-2\">\n              <lucide-icon [img]=\"ShieldCheck\" class=\"text-success size-3.5 shrink-0\" />\n              <span class=\"truncate\">\n                <strong class=\"text-foreground font-medium\">Grounding:</strong> 100% strict context citation\n              </span>\n            </div>\n          </div>\n        </ui-card-header>\n      </ui-card>\n\n      <!-- Optional Trace JSON Drawer -->\n      @if (showTraceJson()) {\n        <ui-card class=\"border-border bg-card/95 shadow-xs\">\n          <ui-card-header class=\"pb-2\">\n            <div class=\"flex items-center justify-between\">\n              <div class=\"flex items-center gap-2\">\n                <lucide-icon [img]=\"Terminal\" class=\"text-primary size-4\" />\n                <ui-card-title class=\"text-sm font-medium\">OpenTelemetry Pipeline Execution Trace</ui-card-title>\n              </div>\n              <button ui-button variant=\"ghost\" size=\"xs\" class=\"gap-1 px-1.5\" (click)=\"handleCopyTrace()\">\n                @if (copiedTrace()) {\n                  <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                } @else {\n                  <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n                }\n                <span>{{ copiedTrace() ? 'Copied JSON' : 'Copy Trace' }}</span>\n              </button>\n            </div>\n          </ui-card-header>\n          <ui-card-content>\n            <pre class=\"bg-muted/70 text-foreground max-h-64 overflow-x-auto rounded-md p-3 font-mono text-xs\">{{\n              traceJson()\n            }}</pre>\n          </ui-card-content>\n        </ui-card>\n      }\n\n      <!-- 2. Query Input & Runtime Controls Bar -->\n      <ui-card class=\"border-border bg-card shadow-xs\">\n        <ui-card-content class=\"space-y-4 p-4 sm:p-6\">\n          <div class=\"space-y-2\">\n            <div class=\"flex items-center justify-between\">\n              <label for=\"rag-query-input\" class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\">\n                Interactive Test Query\n              </label>\n              <span class=\"text-muted-foreground text-xs\">Press Enter or click Test Query to simulate</span>\n            </div>\n\n            <div class=\"relative flex items-center\">\n              <ui-input\n                id=\"rag-query-input\"\n                [value]=\"currentQuery()\"\n                (valueChange)=\"currentQuery.set($event)\"\n                (keydown.enter)=\"handleRunPipeline()\"\n                class=\"pr-20 text-sm font-medium shadow-xs\"\n                placeholder=\"Enter search prompt or technical query...\"\n                [prefixIcon]=\"searchIcon\"\n              />\n              <ng-template #searchIcon>\n                <lucide-icon [img]=\"Search\" class=\"text-muted-foreground size-4\" />\n              </ng-template>\n              <button\n                ui-button\n                size=\"xs\"\n                class=\"absolute right-1.5 gap-1 px-1.5\"\n                [disabled]=\"isExecuting()\"\n                (click)=\"handleRunPipeline()\"\n              >\n                <lucide-icon [img]=\"Zap\" class=\"size-3\" />\n                <span>Run</span>\n              </button>\n            </div>\n          </div>\n\n          <!-- Query Presets Chips -->\n          <div class=\"flex flex-wrap items-center gap-1.5\">\n            <span class=\"text-muted-foreground text-xs font-medium\">Presets:</span>\n            @for (preset of presetQueries; track preset) {\n              <button type=\"button\" [class]=\"presetClass(preset)\" (click)=\"handleSelectPreset(preset)\">\n                {{ preset }}\n              </button>\n            }\n          </div>\n\n          <ui-separator />\n\n          <!-- Controls Toolbar: Top-K Slider + Reranker Switch + Metrics -->\n          <div class=\"grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n            <!-- Top-K Slider -->\n            <div class=\"space-y-2\">\n              <div class=\"flex items-center justify-between\">\n                <div class=\"flex items-center gap-1.5\">\n                  <lucide-icon [img]=\"SlidersHorizontal\" class=\"text-primary size-3.5\" />\n                  <span class=\"text-foreground text-xs font-medium\">Top-K Chunks</span>\n                </div>\n                <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">Top {{ topK() }} Chunks</span>\n              </div>\n              <ui-slider\n                [value]=\"[topK()]\"\n                [min]=\"1\"\n                [max]=\"3\"\n                [step]=\"1\"\n                [tooltip]=\"false\"\n                class=\"w-full\"\n                (valueChange)=\"onTopKChange($event)\"\n              />\n              <div class=\"text-muted-foreground flex justify-between font-mono text-xs\">\n                <span>k=1</span>\n                <span>k=2</span>\n                <span>k=3</span>\n              </div>\n            </div>\n\n            <!-- Reranker Toggle -->\n            <div class=\"border-border/80 bg-muted/20 flex flex-col justify-between space-y-2 rounded-lg border p-3\">\n              <div class=\"flex items-center justify-between\">\n                <div class=\"space-y-0.5\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <lucide-icon [img]=\"Filter\" class=\"text-primary size-3.5\" />\n                    <span class=\"text-foreground text-xs font-semibold\">Cohere Rerank v3</span>\n                  </div>\n                  <p class=\"text-muted-foreground text-xs\">Cross-encoder contextual re-scoring</p>\n                </div>\n                <button ui-switch [checked]=\"rerankerEnabled()\" (checkedChange)=\"rerankerEnabled.set($event)\"></button>\n              </div>\n              <div class=\"flex items-center gap-2\">\n                <span ui-badge [variant]=\"rerankerEnabled() ? 'outline' : 'secondary'\" [class]=\"rerankerBadgeClass()\">\n                  {{ rerankerEnabled() ? 'Active (+0.142 boost)' : 'Bypassed (Cosine only)' }}\n                </span>\n              </div>\n            </div>\n\n            <!-- Pipeline Telemetry Overview -->\n            <div\n              class=\"border-border/80 bg-muted/20 flex flex-col justify-between space-y-1.5 rounded-lg border p-3 sm:col-span-2 lg:col-span-1\"\n            >\n              <div class=\"flex items-center justify-between\">\n                <span class=\"text-foreground text-xs font-semibold\">Execution Latency</span>\n                <span class=\"text-primary font-mono text-xs font-medium\">{{ totalPipelineLatency() }} total</span>\n              </div>\n              <div class=\"space-y-1\">\n                <div class=\"text-muted-foreground flex justify-between text-xs\">\n                  <span>Dense + BM25</span>\n                  <span class=\"font-mono\">106ms</span>\n                </div>\n                <ui-progress [value]=\"15\" class=\"h-1.5\" />\n              </div>\n              <div class=\"space-y-1\">\n                <div class=\"text-muted-foreground flex justify-between text-xs\">\n                  <span>LLM Synthesis</span>\n                  <span class=\"font-mono\">820ms (85%)</span>\n                </div>\n                <ui-progress [value]=\"85\" class=\"h-1.5\" />\n              </div>\n            </div>\n          </div>\n        </ui-card-content>\n      </ui-card>\n\n      <!-- 3. Interactive RAG 5-Stage Architecture Flow -->\n      <div class=\"space-y-3\">\n        <div class=\"flex items-center justify-between\">\n          <div>\n            <h3 class=\"text-foreground text-xs font-semibold tracking-wider uppercase\">RAG Pipeline Execution Graph</h3>\n            <p class=\"text-muted-foreground text-xs\">\n              Click any stage node to inspect low-level telemetry, prompt variables, and intermediate embeddings.\n            </p>\n          </div>\n          <span ui-badge variant=\"outline\" class=\"font-mono text-xs\">5 Connected Stages</span>\n        </div>\n\n        <!-- 5-Stage Connected Cards -->\n        <div class=\"grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-5\">\n          @for (stage of stages(); track stage.id) {\n            <div\n              [class]=\"stageCardClass(stage)\"\n              tabindex=\"0\"\n              role=\"button\"\n              [attr.aria-pressed]=\"selectedStageId() === stage.id\"\n              (click)=\"selectedStageId.set(stage.id)\"\n              (keydown)=\"onStageKeydown($event, stage)\"\n            >\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between\">\n                  <div [class]=\"stageIconClass(stage)\">\n                    <lucide-icon [img]=\"stage.icon\" class=\"size-3.5\" />\n                  </div>\n                  <span ui-badge variant=\"secondary\" [class]=\"stageLatencyClass(stage)\">{{ stage.latency }}</span>\n                </div>\n\n                <div>\n                  <p class=\"text-foreground text-xs leading-tight font-semibold\">{{ stage.name }}</p>\n                  <p class=\"text-muted-foreground mt-1 line-clamp-2 text-xs leading-relaxed\">{{ stage.subtitle }}</p>\n                </div>\n              </div>\n\n              <div\n                class=\"border-border/60 text-muted-foreground mt-3 flex items-center justify-between border-t pt-2 text-xs\"\n              >\n                <span class=\"truncate font-medium\">{{ stage.tag }}</span>\n                <lucide-icon [img]=\"ChevronRight\" [class]=\"stageChevronClass(stage)\" />\n              </div>\n            </div>\n          }\n        </div>\n\n        <!-- Stage Detail Drill-down Panel -->\n        <ui-card class=\"border-border bg-card/60 shadow-xs\">\n          <ui-card-header class=\"pb-3\">\n            <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n              <div class=\"flex items-center gap-2\">\n                <div class=\"bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md\">\n                  <lucide-icon [img]=\"currentStage().icon\" class=\"size-4\" />\n                </div>\n                <div>\n                  <ui-card-title class=\"text-foreground text-sm font-semibold\">\n                    {{ currentStage().name }} — Inspector\n                  </ui-card-title>\n                  <ui-card-description class=\"text-xs\">{{ currentStage().details.description }}</ui-card-description>\n                </div>\n              </div>\n              <span ui-badge variant=\"outline\" class=\"w-fit font-mono text-xs\">\n                Stage {{ currentStage().id }} of 5 · {{ currentStage().latency }}\n              </span>\n            </div>\n          </ui-card-header>\n          <ui-card-content class=\"space-y-4 pt-0\">\n            <!-- Metrics Grid for Stage -->\n            <div class=\"grid grid-cols-2 gap-2 sm:grid-cols-4\">\n              @for (metric of currentStage().details.metrics; track metric.label) {\n                <div class=\"border-border/80 bg-muted/40 rounded-md border p-2.5\">\n                  <p class=\"text-muted-foreground text-xs\">{{ metric.label }}</p>\n                  <p class=\"text-foreground mt-0.5 truncate font-mono text-xs font-semibold\">{{ metric.value }}</p>\n                </div>\n              }\n            </div>\n\n            <!-- Code or Data Artifact Preview -->\n            @if (currentStage().details.codeOrData; as codeOrData) {\n              <div class=\"space-y-1.5\">\n                <div class=\"text-muted-foreground flex items-center justify-between text-xs font-medium\">\n                  <span class=\"flex items-center gap-1.5\">\n                    <lucide-icon [img]=\"Code2\" class=\"text-primary size-3.5\" />\n                    {{ codeOrData.title }}\n                  </span>\n                  <span class=\"font-mono text-xs\">read-only buffer</span>\n                </div>\n                <pre\n                  class=\"bg-muted/80 text-foreground max-h-40 overflow-x-auto rounded-md p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap\"\n                  >{{ codeOrData.content }}</pre>\n              </div>\n            }\n          </ui-card-content>\n        </ui-card>\n      </div>\n\n      <!-- 4. Retrieved Context Chunks Inspector -->\n      <div class=\"space-y-3\">\n        <div class=\"flex items-center justify-between\">\n          <div>\n            <h3 class=\"text-foreground text-xs font-semibold tracking-wider uppercase\">\n              Retrieved Context Chunks ({{ displayedChunks().length }} Active Chunks)\n            </h3>\n            <p class=\"text-muted-foreground text-xs\">\n              Ranked by cross-encoder relevance score. Keyword spans matching user intent are highlighted.\n            </p>\n          </div>\n          <div class=\"flex items-center gap-2\">\n            <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">Fusion: Dense (70%) + Sparse (30%)</span>\n          </div>\n        </div>\n\n        <div class=\"space-y-3\">\n          @for (chunk of displayedChunks(); track chunk.id) {\n            <ui-card [class]=\"chunkCardClass(chunk)\">\n              <ui-card-header class=\"p-4 pb-2\">\n                <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n                  <div class=\"flex flex-wrap items-center gap-2\">\n                    <!-- Citation Badge Pill -->\n                    <span [class]=\"citationPillClass(chunk)\">[{{ chunk.citationIndex }}]</span>\n\n                    <!-- Relevancy Score Badge -->\n                    <span ui-badge variant=\"outline\" [class]=\"scoreBadgeClass(chunk)\">\n                      Score: {{ chunkScore(chunk) }}\n                    </span>\n\n                    <!-- Rerank Delta Badge -->\n                    @if (rerankerEnabled()) {\n                      <span ui-badge variant=\"secondary\" class=\"text-muted-foreground font-mono text-xs\">\n                        +{{ chunk.rerankBoost.toFixed(3) }} boost (Rank #{{ chunk.citationIndex }})\n                      </span>\n                    }\n\n                    <!-- Document Path -->\n                    <span class=\"text-foreground font-mono text-xs font-medium\">{{ chunk.documentPath }}</span>\n                  </div>\n\n                  <!-- Metadata & Copy Action -->\n                  <div class=\"flex items-center gap-2\">\n                    <span class=\"text-muted-foreground hidden font-mono text-xs sm:inline\">\n                      {{ chunk.chunkOffset }} · {{ chunk.tokenCount }} tok\n                    </span>\n                    <button\n                      ui-button\n                      variant=\"ghost\"\n                      size=\"xs\"\n                      class=\"text-muted-foreground hover:text-foreground gap-1 px-1.5\"\n                      (click)=\"handleCopyChunk(chunk.id, chunk.fullSnippet)\"\n                    >\n                      @if (copiedChunkId() === chunk.id) {\n                        <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                      } @else {\n                        <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                      }\n                      <span class=\"text-xs\">{{ copiedChunkId() === chunk.id ? 'Copied' : 'Copy' }}</span>\n                    </button>\n                  </div>\n                </div>\n              </ui-card-header>\n\n              <ui-card-content class=\"p-4 pt-2\">\n                <!-- Chunk Text with Highlighted Spans -->\n                <div\n                  class=\"border-border/70 bg-muted/40 text-foreground rounded-md border p-3 font-mono text-xs leading-relaxed\"\n                >\n                  <span>{{ chunk.snippetPrefix }}</span>\n                  <mark class=\"bg-primary/20 text-primary dark:bg-primary/30 rounded px-1 py-0.5 font-semibold\">{{\n                    chunk.highlightedText\n                  }}</mark>\n                  <span>{{ chunk.snippetSuffix }}</span>\n                </div>\n\n                <!-- Scoring Breakdown Sub-bar -->\n                <div class=\"text-muted-foreground mt-2.5 flex flex-wrap items-center justify-between gap-2 text-xs\">\n                  <div class=\"flex flex-wrap items-center gap-3 font-mono\">\n                    <span>\n                      Dense Cosine:\n                      <strong class=\"text-foreground font-semibold\">{{ chunk.cosineScore.toFixed(3) }}</strong>\n                    </span>\n                    <span>\n                      Sparse BM25:\n                      <strong class=\"text-foreground font-semibold\">{{ chunk.bm25Score.toFixed(3) }}</strong>\n                    </span>\n                    @if (rerankerEnabled()) {\n                      <span>\n                        Cross-Encoder:\n                        <strong class=\"text-success font-semibold\">{{ chunk.relevanceScore.toFixed(3) }}</strong>\n                      </span>\n                    }\n                  </div>\n                  <span class=\"text-muted-foreground text-xs\">Updated {{ chunk.updatedAgo }}</span>\n                </div>\n              </ui-card-content>\n            </ui-card>\n          }\n        </div>\n      </div>\n\n      <!-- 5. Final Synthesized Output Card with Citations -->\n      <ui-card class=\"border-border bg-card shadow-xs\">\n        <ui-card-header class=\"pb-3\">\n          <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n            <div class=\"flex items-center gap-2\">\n              <div class=\"bg-primary text-primary-foreground flex size-7 items-center justify-center rounded-md\">\n                <lucide-icon [img]=\"Sparkles\" class=\"size-4\" />\n              </div>\n              <div>\n                <ui-card-title class=\"text-foreground text-base font-semibold\"\n                  >Synthesized Grounded Response</ui-card-title\n                >\n                <ui-card-description class=\"text-xs\">\n                  Generated via Claude 3.5 Sonnet streaming with verified inline citations.\n                </ui-card-description>\n              </div>\n            </div>\n\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <span ui-badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success font-mono text-xs\">\n                <lucide-icon [img]=\"ShieldCheck\" [class]=\"'mr-1 size-3'\" />\n                100% Grounded · 0 Hallucinations\n              </span>\n              <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">820ms (TTFT 180ms)</span>\n            </div>\n          </div>\n        </ui-card-header>\n\n        <ui-card-content class=\"space-y-4\">\n          <!-- Markdown Formatted Synthesized Body -->\n          <div\n            class=\"border-border/80 bg-muted/20 text-foreground space-y-3 rounded-lg border p-4 text-sm leading-relaxed sm:p-5\"\n          >\n            <p class=\"text-foreground font-medium\">To configure OKLCH color palettes in Tailwind CSS v4 with UIPKGE:</p>\n\n            <!-- Text runs touch the citation buttons with no whitespace, as in the React JSX, and each\n              button's label sits tight (>[N]</button) so its textContent is exactly '[N]'. -->\n            <ol class=\"text-foreground/90 list-decimal space-y-2.5 pl-5 text-sm\">\n              <li class=\"leading-relaxed\">\n                <strong>Define Theme Tokens in CSS:</strong> Use\n                <code class=\"bg-muted text-foreground rounded px-1.5 py-0.5 font-mono text-xs\">&#64;theme inline</code>\n                in your root stylesheet to declare single-source-of-truth OKLCH variables<button\n                  type=\"button\"\n                  [class]=\"citationButtonClass\"\n                  (mouseenter)=\"activeCitationHover.set(1)\"\n                  (mouseleave)=\"activeCitationHover.set(null)\"\n                  >[1]</button\n                >. Unlike legacy RGB/HSL, OKLCH ensures perceptually uniform lightness across hue shifts<button\n                  type=\"button\"\n                  [class]=\"citationButtonClass\"\n                  (mouseenter)=\"activeCitationHover.set(1)\"\n                  (mouseleave)=\"activeCitationHover.set(null)\"\n                  >[1]</button\n                >.\n              </li>\n\n              <li class=\"leading-relaxed\">\n                <strong>Calibrate Dark Mode Contrasts:</strong> Anchor chroma and shift lightness along the OKLCH L-axis\n                (e.g.,\n                <code class=\"bg-muted rounded px-1.5 py-0.5 font-mono text-xs\"\n                  >--color-primary: oklch(0.65 0.22 260)</code\n                >\n                and\n                <code class=\"bg-muted rounded px-1.5 py-0.5 font-mono text-xs\"\n                  >--color-border: oklch(0.28 0.01 260)</code\n                >) to maintain WCAG AA compliance across both light and dark modes<button\n                  type=\"button\"\n                  [class]=\"citationButtonClass\"\n                  (mouseenter)=\"activeCitationHover.set(2)\"\n                  (mouseleave)=\"activeCitationHover.set(null)\"\n                  >[2]</button\n                >.\n              </li>\n\n              <li class=\"leading-relaxed\">\n                <strong>Maintain Dual-Framework CVA Parity:</strong> Wire CSS tokens directly through Class Variance\n                Authority (<code class=\"bg-muted rounded px-1.5 py-0.5 font-mono text-xs\">cva()</code>) variants so that\n                both Vue and React registry components share identical token namespaces with zero runtime bundle\n                overhead<button\n                  type=\"button\"\n                  [class]=\"citationButtonClass\"\n                  (mouseenter)=\"activeCitationHover.set(3)\"\n                  (mouseleave)=\"activeCitationHover.set(null)\"\n                  >[3]</button\n                >.\n              </li>\n            </ol>\n\n            <!-- Streaming cursor pulse -->\n            <div class=\"text-muted-foreground flex items-center gap-1.5 pt-1 font-mono text-xs\">\n              <span class=\"bg-success size-2 rounded-full motion-safe:animate-ping\"></span>\n              <span>Generation Complete · 214 tokens streamed</span>\n            </div>\n          </div>\n\n          <!-- Citation Footnotes Bar -->\n          <div class=\"border-border/70 bg-muted/40 space-y-1.5 rounded-md border p-3 text-xs\">\n            <span class=\"text-foreground flex items-center gap-1.5 font-semibold\">\n              <lucide-icon [img]=\"BookOpen\" class=\"text-primary size-3.5\" />\n              Referenced Citations (Hover citation to locate chunk):\n            </span>\n            <div class=\"grid grid-cols-1 gap-1.5 sm:grid-cols-3\">\n              @for (chunk of displayedChunks(); track chunk.id) {\n                <div\n                  [class]=\"footnoteClass(chunk)\"\n                  (mouseenter)=\"activeCitationHover.set(chunk.citationIndex)\"\n                  (mouseleave)=\"activeCitationHover.set(null)\"\n                >\n                  <span class=\"truncate\">[{{ chunk.citationIndex }}] {{ fileName(chunk) }}</span>\n                  <span class=\"font-semibold\">{{ chunk.relevanceScore.toFixed(3) }}</span>\n                </div>\n              }\n            </div>\n          </div>\n\n          <!-- Telemetry Footer & Action Buttons -->\n          <div\n            class=\"border-border text-muted-foreground flex flex-col gap-3 border-t pt-2 text-xs sm:flex-row sm:items-center sm:justify-between\"\n          >\n            <div class=\"flex flex-wrap items-center gap-3 font-mono\">\n              <span>Input: <strong class=\"text-foreground\">1,248 tok</strong></span>\n              <span>Output: <strong class=\"text-foreground\">214 tok</strong></span>\n              <span>Cost: <strong class=\"text-foreground\">$0.00438</strong></span>\n              <span>Grounding: <strong class=\"text-success\">99.4%</strong></span>\n            </div>\n\n            <div class=\"flex items-center gap-2\">\n              <div class=\"border-border bg-card flex items-center rounded-md border p-0.5\">\n                <button\n                  ui-button\n                  aria-label=\"Thumbs up\"\n                  variant=\"ghost\"\n                  size=\"xs\"\n                  [class]=\"feedbackClass('up')\"\n                  (click)=\"toggleFeedback('up')\"\n                >\n                  <lucide-icon [img]=\"ThumbsUp\" class=\"size-3.5\" />\n                </button>\n                <ui-separator orientation=\"vertical\" class=\"h-4\" />\n                <button\n                  ui-button\n                  aria-label=\"Thumbs down\"\n                  variant=\"ghost\"\n                  size=\"xs\"\n                  [class]=\"feedbackClass('down')\"\n                  (click)=\"toggleFeedback('down')\"\n                >\n                  <lucide-icon [img]=\"ThumbsDown\" class=\"size-3.5\" />\n                </button>\n              </div>\n\n              <button ui-button variant=\"outline\" size=\"xs\" class=\"gap-1 px-1.5\" (click)=\"handleRunPipeline()\">\n                <lucide-icon [img]=\"RefreshCw\" class=\"size-3\" />\n                <span>Regenerate</span>\n              </button>\n\n              <button ui-button size=\"xs\" class=\"gap-1 px-1.5\" (click)=\"handleCopyResponse(copyAnswerText)\">\n                @if (copiedResponse()) {\n                  <lucide-icon [img]=\"Check\" class=\"size-3\" />\n                } @else {\n                  <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                }\n                <span>{{ copiedResponse() ? 'Copied' : 'Copy Answer' }}</span>\n              </button>\n            </div>\n          </div>\n        </ui-card-content>\n      </ui-card>\n    </div>\n  `,\n})\nexport class UiRagPipelineVisualizerComponent {\n  @Input('class') className?: string\n\n  protected readonly Workflow = Workflow\n  protected readonly FileCode = FileCode\n  protected readonly RefreshCw = RefreshCw\n  protected readonly Play = Play\n  protected readonly Cpu = Cpu\n  protected readonly Database = Database\n  protected readonly Layers = Layers\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Terminal = Terminal\n  protected readonly Check = Check\n  protected readonly Copy = Copy\n  protected readonly Search = Search\n  protected readonly Zap = Zap\n  protected readonly SlidersHorizontal = SlidersHorizontal\n  protected readonly Filter = Filter\n  protected readonly ChevronRight = ChevronRight\n  protected readonly Code2 = Code2\n  protected readonly Sparkles = Sparkles\n  protected readonly BookOpen = BookOpen\n  protected readonly ThumbsUp = ThumbsUp\n  protected readonly ThumbsDown = ThumbsDown\n\n  protected readonly presetQueries = presetQueries\n  protected readonly copyAnswerText = COPY_ANSWER_TEXT\n  protected readonly citationButtonClass = CITATION_BUTTON_CLASS\n\n  readonly currentQuery = signal('How do I configure OKLCH color palettes in Tailwind v4 with UIPKGE?')\n  readonly topK = signal(3)\n  readonly rerankerEnabled = signal(true)\n  readonly selectedStageId = signal(3)\n  readonly isExecuting = signal(false)\n  readonly copiedChunkId = signal<string | null>(null)\n  readonly copiedResponse = signal(false)\n  readonly copiedTrace = signal(false)\n  readonly activeCitationHover = signal<number | null>(null)\n  readonly showTraceJson = signal(false)\n  readonly userFeedback = signal<'up' | 'down' | null>(null)\n\n  private readonly timers = new Set<ReturnType<typeof setTimeout>>()\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => this.timers.forEach(clearTimeout))\n  }\n\n  readonly stages = computed<RagPipelineStage[]>(() => {\n    const rerankerEnabled = this.rerankerEnabled()\n    const topK = this.topK()\n    return [\n      {\n        id: 1,\n        name: '1. User Query & HyDE Expansion',\n        subtitle: 'Query rewritten with hypothetical document embedding · 42ms',\n        latency: '42ms',\n        durationMs: 42,\n        status: 'completed',\n        icon: Sparkles,\n        tag: 'Prompt Rewriting',\n        details: {\n          description:\n            'Generates a synthetic hypothetical answer using Claude 3.5 Haiku to bridge vocabulary mismatch between conversational queries and technical documentation.',\n          metrics: [\n            { label: 'Expansion Model', value: 'Claude 3.5 Haiku' },\n            { label: 'Latency', value: '42ms' },\n            { label: 'Prompt Tokens', value: '48 tokens' },\n            { label: 'Hypothesis Length', value: '112 tokens' },\n          ],\n          codeOrData: {\n            title: 'Generated Hypothetical Document (HyDE)',\n            content:\n              'Tailwind CSS v4 defines color tokens using CSS variables inside `@theme inline` with `oklch(L C H)` functions. In dual-framework component registries like UIPKGE, OKLCH ensures uniform perceptual lightness across hue shifts in light and dark variants without breaking WCAG AA contrast.',\n          },\n        },\n      },\n      {\n        id: 2,\n        name: '2. Hybrid Dense + Sparse Search',\n        subtitle: 'Dense Cosine + BM25 Lexical search across 480k chunks · 64ms',\n        latency: '64ms',\n        durationMs: 64,\n        status: 'completed',\n        icon: Database,\n        tag: 'Reciprocal Rank Fusion',\n        details: {\n          description:\n            'Performs vector similarity search on 1536-dim embeddings combined with sparse BM25 keyword matching across 482,910 document chunks.',\n          metrics: [\n            { label: 'Vector Index', value: 'Pinecone Serverless (HNSW)' },\n            { label: 'Dimension', value: '1536-dim (text-embedding-3-small)' },\n            { label: 'Fusion Weight', value: 'α = 0.70 Dense + 0.30 Sparse' },\n            { label: 'Candidate Pool', value: '20 Candidate Chunks' },\n          ],\n          codeOrData: {\n            title: 'Dense Vector Projection (32-float slice)',\n            content:\n              '[-0.048, 0.135, 0.092, -0.018, 0.245, -0.110, 0.049, 0.174, -0.082, 0.001, 0.325, -0.059, 0.097, 0.191, -0.149, 0.068, -0.024, 0.212, 0.082, -0.094, 0.121, -0.037, 0.156, 0.031, -0.172, 0.099, 0.056, -0.078, 0.195, -0.013, 0.072, 0.141]',\n          },\n        },\n      },\n      {\n        id: 3,\n        name: '3. Cross-Encoder Reranker',\n        subtitle: rerankerEnabled\n          ? 'Cohere Rerank v3: re-scores Top 20 -> Top 3 · 28ms'\n          : 'Bypassed (Using raw hybrid search ranking) · 0ms',\n        latency: rerankerEnabled ? '28ms' : '0ms',\n        durationMs: rerankerEnabled ? 28 : 0,\n        status: rerankerEnabled ? 'completed' : 'bypassed',\n        icon: Filter,\n        tag: rerankerEnabled ? 'Cross-Attention Re-Scoring' : 'Bypassed',\n        details: {\n          description: rerankerEnabled\n            ? 'Applies deep transformer cross-attention to score query-document pairs simultaneously, eliminating false-positive semantic matches.'\n            : 'Reranker is currently bypassed. Chunks are ordered purely by initial hybrid reciprocal rank scores.',\n          metrics: [\n            { label: 'Model', value: 'cohere-rerank-v3.0-multilingual' },\n            { label: 'Top-K Retained', value: `Top ${topK} of 20` },\n            { label: 'Relevance Cutoff', value: '≥ 0.800 score' },\n            { label: 'Max Rank Shift', value: '+3 positions (Chunk #3)' },\n          ],\n          codeOrData: {\n            title: 'Reranker Score Calibration Delta',\n            content:\n              'Chunk #1: 0.800 (Cosine) -> 0.942 (Rerank)  [+0.142 boost]\\nChunk #2: 0.792 (Cosine) -> 0.887 (Rerank)  [+0.095 boost]\\nChunk #3: 0.776 (Cosine) -> 0.824 (Rerank)  [+0.048 boost]\\n17 candidates pruned below 0.800 relevance threshold',\n          },\n        },\n      },\n      {\n        id: 4,\n        name: '4. Context Window Assembly',\n        subtitle: '1,248 tokens packed, 98% prompt density · 8ms',\n        latency: '8ms',\n        durationMs: 8,\n        status: 'completed',\n        icon: Layers,\n        tag: 'Token Packaging',\n        details: {\n          description:\n            'Deduplicates overlapping chunk boundaries, injects citation boundary anchors `[1]`, `[2]`, `[3]`, and formats system directives for zero-hallucination grounding.',\n          metrics: [\n            { label: 'Window Budget', value: '1,248 / 8,192 tokens (15.2%)' },\n            { label: 'Prompt Density', value: '98.4% relevance tokens' },\n            { label: 'Chunks Packaged', value: `${Math.min(topK, 3)} chunks` },\n            { label: 'Delimiter Format', value: '<context_document_id>' },\n          ],\n          codeOrData: {\n            title: 'Assembled Prompt Context Envelope',\n            content:\n              '<system>\\nYou are a Lead Design Systems Architect. Synthesize answers strictly from provided context chunks.\\nAlways ground every technical claim with an inline citation key [1], [2], or [3].\\n</system>\\n\\n<context>\\n[1] doc: docs/styling/tailwind-v4-oklch.md (score: 0.942)\\n[2] doc: docs/tokens/color-derivation.md (score: 0.887)\\n[3] doc: blog/2026/design-systems-monorepo.md (score: 0.824)\\n</context>',\n          },\n        },\n      },\n      {\n        id: 5,\n        name: '5. LLM Synthesis & Grounding',\n        subtitle: 'Claude 3.5 Sonnet streaming generation · 820ms',\n        latency: '820ms',\n        durationMs: 820,\n        status: 'completed',\n        icon: Bot,\n        tag: 'Streaming Synthesis',\n        details: {\n          description:\n            'Generates final structured markdown answer with streaming tokens, verifies every claim against citation sources, and produces grounded output.',\n          metrics: [\n            { label: 'Model', value: 'claude-3-5-sonnet-20241022' },\n            { label: 'Time to First Token', value: '180ms' },\n            { label: 'Generation Speed', value: '68.2 tokens/sec' },\n            { label: 'Grounding Verification', value: '100% (3/3 facts grounded)' },\n          ],\n          codeOrData: {\n            title: 'Telemetry & Cost Summary',\n            content:\n              'Prompt Tokens: 1,248 ($0.00374)\\nCompletion Tokens: 214 ($0.00064)\\nTotal Cost: $0.00438\\nHallucination Detection: 0 violations detected',\n          },\n        },\n      },\n    ]\n  })\n\n  readonly displayedChunks = computed(() => {\n    const count = Math.min(Math.max(1, this.topK()), allChunks.length)\n    return allChunks.slice(0, count)\n  })\n\n  readonly currentStage = computed(() => {\n    const stages = this.stages()\n    return stages.find((s) => s.id === this.selectedStageId()) ?? stages[0]!\n  })\n\n  readonly totalPipelineLatency = computed(() => `${this.stages().reduce((acc, s) => acc + s.durationMs, 0)}ms`)\n\n  readonly traceJson = computed(() => {\n    const rerankerEnabled = this.rerankerEnabled()\n    return JSON.stringify(\n      {\n        traceId: 'trc_rag_948201a4',\n        pipeline: 'enterprise_knowledge_rag_v3',\n        timestamp: new Date().toISOString(),\n        query: this.currentQuery(),\n        config: {\n          embeddingModel: 'text-embedding-3-small',\n          dimensions: 1536,\n          vectorDb: 'Pinecone Serverless',\n          topK: this.topK(),\n          reranker: rerankerEnabled ? 'cohere-rerank-v3' : 'none',\n          alphaFusion: 0.7,\n        },\n        stages: this.stages().map((s) => ({\n          id: s.id,\n          name: s.name,\n          durationMs: s.durationMs,\n          status: s.status,\n        })),\n        retrievedChunks: this.displayedChunks().map((c) => ({\n          citation: c.citationIndex,\n          docPath: c.documentPath,\n          relevance: rerankerEnabled ? c.relevanceScore : c.cosineScore,\n          rerankBoost: rerankerEnabled ? c.rerankBoost : 0,\n          tokenCount: c.tokenCount,\n        })),\n        synthesis: {\n          model: 'claude-3-5-sonnet-20241022',\n          promptTokens: 1248,\n          completionTokens: 214,\n          groundedFactRatio: 1.0,\n          latencyMs: 820,\n        },\n      },\n      null,\n      2,\n    )\n  })\n\n  get rootClass(): string {\n    return cn('w-full space-y-6 font-sans', this.className)\n  }\n\n  private later(fn: () => void, ms: number): void {\n    const id = setTimeout(() => {\n      this.timers.delete(id)\n      fn()\n    }, ms)\n    this.timers.add(id)\n  }\n\n  handleRunPipeline(): void {\n    if (this.isExecuting()) return\n    this.isExecuting.set(true)\n    this.selectedStageId.set(1)\n    this.later(() => this.selectedStageId.set(2), 200)\n    this.later(() => this.selectedStageId.set(3), 450)\n    this.later(() => this.selectedStageId.set(4), 700)\n    this.later(() => {\n      this.selectedStageId.set(5)\n      this.isExecuting.set(false)\n    }, 950)\n  }\n\n  handleSelectPreset(preset: string): void {\n    this.currentQuery.set(preset)\n    this.handleRunPipeline()\n  }\n\n  handleCopyChunk(id: string, text: string): void {\n    void navigator.clipboard?.writeText(text)\n    this.copiedChunkId.set(id)\n    this.later(() => this.copiedChunkId.set(null), 2000)\n  }\n\n  handleCopyResponse(text: string): void {\n    void navigator.clipboard?.writeText(text)\n    this.copiedResponse.set(true)\n    this.later(() => this.copiedResponse.set(false), 2000)\n  }\n\n  handleCopyTrace(): void {\n    void navigator.clipboard?.writeText(this.traceJson())\n    this.copiedTrace.set(true)\n    this.later(() => this.copiedTrace.set(false), 2000)\n  }\n\n  onTopKChange(value: number[]): void {\n    this.topK.set(value[0] ?? this.topK())\n  }\n\n  onStageKeydown(event: KeyboardEvent, stage: RagPipelineStage): void {\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      this.selectedStageId.set(stage.id)\n    }\n  }\n\n  toggleFeedback(value: 'up' | 'down'): void {\n    this.userFeedback.set(this.userFeedback() === value ? null : value)\n  }\n\n  chunkScore(chunk: RagRetrievedChunk): string {\n    return this.rerankerEnabled() ? chunk.relevanceScore.toFixed(3) : chunk.cosineScore.toFixed(3)\n  }\n\n  fileName(chunk: RagRetrievedChunk): string {\n    return chunk.documentPath.split('/').pop() ?? ''\n  }\n\n  presetClass(preset: string): string {\n    return cn(\n      'focus-visible:ring-ring min-h-6 cursor-pointer rounded-full border px-2.5 py-0.5 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-hidden',\n      this.currentQuery() === preset\n        ? 'border-primary bg-primary/10 text-primary font-medium'\n        : 'border-border bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  rerankerBadgeClass(): string {\n    return cn(\n      'font-mono text-xs',\n      this.rerankerEnabled() ? 'border-success/30 bg-success/10 text-success' : 'text-muted-foreground',\n    )\n  }\n\n  stageCardClass(stage: RagPipelineStage): string {\n    return cn(\n      'group focus-visible:ring-ring relative flex cursor-pointer flex-col justify-between rounded-lg border p-3.5 transition-colors focus-visible:ring-2 focus-visible:outline-hidden',\n      this.selectedStageId() === stage.id\n        ? 'border-primary bg-primary/5 ring-primary/40 shadow-xs ring-1'\n        : 'border-border bg-card hover:border-primary/40 hover:bg-muted/30',\n    )\n  }\n\n  stageIconClass(stage: RagPipelineStage): string {\n    return cn(\n      'flex size-7 items-center justify-center rounded-md text-xs font-semibold',\n      this.selectedStageId() === stage.id\n        ? 'bg-primary text-primary-foreground'\n        : 'bg-muted text-foreground group-hover:bg-primary/20 group-hover:text-primary',\n    )\n  }\n\n  stageLatencyClass(stage: RagPipelineStage): string {\n    return cn('font-mono text-xs', stage.status === 'bypassed' && 'text-muted-foreground line-through opacity-70')\n  }\n\n  stageChevronClass(stage: RagPipelineStage): string {\n    return cn(\n      'size-3.5 transition-transform',\n      this.selectedStageId() === stage.id ? 'text-primary translate-x-0.5' : 'text-muted-foreground',\n    )\n  }\n\n  chunkCardClass(chunk: RagRetrievedChunk): string {\n    return cn(\n      'border-border bg-card shadow-xs transition-colors',\n      this.activeCitationHover() === chunk.citationIndex && 'border-primary/80 ring-primary/20 bg-primary/5 ring-2',\n    )\n  }\n\n  citationPillClass(chunk: RagRetrievedChunk): string {\n    return cn(\n      'flex size-6 items-center justify-center rounded-md font-mono text-xs font-bold transition-colors',\n      this.activeCitationHover() === chunk.citationIndex\n        ? 'bg-primary text-primary-foreground'\n        : 'bg-muted text-foreground',\n    )\n  }\n\n  scoreBadgeClass(chunk: RagRetrievedChunk): string {\n    return cn(\n      'font-mono text-xs font-semibold',\n      chunk.relevanceScore >= 0.9\n        ? 'border-success/30 bg-success/10 text-success'\n        : chunk.relevanceScore >= 0.85\n          ? 'border-success/30 bg-success/10 text-success'\n          : 'border-info/30 bg-info/10 text-info',\n    )\n  }\n\n  footnoteClass(chunk: RagRetrievedChunk): string {\n    return cn(\n      'flex cursor-pointer items-center justify-between rounded border p-1.5 font-mono text-xs transition-colors',\n      this.activeCitationHover() === chunk.citationIndex\n        ? 'border-primary bg-primary/10 text-primary'\n        : 'border-border/60 bg-card text-muted-foreground hover:bg-muted',\n    )\n  }\n\n  /** px-1.5 (not React's `px-2`): React's has-[>svg]:px-1.5 outranks it; the svg here sits in <lucide-icon>. */\n  feedbackClass(value: 'up' | 'down'): string {\n    return cn('px-1.5', this.userFeedback() === value && (value === 'up' ? 'text-success' : 'text-destructive'))\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/rag-pipeline-visualizer/rag-pipeline-visualizer.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/rag-pipeline-visualizer/index.ts",
      "content": "export {\n  UiRagPipelineVisualizerComponent,\n  type RagPipelineStage,\n  type RagRetrievedChunk,\n} from './rag-pipeline-visualizer.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/rag-pipeline-visualizer/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/progress.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/slider.json",
    "https://uipkge.dev/r/angular/switch.json"
  ],
  "description": "LlamaIndex and LangChain style Retrieval-Augmented Generation (RAG) pipeline debugger and chunk retrieval inspector. Features 5-stage interactive DAG flow, HyDE expansion preview, hybrid dense/sparse search telemetry, cross-encoder reranking, and grounded response synthesis.",
  "categories": [
    "ai",
    "dashboard",
    "database",
    "app"
  ]
}