{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "fine-tuning-job-monitor",
  "title": "Fine Tuning Job Monitor",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/fine-tuning-job-monitor/fine-tuning-job-monitor.component.ts",
      "content": "import { Component, DestroyRef, Input, inject, signal, ChangeDetectionStrategy } from '@angular/core'\nimport {\n  Activity,\n  AlertCircle,\n  Bot,\n  Check,\n  CheckCircle2,\n  Clock,\n  Copy,\n  Cpu,\n  Database,\n  Download,\n  Gauge,\n  Loader2,\n  LucideAngularModule,\n  Rocket,\n  Save,\n  ShieldCheck,\n  Sliders,\n  Sparkles,\n  TrendingDown,\n  XCircle,\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 { UiProgressComponent } from '@/ui/progress/progress.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\n\nexport interface CheckpointRecord {\n  id: string\n  epoch: number\n  step: number\n  totalSteps: number\n  trainLoss: number\n  valLoss: number\n  size: string\n  filename: string\n  createdAgo: string\n  isBest?: boolean\n  status: 'saved' | 'best' | 'evaluating'\n}\n\nexport interface LossReading {\n  step: number\n  trainLoss: number\n  valLoss?: number\n  isProjected?: boolean\n  label?: string\n}\n\nexport interface FineTuningJobNotification {\n  title: string\n  message: string\n  type: 'success' | 'info' | 'destructive'\n}\n\nconst defaultLossData: LossReading[] = [\n  { step: 0, trainLoss: 1.84, valLoss: 1.92, label: 'Init' },\n  { step: 25, trainLoss: 1.32, valLoss: undefined },\n  { step: 50, trainLoss: 0.95, valLoss: 1.02, label: 'Warmup Done' },\n  { step: 75, trainLoss: 0.915, valLoss: undefined },\n  { step: 100, trainLoss: 0.892, valLoss: 0.945, label: 'Epoch 1 (Ckpt #1)' },\n  { step: 125, trainLoss: 0.79, valLoss: undefined },\n  { step: 150, trainLoss: 0.71, valLoss: 0.748, label: 'Step 150' },\n  { step: 175, trainLoss: 0.64, valLoss: undefined },\n  { step: 200, trainLoss: 0.584, valLoss: 0.612, label: 'Epoch 2 (Ckpt #2)' },\n  { step: 225, trainLoss: 0.535, valLoss: undefined },\n  { step: 250, trainLoss: 0.495, valLoss: 0.52, label: 'Step 250' },\n  { step: 275, trainLoss: 0.46, valLoss: undefined },\n  { step: 300, trainLoss: 0.435, valLoss: 0.468, label: 'Epoch 3 (Ckpt #3)' },\n  { step: 320, trainLoss: 0.412, valLoss: 0.458, label: 'Step 320 (Live)' },\n  // Projected points\n  { step: 360, trainLoss: 0.395, valLoss: undefined, isProjected: true },\n  { step: 400, trainLoss: 0.38, valLoss: 0.415, isProjected: true, label: 'Epoch 4 (Proj)' },\n  { step: 450, trainLoss: 0.365, valLoss: undefined, isProjected: true },\n  { step: 500, trainLoss: 0.355, valLoss: 0.39, isProjected: true, label: 'Epoch 5 (Target)' },\n]\n\nconst defaultCheckpoints: CheckpointRecord[] = [\n  {\n    id: 'ckpt-3',\n    epoch: 3,\n    step: 300,\n    totalSteps: 500,\n    trainLoss: 0.435,\n    valLoss: 0.468,\n    size: '1.2 GB LoRA',\n    filename: 'adapter_model_step300.safetensors',\n    createdAgo: '24m ago',\n    isBest: true,\n    status: 'best',\n  },\n  {\n    id: 'ckpt-2',\n    epoch: 2,\n    step: 200,\n    totalSteps: 500,\n    trainLoss: 0.584,\n    valLoss: 0.612,\n    size: '1.2 GB LoRA',\n    filename: 'adapter_model_step200.safetensors',\n    createdAgo: '1h 44m ago',\n    isBest: false,\n    status: 'saved',\n  },\n  {\n    id: 'ckpt-1',\n    epoch: 1,\n    step: 100,\n    totalSteps: 500,\n    trainLoss: 0.892,\n    valLoss: 0.945,\n    size: '1.2 GB LoRA',\n    filename: 'adapter_model_step100.safetensors',\n    createdAgo: '3h 02m ago',\n    isBest: false,\n    status: 'saved',\n  },\n]\n\n// SVG telemetry loss-curve geometry (viewBox 0 0 740 220). Steps map to x, loss 0..2 to y.\nconst SVG_WIDTH = 740\nconst PAD_LEFT = 45\nconst PAD_RIGHT = 695\nconst USABLE_W = PAD_RIGHT - PAD_LEFT\nconst PAD_TOP = 25\nconst PAD_BOTTOM = 190\nconst USABLE_H = PAD_BOTTOM - PAD_TOP\n\n/**\n * FineTuningJobMonitor -- the React block, 1:1. LLM fine-tuning job telemetry: a header\n * with copyable job id, live status and cancel / download actions, four telemetry cards\n * (training loss, validation loss, learning rate, GPU cluster), a dual-series SVG loss\n * convergence chart with step milestones, a checkpoint history table with deploy /\n * download actions, and a hyperparameters & dataset manifest.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-fine-tuning-job-monitor, [ui-fine-tuning-job-monitor]',\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    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiProgressComponent,\n    UiSeparatorComponent,\n    UiTableComponent,\n    UiTableBodyComponent,\n    UiTableCellComponent,\n    UiTableHeadComponent,\n    UiTableHeaderComponent,\n    UiTableRowComponent,\n  ],\n  template: `\n    <div data-slot=\"fine-tuning-job-monitor\" [class]=\"rootClass\">\n      <!-- Header Section -->\n      <div class=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n        <div class=\"space-y-1.5\">\n          <div class=\"flex flex-wrap items-center gap-2.5\">\n            <div class=\"flex items-center gap-2.5\">\n              <div\n                class=\"border-info/30 bg-info/10 text-info flex size-10 items-center justify-center rounded-xl border shadow-xs\"\n              >\n                <lucide-icon [img]=\"Bot\" class=\"size-5\" />\n              </div>\n              <div>\n                <div class=\"flex flex-wrap items-center gap-2\">\n                  <h1 class=\"text-foreground font-mono text-xl font-bold tracking-tight break-all sm:text-2xl\">\n                    {{ fineTunedModelName }}\n                  </h1>\n                  <button\n                    type=\"button\"\n                    class=\"bg-muted hover:bg-muted/80 text-muted-foreground hover:text-foreground border-border/80 inline-flex min-h-6 items-center gap-1 rounded-md border px-2 py-0.5 font-mono text-xs transition-colors\"\n                    [title]=\"'Copy Job ID: ' + jobId\"\n                    (click)=\"copyJobId()\"\n                  >\n                    <span class=\"text-foreground font-medium\">{{ jobId }}</span>\n                    @if (copiedJobId()) {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" class=\"size-3 opacity-70\" />\n                    }\n                  </button>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div class=\"text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs\">\n            <div class=\"flex items-center gap-1.5\">\n              <span class=\"text-muted-foreground\">Base Model:</span>\n              <span class=\"text-foreground font-medium\">{{ baseModel }}</span>\n            </div>\n            <span>·</span>\n            <!-- Live Status Badge -->\n            <div class=\"flex items-center gap-1.5\">\n              @if (!isJobCancelled()) {\n                <span class=\"relative flex size-2\">\n                  <span class=\"bg-info absolute inline-flex h-full w-full rounded-full opacity-75\"></span>\n                  <span class=\"bg-info relative inline-flex size-2 rounded-full\"></span>\n                </span>\n              } @else {\n                <span class=\"bg-destructive size-2 rounded-full\"></span>\n              }\n              <span [class]=\"statusClass()\">\n                {{ isJobCancelled() ? 'Job Cancelled by Operator' : status }}\n              </span>\n            </div>\n            <span>·</span>\n            <div class=\"flex items-center gap-1\">\n              <lucide-icon [img]=\"Clock\" class=\"size-3.5\" />\n              <span class=\"tabular-nums\">Elapsed: {{ elapsedTime }}</span>\n              <span class=\"text-muted-foreground\">· ETA {{ etaRemaining }}</span>\n            </div>\n          </div>\n        </div>\n\n        <!-- Action Buttons -->\n        <div class=\"flex flex-wrap items-center gap-2.5\">\n          <!-- px-2.5: React's size=\"sm\" gets it from has-[>svg], but here the svg sits inside <lucide-icon>. -->\n          <button\n            ui-button\n            variant=\"ghost\"\n            size=\"sm\"\n            [disabled]=\"isCancelling() || isJobCancelled()\"\n            class=\"text-muted-foreground hover:bg-destructive/10 hover:text-destructive h-8.5 gap-1.5 px-2.5 text-xs font-medium\"\n            (click)=\"handleCancelJob()\"\n          >\n            <!-- [class] (not class=) feeds only the svg, so animate-spin is not applied twice (host + svg). -->\n            <lucide-icon [img]=\"XCircle\" [class]=\"cancelIconClass()\" />\n            <span>{{ isJobCancelled() ? 'Cancelled' : isCancelling() ? 'Cancelling...' : 'Cancel Job' }}</span>\n          </button>\n\n          <button\n            ui-button\n            aria-label=\"Download attachment\"\n            variant=\"default\"\n            size=\"sm\"\n            [disabled]=\"isDownloadingWeights()\"\n            class=\"h-8.5 gap-2 px-2.5 text-xs font-medium shadow-xs\"\n            (click)=\"handleDownloadWeights()\"\n          >\n            <lucide-icon [img]=\"Download\" [class]=\"downloadIconClass()\" />\n            <span>{{ isDownloadingWeights() ? 'Preparing Archive...' : 'Download Weights / LoRA Adapter' }}</span>\n          </button>\n        </div>\n      </div>\n\n      <!-- Notification Toast Banner -->\n      @if (activeNotification(); as note) {\n        <div [class]=\"notificationClass(note)\" role=\"status\">\n          <div class=\"flex items-center gap-2.5\">\n            @if (note.type === 'success') {\n              <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-4 shrink-0\" />\n            } @else {\n              <lucide-icon [img]=\"AlertCircle\" class=\"text-destructive size-4 shrink-0\" />\n            }\n            <div>\n              <span class=\"text-foreground font-semibold\">{{ note.title }}: </span>\n              <span class=\"text-muted-foreground\">{{ note.message }}</span>\n            </div>\n          </div>\n          <button\n            ui-button\n            aria-label=\"Dismiss notification\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            class=\"text-muted-foreground hover:text-foreground size-6\"\n            (click)=\"activeNotification.set(null)\"\n          >\n            <span class=\"text-xs\">✕</span>\n          </button>\n        </div>\n      }\n\n      <!-- 4 Training Telemetry Cards -->\n      <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n        <!-- 1. Current Training Loss -->\n        <div ui-card class=\"border-border bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"pb-2\">\n            <div class=\"flex items-center justify-between gap-2\">\n              <div class=\"flex min-w-0 items-center gap-2\">\n                <div\n                  class=\"border-info/20 bg-info/10 text-info flex size-8 shrink-0 items-center justify-center rounded-lg border\"\n                >\n                  <lucide-icon [img]=\"Activity\" class=\"size-4\" />\n                </div>\n                <h3 ui-card-title class=\"truncate text-sm font-medium\">Training Loss</h3>\n              </div>\n              <span ui-badge wrap variant=\"outline\" class=\"font-mono text-xs tabular-nums\">\n                Step {{ currentStep }}/{{ totalSteps }}\n              </span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2.5 pt-1\">\n            <div class=\"flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5\">\n              <div class=\"flex items-baseline gap-1.5\">\n                <span class=\"text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums\">\n                  {{ fixed3(trainingLoss) }}\n                </span>\n                <span class=\"text-muted-foreground text-xs font-normal\">cross-entropy</span>\n              </div>\n              <div class=\"text-success flex items-center gap-1 text-xs font-semibold\">\n                <lucide-icon [img]=\"TrendingDown\" class=\"size-3.5\" />\n                <span class=\"tabular-nums\">-77.6%</span>\n              </div>\n            </div>\n\n            <div class=\"border-border/60 border-t pt-2 text-xs\">\n              <div class=\"text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-0.5\">\n                <span>Initial Loss:</span>\n                <span class=\"text-foreground font-mono font-medium tabular-nums\">{{ fixed3(initialLoss) }}</span>\n              </div>\n              <p class=\"text-muted-foreground mt-0.5 text-xs\">Target convergence corridor: &lt; 0.450</p>\n            </div>\n          </div>\n        </div>\n\n        <!-- 2. Validation Loss -->\n        <div ui-card class=\"border-border bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"pb-2\">\n            <div class=\"flex items-center justify-between gap-2\">\n              <div class=\"flex min-w-0 items-center gap-2\">\n                <div\n                  class=\"border-success/20 bg-success/10 text-success flex size-8 shrink-0 items-center justify-center rounded-lg border\"\n                >\n                  <lucide-icon [img]=\"ShieldCheck\" class=\"size-4\" />\n                </div>\n                <h3 ui-card-title class=\"truncate text-sm font-medium\">Validation Loss</h3>\n              </div>\n              <span ui-badge wrap variant=\"success\" class=\"shrink-0 text-xs\">Generalizing</span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2.5 pt-1\">\n            <div class=\"flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5\">\n              <div class=\"flex items-baseline gap-1.5\">\n                <span class=\"text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums\">\n                  {{ fixed3(validationLoss) }}\n                </span>\n                <span class=\"text-muted-foreground text-xs font-normal\">eval set</span>\n              </div>\n              <span class=\"text-success text-success text-xs font-medium tabular-nums\">+0.046 gap</span>\n            </div>\n\n            <div class=\"border-border/60 border-t pt-2 text-xs\">\n              <div class=\"text-muted-foreground flex items-center justify-between gap-x-2\">\n                <span>Overfitting Status:</span>\n                <span class=\"text-success font-medium\">No Overfitting</span>\n              </div>\n              <p class=\"text-muted-foreground mt-0.5 text-xs\">Evaluated every 50 steps on 4,250 rows</p>\n            </div>\n          </div>\n        </div>\n\n        <!-- 3. Learning Rate -->\n        <div ui-card class=\"border-border bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"pb-2\">\n            <div class=\"flex items-center justify-between gap-2\">\n              <div class=\"flex min-w-0 items-center gap-2\">\n                <div\n                  class=\"border-chart-1/20 bg-chart-1/10 text-chart-1 flex size-8 shrink-0 items-center justify-center rounded-lg border\"\n                >\n                  <lucide-icon [img]=\"Gauge\" class=\"size-4\" />\n                </div>\n                <h3 ui-card-title class=\"truncate text-sm font-medium\">Learning Rate</h3>\n              </div>\n              <span ui-badge wrap variant=\"outline\" class=\"shrink-0 font-mono text-xs\">Cosine</span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2.5 pt-1\">\n            <div class=\"flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5\">\n              <div class=\"flex items-baseline gap-1.5\">\n                <span class=\"text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums\">\n                  {{ learningRate }}\n                </span>\n                <span class=\"text-muted-foreground text-xs font-normal\">AdamW</span>\n              </div>\n              <span class=\"text-muted-foreground text-xs font-medium tabular-nums\">Decay active</span>\n            </div>\n\n            <div class=\"border-border/60 border-t pt-2 text-xs\">\n              <div class=\"text-muted-foreground flex items-center justify-between gap-x-2\">\n                <span>Schedule:</span>\n                <span class=\"text-foreground font-medium\">Cosine Decay (10% warmup)</span>\n              </div>\n              <p class=\"text-muted-foreground mt-0.5 text-xs\">Peak: 1.5e-4 · Min: 1.0e-6</p>\n            </div>\n          </div>\n        </div>\n\n        <!-- 4. GPU Compute Cluster -->\n        <div ui-card class=\"border-border bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"pb-2\">\n            <div class=\"flex items-center justify-between gap-2\">\n              <div class=\"flex min-w-0 items-center gap-2\">\n                <div\n                  class=\"border-warning/20 bg-warning/10 text-warning flex size-8 shrink-0 items-center justify-center rounded-lg border\"\n                >\n                  <lucide-icon [img]=\"Cpu\" class=\"size-4\" />\n                </div>\n                <h3 ui-card-title class=\"truncate text-sm font-medium\">GPU Compute Cluster</h3>\n              </div>\n              <span ui-badge wrap variant=\"success\" class=\"shrink-0 text-xs tabular-nums\">\n                {{ gpuUtilization }}% Utilized\n              </span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2.5 pt-1\">\n            <div class=\"flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5\">\n              <div>\n                <span class=\"text-foreground text-lg font-bold tracking-tight sm:text-xl\">{{ gpuCluster }}</span>\n              </div>\n            </div>\n\n            <div class=\"space-y-1\">\n              <div\n                ui-progress\n                [value]=\"gpuUtilization\"\n                class=\"[&_[data-slot=progress-indicator]]:bg-success h-1.5\"\n              ></div>\n              <div class=\"text-muted-foreground flex items-center justify-between gap-x-2 text-xs\">\n                <span>\n                  VRAM: <span class=\"text-foreground font-mono font-medium tabular-nums\">{{ vramUsage }}</span>\n                </span>\n                <span class=\"font-mono tabular-nums\">{{ locale(tokensPerSec) }} tok/s</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Training & Validation Loss SVG Trend Curve -->\n      <div ui-card class=\"border-border bg-card text-card-foreground shadow-xs\">\n        <div ui-card-header class=\"pb-3\">\n          <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n            <div class=\"space-y-1\">\n              <div class=\"flex items-center gap-2\">\n                <lucide-icon [img]=\"Activity\" class=\"text-primary size-4\" />\n                <h3 ui-card-title class=\"text-base font-semibold\">Training & Validation Loss Convergence</h3>\n              </div>\n              <p ui-card-description class=\"text-xs\">\n                Dual-series loss trajectory across {{ totalSteps }} gradient steps. Solid lines represent observed loss;\n                dashed lines denote projected cosine decay trajectory.\n              </p>\n            </div>\n\n            <!-- Legend & Metrics -->\n            <div class=\"flex flex-wrap items-center gap-3\">\n              <div class=\"text-muted-foreground flex flex-wrap items-center gap-3 text-xs\">\n                <div class=\"flex items-center gap-1.5\">\n                  <span class=\"bg-info size-2.5 rounded-full\"></span>\n                  <span class=\"text-foreground font-medium\">Training Loss ({{ fixed3(trainingLoss) }})</span>\n                </div>\n                <div class=\"flex items-center gap-1.5\">\n                  <span class=\"bg-success size-2.5 rounded-full\"></span>\n                  <span class=\"text-foreground font-medium\">Validation Loss ({{ fixed3(validationLoss) }})</span>\n                </div>\n                <div class=\"flex items-center gap-1.5\">\n                  <span class=\"border-destructive bg-destructive h-0.5 w-3 border-b border-dashed\"></span>\n                  <span class=\"text-muted-foreground\">Target (&lt; 0.400)</span>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div ui-card-content class=\"space-y-4\">\n          <!-- SVG Chart Canvas Area -->\n          <div class=\"bg-muted/20 border-border/60 relative w-full overflow-x-auto rounded-lg border p-2 sm:p-4\">\n            <svg\n              class=\"h-64 w-full max-w-[600px] min-w-full\"\n              [attr.viewBox]=\"'0 0 ' + svgWidth + ' 220'\"\n              preserveAspectRatio=\"none\"\n            >\n              <defs>\n                <linearGradient id=\"grad-train-loss-angular\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                  <stop offset=\"0%\" stop-color=\"var(--color-sky-500, #0ea5e9)\" stop-opacity=\"0.28\" />\n                  <stop offset=\"100%\" stop-color=\"var(--color-sky-500, #0ea5e9)\" stop-opacity=\"0.0\" />\n                </linearGradient>\n              </defs>\n\n              <!-- Shaded Target Convergence Zone (Loss 0.00 to 0.40 -> Y: 190 to 157) -->\n              <rect\n                [attr.x]=\"padLeft\"\n                [attr.y]=\"targetLossY\"\n                [attr.width]=\"usableW\"\n                [attr.height]=\"padBottom - targetLossY\"\n                fill=\"var(--color-emerald-500, #10b981)\"\n                fill-opacity=\"0.05\"\n                rx=\"4\"\n              />\n\n              <!-- Horizontal Grid Lines -->\n              <line\n                [attr.x1]=\"padLeft\"\n                y1=\"25\"\n                [attr.x2]=\"padRight\"\n                y2=\"25\"\n                stroke=\"currentColor\"\n                class=\"text-border/40\"\n                stroke-width=\"1\"\n              />\n              <line\n                [attr.x1]=\"padLeft\"\n                y1=\"66\"\n                [attr.x2]=\"padRight\"\n                y2=\"66\"\n                stroke=\"currentColor\"\n                class=\"text-border/40\"\n                stroke-width=\"1\"\n              />\n              <line\n                [attr.x1]=\"padLeft\"\n                y1=\"107\"\n                [attr.x2]=\"padRight\"\n                y2=\"107\"\n                stroke=\"currentColor\"\n                class=\"text-border/40\"\n                stroke-width=\"1\"\n              />\n              <line\n                [attr.x1]=\"padLeft\"\n                y1=\"148\"\n                [attr.x2]=\"padRight\"\n                y2=\"148\"\n                stroke=\"currentColor\"\n                class=\"text-border/40\"\n                stroke-width=\"1\"\n              />\n              <line\n                [attr.x1]=\"padLeft\"\n                [attr.y1]=\"targetLossY\"\n                [attr.x2]=\"padRight\"\n                [attr.y2]=\"targetLossY\"\n                stroke=\"var(--color-rose-500, #f43f5e)\"\n                stroke-width=\"1.5\"\n                stroke-dasharray=\"4 4\"\n              />\n              <line\n                [attr.x1]=\"padLeft\"\n                [attr.y1]=\"padBottom\"\n                [attr.x2]=\"padRight\"\n                [attr.y2]=\"padBottom\"\n                stroke=\"currentColor\"\n                class=\"text-border/40\"\n                stroke-width=\"1\"\n              />\n\n              <!-- Y-Axis Scale Labels -->\n              <text [attr.x]=\"padLeft - 8\" y=\"29\" text-anchor=\"end\" class=\"fill-muted-foreground font-mono text-xs\">\n                2.00\n              </text>\n              <text [attr.x]=\"padLeft - 8\" y=\"70\" text-anchor=\"end\" class=\"fill-muted-foreground font-mono text-xs\">\n                1.50\n              </text>\n              <text [attr.x]=\"padLeft - 8\" y=\"111\" text-anchor=\"end\" class=\"fill-muted-foreground font-mono text-xs\">\n                1.00\n              </text>\n              <text [attr.x]=\"padLeft - 8\" y=\"152\" text-anchor=\"end\" class=\"fill-muted-foreground font-mono text-xs\">\n                0.50\n              </text>\n              <text\n                [attr.x]=\"padLeft - 8\"\n                [attr.y]=\"targetLossY + 4\"\n                text-anchor=\"end\"\n                class=\"fill-destructive font-mono text-xs font-semibold\"\n              >\n                0.40\n              </text>\n              <text\n                [attr.x]=\"padLeft - 8\"\n                [attr.y]=\"padBottom + 4\"\n                text-anchor=\"end\"\n                class=\"fill-muted-foreground font-mono text-xs\"\n              >\n                0.00\n              </text>\n\n              <!-- Current Step 320 Vertical Indicator Line -->\n              <line\n                [attr.x1]=\"currentStepX\"\n                y1=\"20\"\n                [attr.x2]=\"currentStepX\"\n                [attr.y2]=\"padBottom\"\n                stroke=\"var(--color-sky-500, #0ea5e9)\"\n                stroke-width=\"1.5\"\n                stroke-dasharray=\"2 3\"\n              />\n\n              <!-- Training Loss Area & Solid Line (Observed) -->\n              <path [attr.d]=\"trainAreaHistoric\" fill=\"url(#grad-train-loss-angular)\" />\n              <path\n                [attr.d]=\"trainPathHistoric\"\n                fill=\"none\"\n                stroke=\"var(--color-sky-500, #0ea5e9)\"\n                stroke-width=\"2.5\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n              />\n\n              <!-- Training Loss Projected Line (Dashed) -->\n              <path\n                [attr.d]=\"trainPathProjected\"\n                fill=\"none\"\n                stroke=\"var(--color-sky-500, #0ea5e9)\"\n                stroke-width=\"2\"\n                stroke-dasharray=\"4 4\"\n                stroke-opacity=\"0.6\"\n                stroke-linecap=\"round\"\n              />\n\n              <!-- Validation Loss Curve (Observed Solid Emerald) -->\n              <path\n                [attr.d]=\"valPathHistoric\"\n                fill=\"none\"\n                stroke=\"var(--color-emerald-500, #10b981)\"\n                stroke-width=\"2.5\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n              />\n\n              <!-- Validation Loss Projected Line (Dashed Emerald) -->\n              <path\n                [attr.d]=\"valPathProjected\"\n                fill=\"none\"\n                stroke=\"var(--color-emerald-500, #10b981)\"\n                stroke-width=\"2\"\n                stroke-dasharray=\"4 4\"\n                stroke-opacity=\"0.6\"\n                stroke-linecap=\"round\"\n              />\n\n              <!-- Validation Loss Checkpoint Nodes (Observed) -->\n              @for (pt of valPointsHistoric; track pt.step) {\n                <circle\n                  [attr.cx]=\"mapX(pt.step)\"\n                  [attr.cy]=\"mapY(pt.valLoss ?? 0)\"\n                  r=\"4\"\n                  class=\"stroke-background fill-success cursor-pointer stroke-2 transition-transform hover:scale-125\"\n                  (mouseenter)=\"selectStep(pt.step)\"\n                />\n              }\n\n              <!-- Training Loss Interactive Point Markers -->\n              @for (pt of historicalPoints; track pt.step; let idx = $index) {\n                <circle\n                  [attr.cx]=\"mapX(pt.step)\"\n                  [attr.cy]=\"mapY(pt.trainLoss)\"\n                  [attr.r]=\"pt.step === currentStep ? 5 : 3\"\n                  [attr.class]=\"trainMarkerClass(pt)\"\n                  (mouseenter)=\"selectedStepIndex.set(idx)\"\n                />\n              }\n\n              <!-- Current Step Marker Pin Top Flag -->\n              <g [attr.transform]=\"'translate(' + (currentStepX - 44) + ', 10)'\">\n                <rect width=\"88\" height=\"18\" rx=\"4\" class=\"dark:fill-info fill-sky-600\" />\n                <text x=\"44\" y=\"13\" text-anchor=\"middle\" class=\"fill-white font-mono text-xs font-semibold\">\n                  Step 320 / 500\n                </text>\n              </g>\n            </svg>\n\n            <!-- X-Axis Step Milestones -->\n            <div class=\"mt-2 grid grid-cols-2 gap-2 text-xs sm:grid-cols-4 lg:grid-cols-7\">\n              @for (pt of milestones; track $index) {\n                <div [class]=\"milestoneClass(pt)\">\n                  <p class=\"text-foreground font-mono text-xs font-semibold tabular-nums\">Step {{ pt.step }}</p>\n                  <p class=\"text-muted-foreground truncate text-xs\">{{ pt.label }}</p>\n                  <div class=\"mt-0.5 flex items-center justify-center gap-1 font-mono text-xs tabular-nums\">\n                    <span class=\"text-info\">{{ fixed3(pt.trainLoss) }}</span>\n                    @if (pt.valLoss) {\n                      <span class=\"text-success\">/ {{ fixed3(pt.valLoss) }}</span>\n                    }\n                  </div>\n                </div>\n              }\n            </div>\n          </div>\n\n          <!-- Metric Summary Strip -->\n          <div class=\"grid grid-cols-2 gap-3 text-xs sm:grid-cols-3 lg:grid-cols-5\">\n            <div class=\"border-border bg-muted/30 rounded-lg border p-2.5\">\n              <span class=\"text-muted-foreground\">Step Progress</span>\n              <p class=\"text-foreground font-mono text-sm font-semibold tabular-nums\">\n                {{ currentStep }} / {{ totalSteps }} ({{ progressPercent }}%)\n              </p>\n              <div class=\"mt-1\">\n                <div ui-progress [value]=\"progressPercent\" class=\"[&_[data-slot=progress-indicator]]:bg-info h-1\"></div>\n              </div>\n            </div>\n\n            <div class=\"border-border bg-muted/30 rounded-lg border p-2.5\">\n              <span class=\"text-muted-foreground\">Tokens Processed</span>\n              <p class=\"text-foreground font-mono text-sm font-semibold tabular-nums\">98.4M / 153.8M</p>\n              <span class=\"text-muted-foreground text-xs\">64.0% of corpus</span>\n            </div>\n\n            <div class=\"border-border bg-muted/30 rounded-lg border p-2.5\">\n              <span class=\"text-muted-foreground\">Cluster Throughput</span>\n              <p class=\"text-foreground font-mono text-sm font-semibold tabular-nums\">\n                {{ locale(tokensPerSec) }} tok/s\n              </p>\n              <span class=\"text-success text-xs font-medium\">Zero pipeline bubbles</span>\n            </div>\n\n            <div class=\"border-border bg-muted/30 rounded-lg border p-2.5\">\n              <span class=\"text-muted-foreground\">Current Epoch</span>\n              <p class=\"text-foreground font-mono text-sm font-semibold tabular-nums\">\n                Epoch {{ currentEpoch }} of {{ totalEpochs }}\n              </p>\n              <span class=\"text-muted-foreground text-xs\">100 steps / epoch</span>\n            </div>\n\n            <div class=\"border-border bg-muted/30 col-span-2 rounded-lg border p-2.5 sm:col-span-3 lg:col-span-1\">\n              <span class=\"text-muted-foreground\">Convergence Projection</span>\n              <p class=\"text-success text-success font-mono text-sm font-semibold tabular-nums\">0.355 at Step 500</p>\n              <span class=\"text-muted-foreground text-xs\">Expected final loss</span>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Lower Section: Checkpoint History & Hyperparameters Configuration -->\n      <div class=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n        <!-- Left Column (2 cols on lg): Checkpoint History Table -->\n        <div ui-card class=\"border-border bg-card text-card-foreground shadow-xs lg:col-span-2\">\n          <div ui-card-header class=\"pb-3\">\n            <div class=\"flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between\">\n              <div class=\"space-y-0.5\">\n                <div class=\"flex items-center gap-2\">\n                  <lucide-icon [img]=\"Save\" class=\"text-primary size-4\" />\n                  <h3 ui-card-title class=\"text-base font-semibold\">Checkpoint History & Weight Exporter</h3>\n                </div>\n                <p ui-card-description class=\"text-xs\">\n                  Saved LoRA adapter snapshots. Deploy directly to test playground or download safe tensors.\n                </p>\n              </div>\n              <span ui-badge wrap variant=\"outline\" class=\"w-fit font-mono text-xs font-normal tabular-nums\">\n                {{ checkpoints.length }} Saved Snapshots\n              </span>\n            </div>\n          </div>\n          <div ui-card-content class=\"p-0 sm:p-6 sm:pt-0\">\n            <div class=\"overflow-x-auto\">\n              <div ui-table>\n                <thead ui-table-header>\n                  <tr ui-table-row>\n                    <th ui-table-head class=\"min-w-[140px] text-xs\">Checkpoint</th>\n                    <th ui-table-head class=\"min-w-[80px] text-xs\">Step</th>\n                    <th ui-table-head class=\"min-w-[90px] text-xs\">Train Loss</th>\n                    <th ui-table-head class=\"min-w-[90px] text-xs\">Val Loss</th>\n                    <th ui-table-head class=\"min-w-[110px] text-xs\">Size & Type</th>\n                    <th ui-table-head class=\"min-w-[100px] text-xs\">Created</th>\n                    <th ui-table-head class=\"min-w-[150px] text-right text-xs\">Actions</th>\n                  </tr>\n                </thead>\n                <tbody ui-table-body>\n                  @for (ckpt of checkpoints; track ckpt.id) {\n                    <tr ui-table-row [class]=\"ckpt.isBest ? 'bg-success/[0.04]' : ''\">\n                      <td ui-table-cell class=\"text-xs whitespace-nowrap\">\n                        <div class=\"space-y-0.5\">\n                          <div class=\"flex items-center gap-1.5\">\n                            <span class=\"text-foreground font-mono font-medium\">Epoch {{ ckpt.epoch }}.0</span>\n                            @if (ckpt.isBest) {\n                              <span ui-badge wrap variant=\"success\" class=\"h-4 px-1.5 text-xs font-medium\">\n                                Best Val Loss\n                              </span>\n                            }\n                          </div>\n                          <p class=\"text-muted-foreground font-mono text-xs\">{{ ckpt.filename }}</p>\n                        </div>\n                      </td>\n                      <td ui-table-cell class=\"font-mono text-xs whitespace-nowrap tabular-nums\">\n                        {{ ckpt.step }} / {{ ckpt.totalSteps }}\n                      </td>\n                      <td ui-table-cell class=\"text-xs whitespace-nowrap\">\n                        <span class=\"text-info text-info font-mono font-semibold tabular-nums\">\n                          {{ fixed3(ckpt.trainLoss) }}\n                        </span>\n                      </td>\n                      <td ui-table-cell class=\"text-xs whitespace-nowrap\">\n                        <span class=\"text-success text-success font-mono font-semibold tabular-nums\">\n                          {{ fixed3(ckpt.valLoss) }}\n                        </span>\n                      </td>\n                      <td ui-table-cell class=\"text-muted-foreground font-mono text-xs whitespace-nowrap tabular-nums\">\n                        {{ ckpt.size }}\n                      </td>\n                      <td ui-table-cell class=\"text-muted-foreground text-xs whitespace-nowrap\">\n                        {{ ckpt.createdAgo }}\n                      </td>\n                      <td ui-table-cell class=\"text-right whitespace-nowrap\">\n                        <div class=\"flex items-center justify-end gap-1.5\">\n                          <!-- px-1.5: React's size=\"xs\" gets it from has-[>svg], but here the svg sits inside <lucide-icon>. -->\n                          <button\n                            ui-button\n                            variant=\"outline\"\n                            size=\"xs\"\n                            [disabled]=\"deployingCkptId() === ckpt.id\"\n                            class=\"gap-1 px-1.5 text-xs\"\n                            (click)=\"handleDeployToPlayground(ckpt)\"\n                          >\n                            @if (deployingCkptId() === ckpt.id) {\n                              <lucide-icon [img]=\"Loader2\" [class]=\"'size-3 animate-spin'\" />\n                            } @else {\n                              <lucide-icon [img]=\"Rocket\" class=\"text-info size-3\" />\n                            }\n                            <span>{{ deployingCkptId() === ckpt.id ? 'Deploying...' : 'Deploy to Playground' }}</span>\n                          </button>\n                          <button\n                            ui-button\n                            variant=\"ghost\"\n                            size=\"icon-xs\"\n                            class=\"text-muted-foreground hover:text-foreground size-7\"\n                            title=\"Download LoRA .safetensors\"\n                            (click)=\"handleDownloadWeights()\"\n                          >\n                            <lucide-icon [img]=\"Download\" class=\"size-3.5\" />\n                          </button>\n                        </div>\n                      </td>\n                    </tr>\n                  }\n                </tbody>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Right Column (1 col on lg): Hyperparameters & Dataset Configuration Card -->\n        <div ui-card class=\"border-border bg-card text-card-foreground shadow-xs lg:col-span-1\">\n          <div ui-card-header class=\"pb-3\">\n            <div class=\"flex items-center justify-between gap-x-2\">\n              <div class=\"flex items-center gap-2\">\n                <lucide-icon [img]=\"Sliders\" class=\"text-primary size-4\" />\n                <h3 ui-card-title class=\"text-base font-semibold\">Hyperparameters & Dataset</h3>\n              </div>\n              <span ui-badge wrap variant=\"outline\" class=\"font-mono text-xs\">LoRA PEFT</span>\n            </div>\n            <p ui-card-description class=\"text-xs\">Fine-tuning recipe manifest and dataset partition metadata.</p>\n          </div>\n          <div ui-card-content class=\"space-y-4\">\n            <!-- Training Dataset Box -->\n            <div class=\"border-border/80 bg-muted/40 space-y-2 rounded-lg border p-3 text-xs\">\n              <div class=\"flex items-center justify-between gap-x-2\">\n                <div class=\"flex items-center gap-1.5\">\n                  <lucide-icon [img]=\"Database\" class=\"text-primary size-3.5\" />\n                  <span class=\"text-foreground font-semibold\">Training Dataset</span>\n                </div>\n                <button\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground transition-colors\"\n                  title=\"Copy dataset filename\"\n                  (click)=\"copyDatasetName()\"\n                >\n                  @if (copiedDataset()) {\n                    <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                  } @else {\n                    <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                  }\n                </button>\n              </div>\n              <p class=\"text-foreground font-mono text-xs font-medium break-all\">{{ trainingDataset }}</p>\n              <div class=\"text-muted-foreground flex flex-wrap items-center justify-between gap-1 text-xs\">\n                <span>{{ locale(trainingExamples) }} train · {{ locale(validationExamples) }} val</span>\n                <span class=\"font-mono tabular-nums\">{{ totalTokens }}</span>\n              </div>\n            </div>\n\n            <!-- Key Parameters Grid -->\n            <div class=\"space-y-2.5 text-xs\">\n              <div class=\"border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5\">\n                <span class=\"text-muted-foreground\">Effective Batch Size:</span>\n                <span class=\"text-foreground font-mono font-semibold tabular-nums\">\n                  {{ batchSize }}\n                  <span class=\"text-muted-foreground font-normal\"\n                    >({{ microBatchSize }} × {{ gradAccumSteps }} accum)</span\n                  >\n                </span>\n              </div>\n\n              <div class=\"border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5\">\n                <span class=\"text-muted-foreground\">Context Length:</span>\n                <span class=\"text-foreground font-mono font-semibold tabular-nums\">\n                  {{ locale(contextLength) }} tokens\n                </span>\n              </div>\n\n              <div class=\"border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5\">\n                <span class=\"text-muted-foreground\">Optimizer:</span>\n                <span class=\"text-foreground font-mono font-medium\">{{ optimizer }}</span>\n              </div>\n\n              <div class=\"border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5\">\n                <span class=\"text-muted-foreground\">LoRA Rank & Alpha:</span>\n                <span class=\"text-foreground font-mono font-semibold tabular-nums\">\n                  r={{ loraRank }}, α={{ loraAlpha }} (dropout={{ loraDropout }})\n                </span>\n              </div>\n\n              <div class=\"border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5\">\n                <span class=\"text-muted-foreground\">Target Modules:</span>\n                <span class=\"text-foreground font-mono text-xs\">All Linear (q, k, v, o, gate, up, down)</span>\n              </div>\n\n              <div class=\"border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5\">\n                <span class=\"text-muted-foreground\">Precision & Kernel:</span>\n                <span class=\"text-foreground font-mono\">bfloat16 · FlashAttention-2</span>\n              </div>\n\n              <div class=\"flex items-center justify-between gap-x-2\">\n                <span class=\"text-muted-foreground\">Parallelism Strategy:</span>\n                <span class=\"text-foreground font-medium\">PyTorch FSDP-2 (Hybrid Sharded)</span>\n              </div>\n            </div>\n\n            <ui-separator />\n\n            <!-- Export & Integration Note -->\n            <div class=\"border-border bg-muted/20 space-y-1.5 rounded-lg border p-2.5 text-xs\">\n              <div class=\"text-foreground flex items-center gap-1.5 font-medium\">\n                <lucide-icon [img]=\"Sparkles\" class=\"text-info size-3.5\" />\n                <span>HuggingFace Hub & vLLM Ready</span>\n              </div>\n              <p class=\"text-muted-foreground leading-relaxed\">\n                Checkpoints are stored in standard SafeTensors format with tokenizer configs ready for instant\n                serverless vLLM / TensorRT-LLM deployment.\n              </p>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiFineTuningJobMonitorComponent {\n  @Input() jobId = '#ft-job-2026-0842'\n  @Input() baseModel = 'Meta Llama 3.3 70B Instruct'\n  @Input() fineTunedModelName = 'llama-3.3-70b-uipkge-expert-v2'\n  @Input() status = 'Training in Progress · Epoch 3 of 5 · 64% Complete'\n  @Input() currentEpoch = 3\n  @Input() totalEpochs = 5\n  @Input() progressPercent = 64\n  @Input() currentStep = 320\n  @Input() totalSteps = 500\n  @Input() trainingLoss = 0.412\n  @Input() initialLoss = 1.84\n  @Input() validationLoss = 0.458\n  @Input() learningRate = '1.5e-5'\n  @Input() gpuCluster = '8× NVIDIA H100 80GB SXM5'\n  @Input() gpuUtilization = 100\n  @Input() vramUsage = '76.4 GB / 80 GB'\n  @Input() tokensPerSec = 3480\n  @Input() elapsedTime = '4h 18m 22s'\n  @Input() etaRemaining = '2h 25m'\n  @Input() trainingDataset = 'uipkge_synthetic_sfc_pairs.jsonl'\n  @Input() trainingExamples = 42500\n  @Input() validationExamples = 4250\n  @Input() totalTokens = '153.8M tokens'\n  @Input() batchSize = 32\n  @Input() microBatchSize = 4\n  @Input() gradAccumSteps = 8\n  @Input() contextLength = 4096\n  @Input() optimizer = 'AdamW (beta1=0.9, beta2=0.95)'\n  @Input() loraRank = 64\n  @Input() loraAlpha = 128\n  @Input() loraDropout = 0.05\n  @Input() checkpoints: CheckpointRecord[] = defaultCheckpoints\n  @Input() lossData: LossReading[] = defaultLossData\n  @Input('class') className?: string\n\n  readonly copiedJobId = signal(false)\n  readonly copiedDataset = signal(false)\n  readonly isDownloadingWeights = signal(false)\n  readonly isCancelling = signal(false)\n  readonly isJobCancelled = signal(false)\n  readonly deployingCkptId = signal<string | null>(null)\n  readonly activeNotification = signal<FineTuningJobNotification | null>(null)\n  /** Index into `lossData` of the highlighted milestone. Defaults to step 320. */\n  readonly selectedStepIndex = signal<number | null>(13)\n\n  protected readonly Activity = Activity\n  protected readonly AlertCircle = AlertCircle\n  protected readonly Bot = Bot\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Clock = Clock\n  protected readonly Copy = Copy\n  protected readonly Cpu = Cpu\n  protected readonly Database = Database\n  protected readonly Download = Download\n  protected readonly Gauge = Gauge\n  protected readonly Loader2 = Loader2\n  protected readonly Rocket = Rocket\n  protected readonly Save = Save\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Sliders = Sliders\n  protected readonly Sparkles = Sparkles\n  protected readonly TrendingDown = TrendingDown\n  protected readonly XCircle = XCircle\n\n  protected readonly svgWidth = SVG_WIDTH\n  protected readonly padLeft = PAD_LEFT\n  protected readonly padRight = PAD_RIGHT\n  protected readonly usableW = USABLE_W\n  protected readonly padBottom = PAD_BOTTOM\n\n  private readonly timers = new Set<ReturnType<typeof setTimeout>>()\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => this.timers.forEach(clearTimeout))\n  }\n\n  get rootClass(): string {\n    return cn('mx-auto w-full max-w-6xl space-y-6', this.className)\n  }\n\n  // --- formatting -------------------------------------------------------------------------\n\n  fixed3(n: number): string {\n    return n.toFixed(3)\n  }\n\n  locale(n: number): string {\n    return n.toLocaleString()\n  }\n\n  // --- classes ----------------------------------------------------------------------------\n\n  statusClass(): string {\n    return cn('font-medium', this.isJobCancelled() ? 'text-destructive' : 'text-info')\n  }\n\n  cancelIconClass(): string {\n    return cn('size-3.5', this.isCancelling() && 'animate-spin')\n  }\n\n  downloadIconClass(): string {\n    return cn('size-3.5', this.isDownloadingWeights() && 'animate-bounce')\n  }\n\n  notificationClass(note: FineTuningJobNotification): string {\n    return cn(\n      'flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs transition-colors',\n      note.type === 'destructive'\n        ? 'border-destructive/30 bg-destructive/10 text-destructive'\n        : 'border-border bg-card text-card-foreground',\n    )\n  }\n\n  trainMarkerClass(pt: LossReading): string {\n    return cn(\n      'stroke-background cursor-pointer stroke-2 transition-transform hover:scale-150',\n      pt.step === this.currentStep ? 'fill-info ring-info ring-2' : 'fill-info',\n    )\n  }\n\n  milestoneClass(pt: LossReading): string {\n    const isSelected = this.selectedStepIndex() === this.lossData.indexOf(pt)\n    return cn(\n      'rounded p-1.5 text-center transition-colors',\n      isSelected ? 'border-border/80 bg-muted border shadow-2xs' : 'bg-muted/30 border border-transparent',\n    )\n  }\n\n  // --- chart geometry ---------------------------------------------------------------------\n\n  mapX(step: number): number {\n    return PAD_LEFT + (step / this.totalSteps) * USABLE_W\n  }\n\n  mapY(loss: number): number {\n    const clamped = Math.max(0, Math.min(2.0, loss))\n    return PAD_BOTTOM - (clamped / 2.0) * USABLE_H\n  }\n\n  get targetLossY(): number {\n    return this.mapY(0.4)\n  }\n\n  get currentStepX(): number {\n    return this.mapX(this.currentStep)\n  }\n\n  get historicalPoints(): LossReading[] {\n    return this.lossData.filter((d) => !d.isProjected)\n  }\n\n  get projectedPoints(): LossReading[] {\n    const proj = this.lossData.filter((d) => d.isProjected)\n    const hist = this.historicalPoints\n    const lastHist = hist[hist.length - 1]\n    return lastHist ? [lastHist, ...proj] : proj\n  }\n\n  get valPointsHistoric(): LossReading[] {\n    return this.lossData.filter((d) => !d.isProjected && d.valLoss !== undefined)\n  }\n\n  get valPointsProjected(): LossReading[] {\n    const list = this.lossData.filter((d) => d.valLoss !== undefined && d.isProjected)\n    const hist = this.valPointsHistoric\n    const lastValHist = hist[hist.length - 1]\n    return lastValHist ? [lastValHist, ...list] : list\n  }\n\n  get milestones(): LossReading[] {\n    return this.lossData.filter((d) => d.label)\n  }\n\n  get trainPathHistoric(): string {\n    return this.curve(this.historicalPoints, (p) => p.trainLoss)\n  }\n\n  get trainAreaHistoric(): string {\n    const pts = this.historicalPoints\n    if (!pts.length) return ''\n    const firstX = this.mapX(pts[0]!.step)\n    const lastX = this.mapX(pts[pts.length - 1]!.step)\n    return `${this.trainPathHistoric} L ${lastX},${PAD_BOTTOM} L ${firstX},${PAD_BOTTOM} Z`\n  }\n\n  get trainPathProjected(): string {\n    return this.curve(this.projectedPoints, (p) => p.trainLoss)\n  }\n\n  get valPathHistoric(): string {\n    return this.curve(this.valPointsHistoric, (p) => p.valLoss ?? 0)\n  }\n\n  get valPathProjected(): string {\n    return this.curve(this.valPointsProjected, (p) => p.valLoss ?? 0)\n  }\n\n  /** Smooth cubic path through the points (control points at the horizontal midpoints). */\n  private curve(pts: LossReading[], loss: (p: LossReading) => number): string {\n    if (!pts.length) return ''\n    let path = `M ${this.mapX(pts[0]!.step)},${this.mapY(loss(pts[0]!))}`\n    for (let i = 1; i < pts.length; i++) {\n      const prev = pts[i - 1]!\n      const curr = pts[i]!\n      const x0 = this.mapX(prev.step)\n      const y0 = this.mapY(loss(prev))\n      const x1 = this.mapX(curr.step)\n      const y1 = this.mapY(loss(curr))\n      const cx = (x0 + x1) / 2\n      path += ` C ${cx},${y0} ${cx},${y1} ${x1},${y1}`\n    }\n    return path\n  }\n\n  selectStep(step: number): void {\n    this.selectedStepIndex.set(this.lossData.findIndex((d) => d.step === step))\n  }\n\n  // --- actions ----------------------------------------------------------------------------\n\n  showNotification(title: string, message: string, type: FineTuningJobNotification['type'] = 'success'): void {\n    this.activeNotification.set({ title, message, type })\n    this.later(() => {\n      this.activeNotification.update((prev) => (prev?.title === title ? null : prev))\n    }, 4000)\n  }\n\n  copyJobId(): void {\n    if (typeof navigator !== 'undefined' && navigator.clipboard) {\n      void navigator.clipboard.writeText(this.jobId)\n      this.copiedJobId.set(true)\n      this.later(() => this.copiedJobId.set(false), 2000)\n    }\n  }\n\n  copyDatasetName(): void {\n    if (typeof navigator !== 'undefined' && navigator.clipboard) {\n      void navigator.clipboard.writeText(this.trainingDataset)\n      this.copiedDataset.set(true)\n      this.later(() => this.copiedDataset.set(false), 2000)\n    }\n  }\n\n  handleDownloadWeights(): void {\n    if (this.isDownloadingWeights()) return\n    this.isDownloadingWeights.set(true)\n    this.later(() => {\n      this.isDownloadingWeights.set(false)\n      this.showNotification(\n        'Weights Download Triggered',\n        'LoRA adapter weights (adapter_model.safetensors, 1.2 GB) download initialized with SHA-256 verification.',\n      )\n    }, 900)\n  }\n\n  handleCancelJob(): void {\n    if (this.isCancelling() || this.isJobCancelled()) return\n    this.isCancelling.set(true)\n    this.later(() => {\n      this.isCancelling.set(false)\n      this.isJobCancelled.set(true)\n      this.showNotification(\n        'Job Cancellation Dispatched',\n        'SIGTERM gracefully sent to GPU cluster. Last checkpoint #3 preserved.',\n        'destructive',\n      )\n    }, 700)\n  }\n\n  handleDeployToPlayground(ckpt: CheckpointRecord): void {\n    this.deployingCkptId.set(ckpt.id)\n    this.later(() => {\n      this.deployingCkptId.set(null)\n      this.showNotification(\n        'Checkpoint Deployed to Playground',\n        `Checkpoint Epoch ${ckpt.epoch} (Step ${ckpt.step}) has been loaded into the interactive testing playground with zero cold-start latency.`,\n      )\n    }, 1000)\n  }\n\n  private later(fn: () => void, ms: number): void {\n    const t = setTimeout(() => {\n      this.timers.delete(t)\n      fn()\n    }, ms)\n    this.timers.add(t)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/fine-tuning-job-monitor/fine-tuning-job-monitor.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/fine-tuning-job-monitor/index.ts",
      "content": "export {\n  UiFineTuningJobMonitorComponent,\n  type CheckpointRecord,\n  type FineTuningJobNotification,\n  type LossReading,\n} from './fine-tuning-job-monitor.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/fine-tuning-job-monitor/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/progress.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/table.json"
  ],
  "description": "LLM fine-tuning job telemetry, loss curve convergence tracking, GPU cluster utilization, hyperparameter manifests, and LoRA checkpoint exporter.",
  "categories": [
    "ai",
    "dashboard",
    "app"
  ]
}