Menu

Change language

GitHub repo

Vector Embeddings Inspector

blockai

Pinecone & Qdrant style vector database index inspector, similarity search tester, and dimensionality explorer with cosine distance scoring, vector heatmap visualization, and namespace filtering.

Installation

$npx uipkge-ng@latest add vector-embeddings-inspector
Manifest:https://uipkge.dev/r/angular/vector-embeddings-inspector.jsonInstalls to:src/app/components/blocks/vector-embeddings-inspector/

Variants

Loading interactive previews…

Schema

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

VectorSearchResultItem
interface VectorSearchResultItem {
  id: string
  vectorId: string
  score: number
  cosineDistance: number
  title: string
  namespace: VectorNamespace
  filePath: string
  chunkIndex: number
  totalChunks: number
  tokenCount: number
  updatedAt: string
  snippet: string
  vectorSample: number[]
}
VectorDimensionSample
interface VectorDimensionSample {
  index: number
  dimIndex: number
  value: number
  rawDimName: string
}
TelemetryCard
interface TelemetryCard {
  title: string
  value: string
  subtitle: string
  badge: string
  badgeVariant: BadgeVariant
  metricDetails: string
  icon: LucideIconData
}

Files installed (2)

  • src/app/components/blocks/vector-embeddings-inspector/vector-embeddings-inspector.component.ts42.3 kB
    import { Component, Input, computed, signal, ChangeDetectionStrategy } from '@angular/core'
    import {
      Binary,
      Check,
      Compass,
      Copy,
      Cpu,
      Database,
      Eye,
      FileCode,
      FileText,
      HardDrive,
      Layers,
      LucideAngularModule,
      RefreshCw,
      Search,
      Sparkles,
      Zap,
      type LucideIconData,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export type VectorNamespace = 'docs' | 'blog' | 'api' | 'helpdesk'
    
    export interface VectorSearchResultItem {
      id: string
      vectorId: string
      score: number
      cosineDistance: number
      title: string
      namespace: VectorNamespace
      filePath: string
      chunkIndex: number
      totalChunks: number
      tokenCount: number
      updatedAt: string
      snippet: string
      vectorSample: number[]
    }
    
    export interface VectorDimensionSample {
      index: number
      dimIndex: number
      value: number
      rawDimName: string
    }
    
    interface TelemetryCard {
      title: string
      value: string
      subtitle: string
      badge: string
      badgeVariant: BadgeVariant
      metricDetails: string
      icon: LucideIconData
    }
    
    // Index specifications
    const indexMetadata = {
      name: 'kb-docs-embeddings-v3',
      status: 'Index Ready · Sub-10ms p99',
      metric: 'Cosine Similarity',
      dimensionModel: '1536-dim · OpenAI text-embedding-3-small',
      totalVectors: '482,910 Vectors',
      shards: 4,
      replicas: 2,
      engine: 'HNSW Graph (M=16, efConstruction=200)',
      memoryTotal: '3.8 GB RAM',
      hnswMemory: '2.9 GB',
      vectorMemory: '0.9 GB',
      cacheHitRate: 99.4,
      p99Latency: '8.4ms',
      p50Latency: '2.1ms',
      p95Latency: '5.6ms',
      qpsPeak: '1,420 QPS',
      namespacesCount: 4,
    }
    
    // Telemetry overview
    const telemetryCards: TelemetryCard[] = [
      {
        title: 'Total Indexed Vectors',
        value: '482,910',
        subtitle: '+14,280 indexed today',
        badge: '4 Shards Active',
        badgeVariant: 'secondary',
        metricDetails: '100% synchronized · 0 unindexed docs',
        icon: Database,
      },
      {
        title: 'Index Memory Footprint',
        value: '3.8 GB RAM',
        subtitle: 'HNSW: 2.9 GB · Raw Vectors: 0.9 GB',
        badge: '99.4% Cache Hit',
        badgeVariant: 'outline',
        metricDetails: 'Compressed FP16 · Inverted index',
        icon: Cpu,
      },
      {
        title: 'Query Latency (p99)',
        value: '8.4ms',
        subtitle: 'p50: 2.1ms · p95: 5.6ms',
        badge: '1,420 QPS Peak',
        badgeVariant: 'secondary',
        metricDetails: 'Sub-10ms SLA guaranteed',
        icon: Zap,
      },
      {
        title: 'Index Namespaces',
        value: '4 Namespaces',
        subtitle: 'docs, helpdesk, blog, api',
        badge: 'Multi-Tenant',
        badgeVariant: 'outline',
        metricDetails: 'docs: 240k · helpdesk: 118k',
        icon: Layers,
      },
    ]
    
    // Query presets
    const queryPresets = [
      'How do I configure OKLCH color palettes in Tailwind v4?',
      'Vector quantization & memory footprint reduction',
      'Dual-framework Vue and React component sync',
      'Handling cosine distance thresholds for hybrid search',
    ]
    
    // Query vector 32 float samples (simulated 1536-dim projection)
    const queryVectorSample = [
      -0.048, 0.135, 0.092, -0.018, 0.245, -0.11, 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,
    ]
    
    // Hardcoded search results with full vector dimensions
    const allSearchResults: VectorSearchResultItem[] = [
      {
        id: 'res-1',
        vectorId: '#vec_849201',
        score: 0.942,
        cosineDistance: 0.058,
        title: 'Tailwind v4 OKLCH Architecture',
        namespace: 'docs',
        filePath: 'docs/styling/tailwind-v4-oklch.md',
        chunkIndex: 3,
        totalChunks: 8,
        tokenCount: 384,
        updatedAt: '2 hours ago',
        snippet:
          '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.',
        vectorSample: [
          -0.042, 0.128, 0.089, -0.015, 0.231, -0.104, 0.045, 0.167, -0.078, 0.002, 0.312, -0.054, 0.091, 0.183, -0.142,
          0.063, -0.021, 0.204, 0.077, -0.089, 0.115, -0.034, 0.149, 0.028, -0.165, 0.094, 0.052, -0.073, 0.188, -0.011,
          0.067, 0.134,
        ],
      },
      {
        id: 'res-2',
        vectorId: '#vec_739104',
        score: 0.887,
        cosineDistance: 0.113,
        title: 'Color Tokens Derivation Guide',
        namespace: 'docs',
        filePath: 'docs/tokens/color-derivation.md',
        chunkIndex: 1,
        totalChunks: 5,
        tokenCount: 412,
        updatedAt: '1 day ago',
        snippet:
          '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.',
        vectorSample: [
          -0.038, 0.119, 0.074, -0.029, 0.198, -0.088, 0.032, 0.145, -0.065, 0.018, 0.284, -0.041, 0.082, 0.161, -0.125,
          0.051, -0.015, 0.189, 0.062, -0.075, 0.098, -0.026, 0.131, 0.019, -0.148, 0.081, 0.044, -0.061, 0.162, -0.008,
          0.055, 0.118,
        ],
      },
      {
        id: 'res-3',
        vectorId: '#vec_610482',
        score: 0.824,
        cosineDistance: 0.176,
        title: 'Design Systems Monorepo Guide',
        namespace: 'blog',
        filePath: 'blog/2026/design-systems-monorepo.md',
        chunkIndex: 5,
        totalChunks: 12,
        tokenCount: 526,
        updatedAt: '3 days ago',
        snippet:
          '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.',
        vectorSample: [
          -0.015, 0.082, 0.061, -0.045, 0.154, -0.062, 0.021, 0.112, -0.049, 0.034, 0.215, -0.028, 0.064, 0.128, -0.098,
          0.039, -0.008, 0.142, 0.048, -0.058, 0.074, -0.018, 0.099, 0.012, -0.112, 0.062, 0.031, -0.045, 0.124, -0.004,
          0.041, 0.089,
        ],
      },
      {
        id: 'res-4',
        vectorId: '#vec_552109',
        score: 0.768,
        cosineDistance: 0.232,
        title: 'API Embedding Ingestion Pipeline',
        namespace: 'api',
        filePath: 'api/v1/embeddings/ingest-worker.ts',
        chunkIndex: 2,
        totalChunks: 6,
        tokenCount: 340,
        updatedAt: '5 days ago',
        snippet:
          'Batch ingestion pipelines chunk markdown documents at 512-token boundaries with 64-token sliding window overlap before sending to the embedding model endpoint. Vector metadata records store namespace tags, checksum hashes, and parent chunk UUIDs for deterministic cache validation.',
        vectorSample: [
          0.024, 0.051, 0.038, -0.062, 0.112, -0.041, 0.008, 0.078, -0.032, 0.051, 0.162, -0.015, 0.042, 0.094, -0.071,
          0.022, 0.004, 0.105, 0.031, -0.041, 0.052, -0.009, 0.071, 0.005, -0.084, 0.045, 0.019, -0.031, 0.088, 0.002,
          0.028, 0.062,
        ],
      },
      {
        id: 'res-5',
        vectorId: '#vec_419820',
        score: 0.715,
        cosineDistance: 0.285,
        title: 'Troubleshooting Semantic Search Discrepancies',
        namespace: 'helpdesk',
        filePath: 'helpdesk/kb/search-accuracy-faq.md',
        chunkIndex: 4,
        totalChunks: 7,
        tokenCount: 295,
        updatedAt: '1 week ago',
        snippet:
          'When semantic ranking diverges from keyword relevance, verify tokenizer parity between query embedding model and index model. Cosine thresholds below 0.75 should trigger hybrid search fallback using sparse BM25 reranking to recover exact keyword lexical matches.',
        vectorSample: [
          0.042, 0.031, 0.019, -0.078, 0.084, -0.025, -0.005, 0.052, -0.019, 0.068, 0.118, -0.008, 0.028, 0.067, -0.052,
          0.011, 0.015, 0.074, 0.018, -0.029, 0.036, -0.002, 0.048, -0.004, -0.061, 0.029, 0.008, -0.019, 0.061, 0.008,
          0.015, 0.041,
        ],
      },
    ]
    
    function getNamespaceBadgeClass(namespace: string): string {
      switch (namespace) {
        case 'docs':
          return 'bg-info/10 text-info border-info/20'
        case 'blog':
          return 'bg-chart-1/10 text-chart-1 border-chart-1/20'
        case 'api':
          return 'bg-success/10 text-success border-success/20'
        case 'helpdesk':
          return 'bg-warning/10 text-warning border-warning/20'
        default:
          return 'bg-secondary text-secondary-foreground'
      }
    }
    
    function getScoreColorClass(score: number): string {
      if (score >= 0.9) return 'text-success'
      if (score >= 0.8) return 'text-info'
      return 'text-warning'
    }
    
    function getHeatmapCellClass(val: number, isSelected: boolean): string {
      let baseColor = ''
      if (val >= 0.2) {
        baseColor = 'bg-success/25 border-success/40 text-success hover:bg-success/35'
      } else if (val >= 0.08) {
        baseColor = 'bg-success/15 border-success/25 text-success hover:bg-success/25'
      } else if (val >= 0.0) {
        baseColor = 'bg-primary/10 border-primary/20 text-foreground hover:bg-primary/20'
      } else if (val >= -0.08) {
        baseColor = 'bg-warning/10 border-warning/25 text-warning hover:bg-warning/20'
      } else {
        baseColor = 'bg-destructive/20 border-destructive/35 text-destructive hover:bg-destructive/30'
      }
    
      const selectedRing = isSelected ? 'ring-2 ring-primary ring-offset-1 ring-offset-background z-10 font-bold' : ''
      return cn(baseColor, selectedRing)
    }
    
    /** `+0.128` / `-0.042`: the sign React prints in front of each float. */
    function signed(value: number, digits: number): string {
      return (value >= 0 ? '+' : '') + value.toFixed(digits)
    }
    
    /**
     * VectorEmbeddingsInspector -- the React block, 1:1. A vector-index console: index header
     * with metric badges, four telemetry cards, a similarity-search playground (query input,
     * Top-K and namespace selects, preset pills, ranked results with cosine scores) and a
     * 32-sample heatmap of the inspected vector with a per-dimension deep-dive and raw JSON.
     * All data is hardcoded sample data -- swap `allSearchResults` / `runSearch()` for your
     * vector store's query API.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-vector-embeddings-inspector, [ui-vector-embeddings-inspector]',
      standalone: true,
      // React renders the root <div> itself: keep the host out of layout.
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCardDescriptionComponent,
        UiCardContentComponent,
        UiInputComponent,
        UiProgressComponent,
        UiSelectComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div data-slot="vector-embeddings-inspector" [class]="cn('w-full space-y-6', className)">
          <!-- Header Section -->
          <div ui-card class="border-border bg-card text-card-foreground shadow-xs">
            <div ui-card-header class="pb-4">
              <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
                <div class="space-y-1.5">
                  <div class="flex flex-wrap items-center gap-2">
                    <div class="flex flex-wrap items-center gap-2">
                      <lucide-icon [img]="Database" class="text-primary size-5" />
                      <h1 class="text-foreground text-lg font-semibold tracking-tight sm:text-xl">
                        {{ indexMetadata.name }}
                      </h1>
                    </div>
                    <span
                      ui-badge
                      wrap
                      variant="outline"
                      class="border-success/30 bg-success/10 text-success gap-1.5 font-medium"
                    >
                      <span class="bg-success inline-block size-1.5 animate-pulse rounded-full"></span>
                      {{ indexMetadata.status }}
                    </span>
                  </div>
                  <p class="text-muted-foreground text-xs sm:text-sm">
                    Vector index telemetry, approximate nearest neighbor (ANN) similarity tester, and 1536-dimensional
                    tensor inspector.
                  </p>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <button
                    ui-button
                    variant="default"
                    size="sm"
                    class="gap-1.5 shadow-xs"
                    [disabled]="isSearching()"
                    (click)="runSearch()"
                  >
                    <lucide-icon [img]="RefreshCw" [class]="cn('size-3.5', isSearching() && 'animate-spin')" />
                    <span>Test Similarity Search</span>
                  </button>
                </div>
              </div>
    
              <!-- Metric Badges Row -->
              <div class="border-border/60 mt-3 flex flex-wrap items-center gap-2 border-t pt-3">
                <span ui-badge wrap variant="outline" class="gap-1 text-xs">
                  <lucide-icon [img]="Compass" class="text-muted-foreground size-3" />
                  <span class="text-muted-foreground">Metric:</span>
                  <span class="text-foreground font-medium">{{ indexMetadata.metric }}</span>
                </span>
    
                <span ui-badge wrap variant="outline" class="gap-1 text-xs">
                  <lucide-icon [img]="Cpu" class="text-muted-foreground size-3" />
                  <span class="text-muted-foreground">Embedding:</span>
                  <span class="text-foreground font-medium">{{ indexMetadata.dimensionModel }}</span>
                </span>
    
                <span ui-badge wrap variant="secondary" class="gap-1 text-xs">
                  <lucide-icon [img]="Binary" class="text-muted-foreground size-3" />
                  <span class="text-foreground font-semibold tabular-nums">{{ indexMetadata.totalVectors }}</span>
                </span>
    
                <span ui-badge wrap variant="outline" class="gap-1 text-xs">
                  <lucide-icon [img]="HardDrive" class="text-muted-foreground size-3" />
                  <span class="text-muted-foreground">Engine:</span>
                  <span class="text-foreground font-mono text-xs">{{ indexMetadata.engine }}</span>
                </span>
              </div>
            </div>
          </div>
    
          <!-- 4 Vector Telemetry Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            @for (card of telemetryCards; track card.title) {
              <div ui-card class="border-border bg-card text-card-foreground shadow-xs transition-colors">
                <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                  <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                    {{ card.title }}
                  </h3>
                  <lucide-icon [img]="card.icon" class="text-muted-foreground size-4" />
                </div>
                <div ui-card-content class="space-y-2">
                  <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                    <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">{{ card.value }}</span>
                    <span ui-badge wrap [variant]="card.badgeVariant" class="text-xs">
                      {{ card.badge }}
                    </span>
                  </div>
                  <p class="text-muted-foreground text-xs">{{ card.subtitle }}</p>
                  <div class="border-border/60 text-muted-foreground border-t pt-2 text-xs">{{ card.metricDetails }}</div>
                </div>
              </div>
            }
          </div>
    
          <!-- Interactive Vector Similarity Search Playground -->
          <div ui-card class="border-border bg-card text-card-foreground shadow-xs">
            <div ui-card-header>
              <div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
                <div class="space-y-1">
                  <div class="flex flex-wrap items-center gap-2">
                    <lucide-icon [img]="Search" class="text-primary size-4" />
                    <h3 ui-card-title class="text-base font-semibold">Interactive Similarity Search Playground</h3>
                  </div>
                  <p ui-card-description class="text-xs">
                    Execute live vector similarity lookups against the HNSW index and inspect cosine distance scores.
                  </p>
                </div>
                <div class="flex flex-wrap items-center gap-2 text-xs">
                  <span class="text-muted-foreground">Index Latency:</span>
                  <span class="text-foreground font-mono font-medium tabular-nums">{{ searchExecutionLatency() }}</span>
                </div>
              </div>
            </div>
    
            <div ui-card-content class="space-y-5">
              <!-- Search Input & Quick Controls -->
              <div class="space-y-3">
                <div class="flex flex-col gap-2 sm:flex-row sm:items-center">
                  <div class="relative flex-1">
                    <!-- [class] (not class): lucide-angular copies the classes onto its <svg>, so the host
                         must not be positioned as well or the offset would apply twice. -->
                    <lucide-icon
                      [img]="Search"
                      [class]="'text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2'"
                    />
                    <ui-input
                      type="text"
                      [value]="searchQuery()"
                      (valueChange)="searchQuery.set($event)"
                      placeholder="Enter query to vectorize and search..."
                      class="pr-8 pl-9 text-xs sm:text-sm"
                      (keydown)="onQueryKeydown($event)"
                    />
                    @if (searchQuery()) {
                      <button
                        type="button"
                        class="text-muted-foreground hover:text-foreground absolute top-1/2 right-0.5 flex size-6 -translate-y-1/2 items-center justify-center text-xs"
                        title="Clear query"
                        (click)="searchQuery.set('')"
                      >
                        ✕
                      </button>
                    }
                  </div>
    
                  <!-- Top-K Selector -->
                  <div class="flex flex-wrap items-center gap-2">
                    <ui-select [value]="topK()" (valueChange)="onTopKChange($event)">
                      <button ui-select-trigger class="h-9 w-36 text-xs">
                        <ui-select-value placeholder="Top K Results" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="3">Top 3 Results</ui-select-item>
                        <ui-select-item value="5">Top 5 Results</ui-select-item>
                        <ui-select-item value="10">Top 10 Results</ui-select-item>
                      </ui-select-content>
                    </ui-select>
    
                    <!-- Namespace Filter -->
                    <ui-select [value]="selectedNamespace()" (valueChange)="onNamespaceChange($event)">
                      <button ui-select-trigger class="h-9 w-40 text-xs">
                        <ui-select-value placeholder="Namespace" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="all">All Namespaces (4)</ui-select-item>
                        <ui-select-item value="docs">docs (240k)</ui-select-item>
                        <ui-select-item value="blog">blog (84k)</ui-select-item>
                        <ui-select-item value="api">api (40k)</ui-select-item>
                        <ui-select-item value="helpdesk">helpdesk (118k)</ui-select-item>
                      </ui-select-content>
                    </ui-select>
    
                    <button
                      ui-button
                      variant="default"
                      size="sm"
                      class="h-9 gap-1.5 shadow-xs"
                      [disabled]="isSearching() || !searchQuery()"
                      (click)="runSearch()"
                    >
                      @if (!isSearching()) {
                        <lucide-icon [img]="Sparkles" class="size-3.5" />
                      } @else {
                        <lucide-icon [img]="RefreshCw" class="size-3.5 animate-spin" />
                      }
                      <span class="hidden sm:inline">Search</span>
                    </button>
                  </div>
                </div>
    
                <!-- Query Preset Pills -->
                <div class="flex flex-wrap items-center gap-1.5">
                  <span class="text-muted-foreground text-xs font-medium">Try Query:</span>
                  @for (preset of queryPresets; track preset) {
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      class="h-auto min-h-6 max-w-full rounded-full px-2.5 py-1 text-left text-xs whitespace-normal"
                      (click)="selectPreset(preset)"
                    >
                      {{ preset }}
                    </button>
                  }
                </div>
              </div>
    
              <!-- Query Vectorization Banner -->
              <div
                class="border-border/60 bg-muted/40 flex flex-wrap items-center justify-between gap-3 rounded-lg border p-3 text-xs"
              >
                <div class="flex flex-wrap items-center gap-2">
                  <lucide-icon [img]="Binary" class="text-primary size-4 shrink-0" />
                  <div>
                    <span class="text-foreground font-medium">Query Vector Generated:</span>
                    <!-- Text starts right at the tag: React has no leading space inside this inline span. -->
                    <span class="text-muted-foreground ml-1"
                      >Vectorized in
                      <span class="text-foreground font-mono font-medium tabular-nums">{{ embeddingLatency() }}</span> via
                      <code class="text-foreground font-mono">text-embedding-3-small</code> (1536 float32 values)</span
                    >
                  </div>
                </div>
                <button
                  ui-button
                  variant="ghost"
                  size="sm"
                  class="h-7 gap-1 text-xs"
                  (click)="activeVisualizerTarget.set('query')"
                >
                  <lucide-icon [img]="Eye" class="size-3" />
                  <span>Inspect Query Vector</span>
                </button>
              </div>
    
              <ui-separator />
    
              <!-- Search Results List -->
              <div class="space-y-3">
                <div class="flex items-center justify-between">
                  <h3 class="text-foreground text-xs font-semibold tracking-wider uppercase">
                    Similarity Search Results ({{ visibleResults().length }} matches)
                  </h3>
                  <span class="text-muted-foreground text-xs">
                    Metric: <span class="text-foreground font-medium">Cosine Similarity</span> · Threshold ≥ 0.70
                  </span>
                </div>
    
                <div class="space-y-3">
                  @for (result of visibleResults(); track result.id; let index = $index) {
                    <div
                      class="border-border bg-card/60 hover:bg-accent/10 relative rounded-lg border p-4 shadow-xs transition-colors"
                    >
                      <div class="flex flex-col gap-3">
                        <!-- Top metadata row -->
                        <div class="flex flex-wrap items-center justify-between gap-2">
                          <div class="flex flex-wrap items-center gap-2">
                            <span ui-badge wrap variant="secondary" class="font-mono text-xs font-semibold">
                              #{{ index + 1 }}
                            </span>
                            <h4 class="text-foreground text-sm font-semibold tracking-tight">{{ result.title }}</h4>
                            <span
                              ui-badge
                              variant="outline"
                              [class]="cn('text-xs font-medium capitalize', namespaceBadgeClass(result.namespace))"
                            >
                              {{ result.namespace }}
                            </span>
                            <div class="flex items-center gap-1">
                              <span ui-badge wrap variant="outline" class="text-muted-foreground font-mono text-xs">
                                {{ result.vectorId }}
                              </span>
                              <button
                                ui-button
                                variant="ghost"
                                size="sm"
                                class="text-muted-foreground hover:text-foreground size-6 p-0"
                                title="Copy Vector ID"
                                (click)="copyVectorId(result.vectorId)"
                              >
                                @if (copiedVectorId() === result.vectorId) {
                                  <lucide-icon [img]="Check" class="text-success size-3" />
                                } @else {
                                  <lucide-icon [img]="Copy" class="size-3" />
                                }
                              </button>
                            </div>
                          </div>
    
                          <!-- Scores -->
                          <div class="flex items-center gap-3 text-xs">
                            <div class="flex items-center gap-1.5">
                              <span class="text-muted-foreground">Cosine Similarity:</span>
                              <span [class]="cn('font-mono text-sm font-bold tabular-nums', scoreColorClass(result.score))">
                                {{ result.score.toFixed(3) }}
                              </span>
                            </div>
                            <div class="text-muted-foreground flex items-center gap-1 border-l pl-3">
                              <span>Distance:</span>
                              <span class="text-foreground font-mono font-medium tabular-nums">
                                {{ result.cosineDistance.toFixed(3) }}
                              </span>
                            </div>
                          </div>
                        </div>
    
                        <!-- Match score bar -->
                        <div class="space-y-1">
                          <div class="text-muted-foreground flex justify-between text-xs">
                            <span>Similarity Match</span>
                            <span class="font-mono font-medium tabular-nums">{{ (result.score * 100).toFixed(1) }}%</span>
                          </div>
                          <ui-progress [value]="result.score * 100" class="h-1.5" />
                        </div>
    
                        <!-- Chunk text snippet -->
                        <div
                          class="bg-muted/30 border-border/60 text-foreground rounded-md border p-2.5 text-xs leading-relaxed"
                        >
                          <p class="text-muted-foreground mb-1 font-mono text-xs italic">
                            Chunk #{{ result.chunkIndex }} of {{ result.totalChunks }} · {{ result.filePath }}
                          </p>
                          <p>{{ result.snippet }}</p>
                        </div>
    
                        <!-- Bottom action row -->
                        <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-2 pt-1 text-xs">
                          <div class="flex flex-wrap items-center gap-3">
                            <span class="flex items-center gap-1">
                              <lucide-icon [img]="FileText" class="size-3" />
                              {{ result.tokenCount }} tokens
                            </span>
                            <span>•</span>
                            <span>Updated {{ result.updatedAt }}</span>
                          </div>
    
                          <button
                            ui-button
                            variant="outline"
                            size="sm"
                            class="h-7 gap-1 text-xs"
                            (click)="activeVisualizerTarget.set(result.id)"
                          >
                            <lucide-icon [img]="Eye" class="size-3" />
                            <span>Inspect 1536-Dim Heatmap</span>
                          </button>
                        </div>
                      </div>
                    </div>
                  }
                </div>
              </div>
            </div>
          </div>
    
          <!-- Vector Dimension Array Visualizer -->
          <div ui-card class="border-border bg-card text-card-foreground shadow-xs">
            <div ui-card-header class="pb-3">
              <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
                <div class="space-y-1">
                  <div class="flex flex-wrap items-center gap-2">
                    <lucide-icon [img]="Binary" class="text-primary size-4" />
                    <h3 ui-card-title class="text-base font-semibold">Vector Dimension Array Visualizer</h3>
                  </div>
                  <p ui-card-description class="text-xs">
                    Visual color-coded 32-sample heatmap of the 1536 float values. Select dimensions to inspect activation
                    magnitude.
                  </p>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" (click)="copyFloatsArray()">
                    @if (copiedFloats()) {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-3.5" />
                    }
                    <span>{{ copiedFloats() ? 'Copied Array' : 'Copy Sample Floats' }}</span>
                  </button>
                  <button
                    ui-button
                    variant="outline"
                    size="sm"
                    class="h-8 gap-1.5 text-xs"
                    (click)="rawJsonExpanded.set(!rawJsonExpanded())"
                  >
                    <lucide-icon [img]="FileCode" class="size-3.5" />
                    <span>{{ rawJsonExpanded() ? 'Hide Raw JSON' : 'View Raw Floats' }}</span>
                  </button>
                </div>
              </div>
    
              <!-- Target Vector Selector Tabs -->
              <div class="border-border/60 mt-3 flex flex-wrap items-center gap-1.5 border-t pt-3">
                <span class="text-muted-foreground mr-1 text-xs font-medium">Inspecting:</span>
                <button
                  ui-button
                  variant="outline"
                  size="sm"
                  [class]="targetTabClass('h-7 rounded-md text-xs', 'query')"
                  (click)="activeVisualizerTarget.set('query')"
                >
                  Query Vector
                </button>
                @for (res of visibleResults(); track res.id; let idx = $index) {
                  <button
                    ui-button
                    variant="outline"
                    size="sm"
                    [class]="targetTabClass('h-7 rounded-md font-mono text-xs', res.id)"
                    (click)="activeVisualizerTarget.set(res.id)"
                  >
                    #{{ idx + 1 }} {{ res.vectorId }}
                  </button>
                }
              </div>
            </div>
    
            <div ui-card-content class="space-y-4">
              <!-- Target Vector Metadata Banner -->
              <div
                class="border-border/60 bg-muted/30 flex flex-wrap items-center justify-between gap-3 rounded-lg border p-3 text-xs"
              >
                <div>
                  <div class="text-foreground font-semibold">{{ activeVectorData().label }}</div>
                  <div class="text-muted-foreground font-mono text-xs">{{ activeVectorData().source }}</div>
                </div>
                <div class="flex flex-wrap items-center gap-3 font-mono text-xs">
                  <div>
                    <span class="text-muted-foreground">L2 Norm:</span>&ngsp;<span class="text-foreground font-semibold"
                      >1.0000 (Unit)</span
                    >
                  </div>
                  <div>
                    <span class="text-muted-foreground">Dimensions:</span>&ngsp;<span class="text-foreground font-semibold"
                      >1,536 floats</span
                    >
                  </div>
                  <div>
                    <span class="text-muted-foreground">Format:</span>&ngsp;<span class="text-foreground font-semibold"
                      >FP32 Dense</span
                    >
                  </div>
                </div>
              </div>
    
              <!-- 32-Sample Heatmap Grid -->
              <div class="space-y-2">
                <div class="flex items-center justify-between text-xs">
                  <span class="text-muted-foreground font-medium">
                    Sampled 32 Float Dimensions (1 of every 48 tensor indices):
                  </span>
                  <div class="flex flex-wrap items-center gap-2 text-xs">
                    <span class="flex items-center gap-1">
                      <span class="bg-destructive/80 inline-block size-2 rounded-xs"></span>
                      <span class="text-muted-foreground">&lt; -0.10</span>
                    </span>
                    <span class="flex items-center gap-1">
                      <span class="bg-warning/80 inline-block size-2 rounded-xs"></span>
                      <span class="text-muted-foreground">-0.05</span>
                    </span>
                    <span class="flex items-center gap-1">
                      <span class="bg-primary/40 inline-block size-2 rounded-xs"></span>
                      <span class="text-muted-foreground">~0.00</span>
                    </span>
                    <span class="flex items-center gap-1">
                      <span class="bg-success/80 inline-block size-2 rounded-xs"></span>
                      <span class="text-muted-foreground">&gt; +0.10</span>
                    </span>
                  </div>
                </div>
    
                <div class="grid grid-cols-4 gap-1.5 sm:grid-cols-8 md:grid-cols-16">
                  @for (sample of dimensionSamples(); track sample.index) {
                    <button
                      type="button"
                      [class]="heatmapCellClass(sample)"
                      (click)="selectedDimensionIndex.set(sample.index)"
                    >
                      <span class="text-muted-foreground font-mono text-xs leading-tight"
                        >d{{ pad4(sample.dimIndex) }}</span
                      >
                      <span class="font-mono text-xs leading-tight font-semibold tabular-nums">{{
                        signed(sample.value, 3)
                      }}</span>
                    </button>
                  }
                </div>
              </div>
    
              <!-- Selected Dimension Deep Dive Inspector -->
              <div class="border-border/80 bg-card rounded-lg border p-4 shadow-xs">
                <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
                  <div class="space-y-1">
                    <div class="flex flex-wrap items-center gap-2">
                      <span ui-badge wrap variant="outline" class="font-mono text-xs font-semibold">
                        {{ currentDimension().rawDimName }}
                      </span>
                      <span class="text-foreground text-sm font-semibold">
                        Dimension #{{ currentDimension().dimIndex }} / 1536
                      </span>
                      <span ui-badge wrap variant="secondary" class="text-xs">
                        {{ currentDimension().activation }}
                      </span>
                    </div>
                    <p class="text-muted-foreground text-xs">
                      Tensor index <code class="font-mono">{{ currentDimension().rawDimName }}</code> mapped from
                      1536-dimensional OpenAI text-embedding-3-small vector space.
                    </p>
                  </div>
    
                  <!-- Metric stats of the selected dimension -->
                  <div class="flex flex-wrap items-center gap-4 font-mono text-xs">
                    <div class="bg-muted/40 rounded-md border px-3 py-1.5">
                      <div class="text-muted-foreground text-xs">Float Value</div>
                      <div class="text-foreground text-sm font-bold tabular-nums">
                        {{ signed(currentDimension().value, 6) }}
                      </div>
                    </div>
                    <div class="bg-muted/40 rounded-md border px-3 py-1.5">
                      <div class="text-muted-foreground text-xs">Relative Magnitude</div>
                      <div class="text-foreground text-sm font-bold tabular-nums">
                        {{ currentDimension().magnitudePercent }}%
                      </div>
                    </div>
                    <div class="bg-muted/40 rounded-md border px-3 py-1.5">
                      <div class="text-muted-foreground text-xs">Sign / Polarity</div>
                      <div class="text-foreground text-sm font-bold">{{ currentDimension().sign }}</div>
                    </div>
                  </div>
                </div>
    
                <!-- Magnitude bar -->
                <div class="mt-3 space-y-1">
                  <div class="text-muted-foreground flex justify-between text-xs">
                    <span>Dimension Activation Magnitude vs Max Dimension (+0.325)</span>
                    <span class="font-mono font-medium">{{ currentDimension().magnitudePercent }}%</span>
                  </div>
                  <ui-progress [value]="currentDimension().magnitudePercent" class="h-2" />
                </div>
              </div>
    
              <!-- Raw JSON Array Drawer (Collapsible) -->
              @if (rawJsonExpanded()) {
                <div class="space-y-1">
                  <div class="text-muted-foreground text-xs font-medium">Raw FP32 Float Array (32 Sample Tensor):</div>
                  <pre
                    class="bg-muted/60 border-border/80 text-foreground overflow-x-auto rounded-lg border p-3 font-mono text-xs leading-relaxed"
                  ><code>{{ rawJson() }}</code></pre>
                </div>
              }
            </div>
          </div>
        </div>
      `,
    })
    export class UiVectorEmbeddingsInspectorComponent {
      @Input('class') className?: string
    
      protected readonly Binary = Binary
      protected readonly Check = Check
      protected readonly Compass = Compass
      protected readonly Copy = Copy
      protected readonly Cpu = Cpu
      protected readonly Database = Database
      protected readonly Eye = Eye
      protected readonly FileCode = FileCode
      protected readonly FileText = FileText
      protected readonly HardDrive = HardDrive
      protected readonly RefreshCw = RefreshCw
      protected readonly Search = Search
      protected readonly Sparkles = Sparkles
    
      protected readonly cn = cn
      protected readonly signed = signed
      protected readonly indexMetadata = indexMetadata
      protected readonly telemetryCards = telemetryCards
      protected readonly queryPresets = queryPresets
    
      readonly searchQuery = signal('How do I configure OKLCH color palettes in Tailwind v4?')
      readonly topK = signal('3')
      readonly selectedNamespace = signal('all')
      readonly isSearching = signal(false)
      readonly searchExecutionLatency = signal('3.4ms')
      readonly embeddingLatency = signal('18.2ms')
      readonly copiedVectorId = signal<string | null>(null)
      readonly copiedFloats = signal(false)
      readonly rawJsonExpanded = signal(false)
    
      /** 'query' or a search result id. */
      readonly activeVisualizerTarget = signal<string>('query')
      readonly selectedDimensionIndex = signal(10)
    
      readonly visibleResults = computed(() => {
        let list = allSearchResults
        const ns = this.selectedNamespace()
        if (ns !== 'all') {
          list = list.filter((r) => r.namespace === ns)
        }
        const k = parseInt(this.topK(), 10) || 3
        return list.slice(0, k)
      })
    
      readonly activeVectorData = computed(() => {
        const target = this.activeVisualizerTarget()
        if (target === 'query') {
          return {
            label: 'Query Embedding Vector (Prompt Projection)',
            vectorId: '#vec_query_input',
            sample: queryVectorSample,
            source: 'OpenAI text-embedding-3-small · Live query tensor',
          }
        }
        const found = allSearchResults.find((r) => r.id === target)
        if (found) {
          return {
            label: `Doc Vector: ${found.title}`,
            vectorId: found.vectorId,
            sample: found.vectorSample,
            source: `${found.namespace} namespace · ${found.filePath}`,
          }
        }
        return {
          label: 'Query Embedding Vector',
          vectorId: '#vec_query_input',
          sample: queryVectorSample,
          source: 'Live query tensor',
        }
      })
    
      readonly dimensionSamples = computed<VectorDimensionSample[]>(() =>
        this.activeVectorData().sample.map((val, idx) => ({
          index: idx,
          dimIndex: idx * 48,
          value: val,
          rawDimName: `dim_${String(idx * 48).padStart(4, '0')}`,
        })),
      )
    
      readonly currentDimension = computed(() => {
        const samples = this.dimensionSamples()
        const target = samples[this.selectedDimensionIndex()] ?? samples[0]!
        const val = target.value
        const absVal = Math.abs(val)
        const maxVal = 0.325
        const magnitudePercent = Math.min(100, Math.round((absVal / maxVal) * 100))
        return {
          ...target,
          magnitudePercent,
          sign: val >= 0 ? 'Positive (+)' : 'Negative (-)',
          activation: absVal > 0.2 ? 'Strong Activation' : absVal > 0.08 ? 'Moderate Activation' : 'Near Zero / Baseline',
        }
      })
    
      readonly rawJson = computed(() => JSON.stringify(this.activeVectorData().sample, null, 2))
    
      namespaceBadgeClass(namespace: VectorNamespace): string {
        return getNamespaceBadgeClass(namespace)
      }
    
      scoreColorClass(score: number): string {
        return getScoreColorClass(score)
      }
    
      heatmapCellClass(sample: VectorDimensionSample): string {
        return cn(
          'group flex cursor-pointer flex-col items-center justify-center rounded-md border p-1.5 text-center transition-colors select-none',
          getHeatmapCellClass(sample.value, this.selectedDimensionIndex() === sample.index),
        )
      }
    
      targetTabClass(base: string, target: string): string {
        return cn(
          base,
          this.activeVisualizerTarget() === target &&
            'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground',
        )
      }
    
      pad4(n: number): string {
        return String(n).padStart(4, '0')
      }
    
      runSearch(): void {
        this.isSearching.set(true)
        setTimeout(() => {
          this.isSearching.set(false)
          this.searchExecutionLatency.set((2.8 + Math.random() * 1.8).toFixed(1) + 'ms')
          this.embeddingLatency.set((16.5 + Math.random() * 3.5).toFixed(1) + 'ms')
        }, 320)
      }
    
      onQueryKeydown(event: KeyboardEvent): void {
        if (event.key === 'Enter') this.runSearch()
      }
    
      onTopKChange(val: string): void {
        this.topK.set(val)
        this.runSearch()
      }
    
      onNamespaceChange(val: string): void {
        this.selectedNamespace.set(val)
        this.runSearch()
      }
    
      selectPreset(preset: string): void {
        this.searchQuery.set(preset)
        this.runSearch()
      }
    
      copyVectorId(id: string): void {
        navigator.clipboard?.writeText(id)
        this.copiedVectorId.set(id)
        setTimeout(() => this.copiedVectorId.set(null), 2000)
      }
    
      copyFloatsArray(): void {
        const jsonStr = JSON.stringify(this.activeVectorData().sample, null, 2)
        navigator.clipboard?.writeText(jsonStr)
        this.copiedFloats.set(true)
        setTimeout(() => this.copiedFloats.set(false), 2000)
      }
    }
    
  • src/app/components/blocks/vector-embeddings-inspector/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/vector-embeddings-inspector.json