UIPackage
Menu

Framework

Change language

Boilerplate repo

Fine Tuning Job Monitor

blockai

LLM fine-tuning job telemetry, loss curve convergence tracking, GPU cluster utilization, hyperparameter manifests, and LoRA checkpoint exporter.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/fine-tuning-job-monitor.json
Named registry:npx shadcn-vue@latest add @uipkge/fine-tuning-job-monitorInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
jobIdstring'#ft-job-2026-0842'optional
baseModelstring'Meta Llama 3.3 70B Instruct'optional
fineTunedModelNamestring'llama-3.3-70b-uipkge-expert-v2'optional
statusstring'Training in Progress · Epoch 3 of 5 · 64% Complete'optional
currentEpochnumber3optional
totalEpochsnumber5optional
progressPercentnumber64optional
currentStepnumber320optional
totalStepsnumber500optional
trainingLossnumber0.412optional
initialLossnumber1.84optional
validationLossnumber0.458optional
learningRatestring'1.5e-5'optional
gpuClusterstring'8× NVIDIA H100 80GB SXM5'optional
gpuUtilizationnumber100optional
vramUsagestring'76.4 GB / 80 GB'optional
tokensPerSecnumber3480optional
elapsedTimestring'4h 18m 22s'optional
etaRemainingstring'2h 25m'optional
trainingDatasetstring'uipkge_synthetic_sfc_pairs.jsonl'optional
trainingExamplesnumber42500optional
validationExamplesnumber4250optional
totalTokensstring'153.8M tokens'optional
batchSizenumber32optional
microBatchSizenumber4optional
gradAccumStepsnumber8optional
contextLengthnumber4096optional
optimizerstring'AdamW (beta1=0.9, beta2=0.95)'optional
loraRanknumber64optional
loraAlphanumber128optional
loraDropoutnumber0.05optional
checkpointsCheckpointRecord[]optional
lossDataLossReading[]optional
classHTMLAttributes['class']optional

Schema

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

CheckpointRecord
interface CheckpointRecord {
  id: string
  epoch: number
  step: number
  totalSteps: number
  trainLoss: number
  valLoss: number
  size: string
  filename: string
  createdAgo: string
  isBest?: boolean
  status: 'saved' | 'best' | 'evaluating'
}
LossReading
interface LossReading {
  step: number
  trainLoss: number
  valLoss?: number
  isProjected?: boolean
  label?: string
}

Files installed (1)

  • app/components/blocks/FineTuningJobMonitor.vue45.2 kB
    <script setup lang="ts">
    import { ref, computed, type HTMLAttributes } from 'vue'
    import {
      Activity,
      AlertCircle,
      Bot,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Cpu,
      Database,
      Download,
      Gauge,
      Loader2,
      Rocket,
      Save,
      ShieldCheck,
      Sliders,
      Sparkles,
      TrendingDown,
      XCircle,
    } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
    import { Progress } from '@/components/ui/progress'
    import { Separator } from '@/components/ui/separator'
    import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
    
    export interface CheckpointRecord {
      id: string
      epoch: number
      step: number
      totalSteps: number
      trainLoss: number
      valLoss: number
      size: string
      filename: string
      createdAgo: string
      isBest?: boolean
      status: 'saved' | 'best' | 'evaluating'
    }
    
    export interface LossReading {
      step: number
      trainLoss: number
      valLoss?: number
      isProjected?: boolean
      label?: string
    }
    
    export interface FineTuningJobMonitorProps {
      jobId?: string
      baseModel?: string
      fineTunedModelName?: string
      status?: string
      currentEpoch?: number
      totalEpochs?: number
      progressPercent?: number
      currentStep?: number
      totalSteps?: number
      trainingLoss?: number
      initialLoss?: number
      validationLoss?: number
      learningRate?: string
      gpuCluster?: string
      gpuUtilization?: number
      vramUsage?: string
      tokensPerSec?: number
      elapsedTime?: string
      etaRemaining?: string
      trainingDataset?: string
      trainingExamples?: number
      validationExamples?: number
      totalTokens?: string
      batchSize?: number
      microBatchSize?: number
      gradAccumSteps?: number
      contextLength?: number
      optimizer?: string
      loraRank?: number
      loraAlpha?: number
      loraDropout?: number
      checkpoints?: CheckpointRecord[]
      lossData?: LossReading[]
      class?: HTMLAttributes['class']
    }
    
    const defaultLossData: LossReading[] = [
      { step: 0, trainLoss: 1.84, valLoss: 1.92, label: 'Init' },
      { step: 25, trainLoss: 1.32, valLoss: undefined },
      { step: 50, trainLoss: 0.95, valLoss: 1.02, label: 'Warmup Done' },
      { step: 75, trainLoss: 0.915, valLoss: undefined },
      { step: 100, trainLoss: 0.892, valLoss: 0.945, label: 'Epoch 1 (Ckpt #1)' },
      { step: 125, trainLoss: 0.79, valLoss: undefined },
      { step: 150, trainLoss: 0.71, valLoss: 0.748, label: 'Step 150' },
      { step: 175, trainLoss: 0.64, valLoss: undefined },
      { step: 200, trainLoss: 0.584, valLoss: 0.612, label: 'Epoch 2 (Ckpt #2)' },
      { step: 225, trainLoss: 0.535, valLoss: undefined },
      { step: 250, trainLoss: 0.495, valLoss: 0.52, label: 'Step 250' },
      { step: 275, trainLoss: 0.46, valLoss: undefined },
      { step: 300, trainLoss: 0.435, valLoss: 0.468, label: 'Epoch 3 (Ckpt #3)' },
      { step: 320, trainLoss: 0.412, valLoss: 0.458, label: 'Step 320 (Live)' },
      // Projected points
      { step: 360, trainLoss: 0.395, valLoss: undefined, isProjected: true },
      { step: 400, trainLoss: 0.38, valLoss: 0.415, isProjected: true, label: 'Epoch 4 (Proj)' },
      { step: 450, trainLoss: 0.365, valLoss: undefined, isProjected: true },
      { step: 500, trainLoss: 0.355, valLoss: 0.39, isProjected: true, label: 'Epoch 5 (Target)' },
    ]
    
    const defaultCheckpoints: CheckpointRecord[] = [
      {
        id: 'ckpt-3',
        epoch: 3,
        step: 300,
        totalSteps: 500,
        trainLoss: 0.435,
        valLoss: 0.468,
        size: '1.2 GB LoRA',
        filename: 'adapter_model_step300.safetensors',
        createdAgo: '24m ago',
        isBest: true,
        status: 'best',
      },
      {
        id: 'ckpt-2',
        epoch: 2,
        step: 200,
        totalSteps: 500,
        trainLoss: 0.584,
        valLoss: 0.612,
        size: '1.2 GB LoRA',
        filename: 'adapter_model_step200.safetensors',
        createdAgo: '1h 44m ago',
        isBest: false,
        status: 'saved',
      },
      {
        id: 'ckpt-1',
        epoch: 1,
        step: 100,
        totalSteps: 500,
        trainLoss: 0.892,
        valLoss: 0.945,
        size: '1.2 GB LoRA',
        filename: 'adapter_model_step100.safetensors',
        createdAgo: '3h 02m ago',
        isBest: false,
        status: 'saved',
      },
    ]
    
    const props = withDefaults(defineProps<FineTuningJobMonitorProps>(), {
      jobId: '#ft-job-2026-0842',
      baseModel: 'Meta Llama 3.3 70B Instruct',
      fineTunedModelName: 'llama-3.3-70b-uipkge-expert-v2',
      status: 'Training in Progress · Epoch 3 of 5 · 64% Complete',
      currentEpoch: 3,
      totalEpochs: 5,
      progressPercent: 64,
      currentStep: 320,
      totalSteps: 500,
      trainingLoss: 0.412,
      initialLoss: 1.84,
      validationLoss: 0.458,
      learningRate: '1.5e-5',
      gpuCluster: '8× NVIDIA H100 80GB SXM5',
      gpuUtilization: 100,
      vramUsage: '76.4 GB / 80 GB',
      tokensPerSec: 3480,
      elapsedTime: '4h 18m 22s',
      etaRemaining: '2h 25m',
      trainingDataset: 'uipkge_synthetic_sfc_pairs.jsonl',
      trainingExamples: 42500,
      validationExamples: 4250,
      totalTokens: '153.8M tokens',
      batchSize: 32,
      microBatchSize: 4,
      gradAccumSteps: 8,
      contextLength: 4096,
      optimizer: 'AdamW (beta1=0.9, beta2=0.95)',
      loraRank: 64,
      loraAlpha: 128,
      loraDropout: 0.05,
    })
    
    const activeLossData = computed(() => props.lossData ?? defaultLossData)
    const activeCheckpoints = computed(() => props.checkpoints ?? defaultCheckpoints)
    
    const copiedJobId = ref(false)
    const copiedDataset = ref(false)
    const isDownloadingWeights = ref(false)
    const isCancelling = ref(false)
    const isJobCancelled = ref(false)
    const deployingCkptId = ref<string | null>(null)
    const activeNotification = ref<{ title: string; message: string; type: 'success' | 'info' | 'destructive' } | null>(
      null,
    )
    const selectedStepIndex = ref<number | null>(13) // Default to step 320
    
    function showNotification(title: string, message: string, type: 'success' | 'info' | 'destructive' = 'success') {
      activeNotification.value = { title, message, type }
      setTimeout(() => {
        if (activeNotification.value?.title === title) {
          activeNotification.value = null
        }
      }, 4000)
    }
    
    function copyJobId() {
      if (typeof navigator !== 'undefined' && navigator.clipboard) {
        navigator.clipboard.writeText(props.jobId)
        copiedJobId.value = true
        setTimeout(() => {
          copiedJobId.value = false
        }, 2000)
      }
    }
    
    function copyDatasetName() {
      if (typeof navigator !== 'undefined' && navigator.clipboard) {
        navigator.clipboard.writeText(props.trainingDataset)
        copiedDataset.value = true
        setTimeout(() => {
          copiedDataset.value = false
        }, 2000)
      }
    }
    
    function handleDownloadWeights() {
      if (isDownloadingWeights.value) return
      isDownloadingWeights.value = true
      setTimeout(() => {
        isDownloadingWeights.value = false
        showNotification(
          'Weights Download Triggered',
          'LoRA adapter weights (adapter_model.safetensors, 1.2 GB) download initialized with SHA-256 verification.',
        )
      }, 900)
    }
    
    function handleCancelJob() {
      if (isCancelling.value || isJobCancelled.value) return
      isCancelling.value = true
      setTimeout(() => {
        isCancelling.value = false
        isJobCancelled.value = true
        showNotification(
          'Job Cancellation Dispatched',
          'SIGTERM gracefully sent to GPU cluster. Last checkpoint #3 preserved.',
          'destructive',
        )
      }, 700)
    }
    
    function handleDeployToPlayground(ckpt: CheckpointRecord) {
      deployingCkptId.value = ckpt.id
      setTimeout(() => {
        deployingCkptId.value = null
        showNotification(
          'Checkpoint Deployed to Playground',
          `Checkpoint Epoch ${ckpt.epoch} (Step ${ckpt.step}) has been loaded into the interactive testing playground with zero cold-start latency.`,
        )
      }, 1000)
    }
    
    // SVG Telemetry Loss Curves Math
    // Total steps: 0..500. Loss scale: 0.00..2.00
    // ViewBox: 740 x 220
    // Padding: left=45, right=695 (usable width=650), top=25 (loss=2.00), bottom=190 (loss=0.00) (usable height=165)
    const svgWidth = 740
    const padLeft = 45
    const padRight = 695
    const usableW = padRight - padLeft
    const padTop = 25
    const padBottom = 190
    const usableH = padBottom - padTop
    
    function mapX(step: number): number {
      return padLeft + (step / props.totalSteps) * usableW
    }
    
    function mapY(loss: number): number {
      // loss 0.00 -> 190, loss 2.00 -> 25
      const clamped = Math.max(0, Math.min(2.0, loss))
      return padBottom - (clamped / 2.0) * usableH
    }
    
    const targetLossY = computed(() => mapY(0.4))
    const currentStepX = computed(() => mapX(props.currentStep))
    
    // Separate historical points from projected points
    const historicalPoints = computed(() => activeLossData.value.filter((d) => !d.isProjected))
    const projectedPoints = computed(() => {
      const proj = activeLossData.value.filter((d) => d.isProjected)
      const lastHist = historicalPoints.value[historicalPoints.value.length - 1]
      return lastHist ? [lastHist, ...proj] : proj
    })
    
    const trainPathHistoric = computed(() => {
      const pts = historicalPoints.value
      if (!pts.length) return ''
      let path = `M ${mapX(pts[0].step)},${mapY(pts[0].trainLoss)}`
      for (let i = 1; i < pts.length; i++) {
        const prev = pts[i - 1]
        const curr = pts[i]
        const x0 = mapX(prev.step)
        const y0 = mapY(prev.trainLoss)
        const x1 = mapX(curr.step)
        const y1 = mapY(curr.trainLoss)
        const cx = (x0 + x1) / 2
        path += ` C ${cx},${y0} ${cx},${y1} ${x1},${y1}`
      }
      return path
    })
    
    const trainAreaHistoric = computed(() => {
      const pts = historicalPoints.value
      if (!pts.length) return ''
      const firstX = mapX(pts[0].step)
      const lastX = mapX(pts[pts.length - 1].step)
      return `${trainPathHistoric.value} L ${lastX},${padBottom} L ${firstX},${padBottom} Z`
    })
    
    const trainPathProjected = computed(() => {
      const pts = projectedPoints.value
      if (!pts.length) return ''
      let path = `M ${mapX(pts[0].step)},${mapY(pts[0].trainLoss)}`
      for (let i = 1; i < pts.length; i++) {
        const prev = pts[i - 1]
        const curr = pts[i]
        const x0 = mapX(prev.step)
        const y0 = mapY(prev.trainLoss)
        const x1 = mapX(curr.step)
        const y1 = mapY(curr.trainLoss)
        const cx = (x0 + x1) / 2
        path += ` C ${cx},${y0} ${cx},${y1} ${x1},${y1}`
      }
      return path
    })
    
    const valPointsHistoric = computed(() => activeLossData.value.filter((d) => !d.isProjected && d.valLoss !== undefined))
    
    const valPathHistoric = computed(() => {
      const pts = valPointsHistoric.value
      if (!pts.length) return ''
      let path = `M ${mapX(pts[0].step)},${mapY(pts[0].valLoss!)}`
      for (let i = 1; i < pts.length; i++) {
        const prev = pts[i - 1]
        const curr = pts[i]
        const x0 = mapX(prev.step)
        const y0 = mapY(prev.valLoss!)
        const x1 = mapX(curr.step)
        const y1 = mapY(curr.valLoss!)
        const cx = (x0 + x1) / 2
        path += ` C ${cx},${y0} ${cx},${y1} ${x1},${y1}`
      }
      return path
    })
    
    const valPointsProjected = computed(() => {
      const list = activeLossData.value.filter((d) => d.valLoss !== undefined && d.isProjected)
      const lastValHist = valPointsHistoric.value[valPointsHistoric.value.length - 1]
      return lastValHist ? [lastValHist, ...list] : list
    })
    
    const valPathProjected = computed(() => {
      const pts = valPointsProjected.value
      if (!pts.length) return ''
      let path = `M ${mapX(pts[0].step)},${mapY(pts[0].valLoss!)}`
      for (let i = 1; i < pts.length; i++) {
        const prev = pts[i - 1]
        const curr = pts[i]
        const x0 = mapX(prev.step)
        const y0 = mapY(prev.valLoss!)
        const x1 = mapX(curr.step)
        const y1 = mapY(curr.valLoss!)
        const cx = (x0 + x1) / 2
        path += ` C ${cx},${y0} ${cx},${y1} ${x1},${y1}`
      }
      return path
    })
    </script>
    
    <template>
      <div data-slot="fine-tuning-job-monitor" :class="cn('mx-auto w-full max-w-6xl space-y-6', props.class)">
        <!-- Header Section -->
        <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
          <div class="space-y-1.5">
            <div class="flex flex-wrap items-center gap-2.5">
              <div class="flex items-center gap-2.5">
                <div
                  class="border-info/30 bg-info/10 text-info text-info flex size-10 items-center justify-center rounded-xl border shadow-xs"
                >
                  <Bot class="size-5" />
                </div>
                <div>
                  <div class="flex flex-wrap items-center gap-2">
                    <h1 class="text-foreground font-mono text-xl font-bold tracking-tight break-all sm:text-2xl">
                      {{ fineTunedModelName }}
                    </h1>
                    <button
                      type="button"
                      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"
                      :title="'Copy Job ID: ' + jobId"
                      @click="copyJobId"
                    >
                      <span class="text-foreground font-medium">{{ jobId }}</span>
                      <Check v-if="copiedJobId" class="text-success size-3" />
                      <Copy v-else class="size-3 opacity-70" />
                    </button>
                  </div>
                </div>
              </div>
            </div>
    
            <div class="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
              <div class="flex items-center gap-1.5">
                <span class="text-muted-foreground">Base Model:</span>
                <span class="text-foreground font-medium">{{ baseModel }}</span>
              </div>
              <span>·</span>
              <!-- Live Status Badge -->
              <div class="flex items-center gap-1.5">
                <span v-if="!isJobCancelled" class="relative flex size-2">
                  <span class="bg-info absolute inline-flex h-full w-full rounded-full opacity-75" />
                  <span class="bg-info relative inline-flex size-2 rounded-full" />
                </span>
                <span v-else class="bg-destructive size-2 rounded-full" />
                <span :class="['font-medium', isJobCancelled ? 'text-destructive' : 'text-info']">
                  {{ isJobCancelled ? 'Job Cancelled by Operator' : status }}
                </span>
              </div>
              <span>·</span>
              <div class="flex items-center gap-1">
                <Clock class="size-3.5" />
                <span class="tabular-nums">Elapsed: {{ elapsedTime }}</span>
                <span class="text-muted-foreground">· ETA {{ etaRemaining }}</span>
              </div>
            </div>
          </div>
    
          <!-- Action Buttons -->
          <div class="flex flex-wrap items-center gap-2.5">
            <Button
              variant="ghost"
              size="sm"
              :disabled="isCancelling || isJobCancelled"
              class="text-muted-foreground hover:bg-destructive/10 hover:text-destructive h-8.5 gap-1.5 text-xs font-medium"
              @click="handleCancelJob"
            >
              <XCircle :class="['size-3.5', isCancelling ? 'animate-spin' : '']" />
              <span>{{ isJobCancelled ? 'Cancelled' : isCancelling ? 'Cancelling...' : 'Cancel Job' }}</span>
            </Button>
    
            <Button
              aria-label="Download attachment"
              variant="default"
              size="sm"
              :disabled="isDownloadingWeights"
              class="h-8.5 gap-2 text-xs font-medium shadow-xs"
              @click="handleDownloadWeights"
            >
              <Download :class="['size-3.5', isDownloadingWeights ? 'animate-bounce' : '']" />
              <span>{{ isDownloadingWeights ? 'Preparing Archive...' : 'Download Weights / LoRA Adapter' }}</span>
            </Button>
          </div>
        </div>
    
        <!-- Notification Toast Banner -->
        <div
          v-if="activeNotification"
          :class="[
            'flex items-center justify-between gap-x-2 rounded-lg border p-3 text-xs shadow-xs transition-colors',
            activeNotification.type === 'destructive'
              ? 'border-destructive/30 bg-destructive/10 text-destructive'
              : 'border-border bg-card text-card-foreground',
          ]"
          role="status"
        >
          <div class="flex items-center gap-2.5">
            <CheckCircle2 v-if="activeNotification.type === 'success'" class="text-success size-4 shrink-0" />
            <AlertCircle v-else class="text-destructive size-4 shrink-0" />
            <div>
              <span class="text-foreground font-semibold">{{ activeNotification.title }}: </span>
              <span class="text-muted-foreground">{{ activeNotification.message }}</span>
            </div>
          </div>
          <Button
            aria-label="Dismiss notification"
            variant="ghost"
            size="icon-sm"
            class="text-muted-foreground hover:text-foreground size-6"
            @click="activeNotification = null"
          >
            <span class="text-xs"></span>
          </Button>
        </div>
    
        <!-- 4 Training Telemetry Cards -->
        <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
          <!-- 1. Current Training Loss -->
          <Card class="border-border bg-card text-card-foreground shadow-xs">
            <CardHeader class="pb-2">
              <div class="flex items-center justify-between gap-2">
                <div class="flex min-w-0 items-center gap-2">
                  <div
                    class="border-info/20 bg-info/10 text-info flex size-8 shrink-0 items-center justify-center rounded-lg border"
                  >
                    <Activity class="size-4" />
                  </div>
                  <CardTitle class="truncate text-sm font-medium">Training Loss</CardTitle>
                </div>
                <Badge wrap variant="outline" class="font-mono text-xs tabular-nums">
                  Step {{ currentStep }}/{{ totalSteps }}
                </Badge>
              </div>
            </CardHeader>
            <CardContent class="space-y-2.5 pt-1">
              <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                <div class="flex items-baseline gap-1.5">
                  <span class="text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums">
                    {{ trainingLoss.toFixed(3) }}
                  </span>
                  <span class="text-muted-foreground text-xs font-normal">cross-entropy</span>
                </div>
                <div class="text-success flex items-center gap-1 text-xs font-semibold">
                  <TrendingDown class="size-3.5" />
                  <span class="tabular-nums">-77.6%</span>
                </div>
              </div>
    
              <div class="border-border/60 border-t pt-2 text-xs">
                <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-0.5">
                  <span>Initial Loss:</span>
                  <span class="text-foreground font-mono font-medium tabular-nums">{{ initialLoss.toFixed(3) }}</span>
                </div>
                <p class="text-muted-foreground mt-0.5 text-xs">Target convergence corridor: &lt; 0.450</p>
              </div>
            </CardContent>
          </Card>
    
          <!-- 2. Validation Loss -->
          <Card class="border-border bg-card text-card-foreground shadow-xs">
            <CardHeader class="pb-2">
              <div class="flex items-center justify-between gap-2">
                <div class="flex min-w-0 items-center gap-2">
                  <div
                    class="border-success/20 bg-success/10 text-success flex size-8 shrink-0 items-center justify-center rounded-lg border"
                  >
                    <ShieldCheck class="size-4" />
                  </div>
                  <CardTitle class="truncate text-sm font-medium">Validation Loss</CardTitle>
                </div>
                <Badge wrap variant="success" class="shrink-0 text-xs">Generalizing</Badge>
              </div>
            </CardHeader>
            <CardContent class="space-y-2.5 pt-1">
              <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                <div class="flex items-baseline gap-1.5">
                  <span class="text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums">
                    {{ validationLoss.toFixed(3) }}
                  </span>
                  <span class="text-muted-foreground text-xs font-normal">eval set</span>
                </div>
                <span class="text-success text-success text-xs font-medium tabular-nums"> +0.046 gap </span>
              </div>
    
              <div class="border-border/60 border-t pt-2 text-xs">
                <div class="text-muted-foreground flex items-center justify-between gap-x-2">
                  <span>Overfitting Status:</span>
                  <span class="text-success font-medium">No Overfitting</span>
                </div>
                <p class="text-muted-foreground mt-0.5 text-xs">Evaluated every 50 steps on 4,250 rows</p>
              </div>
            </CardContent>
          </Card>
    
          <!-- 3. Learning Rate -->
          <Card class="border-border bg-card text-card-foreground shadow-xs">
            <CardHeader class="pb-2">
              <div class="flex items-center justify-between gap-2">
                <div class="flex min-w-0 items-center gap-2">
                  <div
                    class="border-chart-1/20 bg-chart-1/10 text-chart-1 flex size-8 shrink-0 items-center justify-center rounded-lg border"
                  >
                    <Gauge class="size-4" />
                  </div>
                  <CardTitle class="truncate text-sm font-medium">Learning Rate</CardTitle>
                </div>
                <Badge wrap variant="outline" class="shrink-0 font-mono text-xs">Cosine</Badge>
              </div>
            </CardHeader>
            <CardContent class="space-y-2.5 pt-1">
              <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                <div class="flex items-baseline gap-1.5">
                  <span class="text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums">
                    {{ learningRate }}
                  </span>
                  <span class="text-muted-foreground text-xs font-normal">AdamW</span>
                </div>
                <span class="text-muted-foreground text-xs font-medium tabular-nums">Decay active</span>
              </div>
    
              <div class="border-border/60 border-t pt-2 text-xs">
                <div class="text-muted-foreground flex items-center justify-between gap-x-2">
                  <span>Schedule:</span>
                  <span class="text-foreground font-medium">Cosine Decay (10% warmup)</span>
                </div>
                <p class="text-muted-foreground mt-0.5 text-xs">Peak: 1.5e-4 · Min: 1.0e-6</p>
              </div>
            </CardContent>
          </Card>
    
          <!-- 4. GPU Compute Cluster -->
          <Card class="border-border bg-card text-card-foreground shadow-xs">
            <CardHeader class="pb-2">
              <div class="flex items-center justify-between gap-2">
                <div class="flex min-w-0 items-center gap-2">
                  <div
                    class="border-warning/20 bg-warning/10 text-warning flex size-8 shrink-0 items-center justify-center rounded-lg border"
                  >
                    <Cpu class="size-4" />
                  </div>
                  <CardTitle class="truncate text-sm font-medium">GPU Compute Cluster</CardTitle>
                </div>
                <Badge wrap variant="success" class="shrink-0 text-xs tabular-nums"> {{ gpuUtilization }}% Utilized </Badge>
              </div>
            </CardHeader>
            <CardContent class="space-y-2.5 pt-1">
              <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                <div>
                  <span class="text-foreground text-lg font-bold tracking-tight sm:text-xl">
                    {{ gpuCluster }}
                  </span>
                </div>
              </div>
    
              <div class="space-y-1">
                <Progress :model-value="gpuUtilization" class="[&_[data-slot=progress-indicator]]:bg-success h-1.5" />
                <div class="text-muted-foreground flex items-center justify-between gap-x-2 text-xs">
                  <span
                    >VRAM: <span class="text-foreground font-mono font-medium tabular-nums">{{ vramUsage }}</span></span
                  >
                  <span class="font-mono tabular-nums">{{ tokensPerSec.toLocaleString() }} tok/s</span>
                </div>
              </div>
            </CardContent>
          </Card>
        </div>
    
        <!-- Training & Validation Loss SVG Trend Curve -->
        <Card class="border-border bg-card text-card-foreground shadow-xs">
          <CardHeader class="pb-3">
            <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <div class="space-y-1">
                <div class="flex items-center gap-2">
                  <Activity class="text-primary size-4" />
                  <CardTitle class="text-base font-semibold">Training & Validation Loss Convergence</CardTitle>
                </div>
                <CardDescription class="text-xs">
                  Dual-series loss trajectory across {{ totalSteps }} gradient steps. Solid lines represent observed loss;
                  dashed lines denote projected cosine decay trajectory.
                </CardDescription>
              </div>
    
              <!-- Legend & Metrics -->
              <div class="flex flex-wrap items-center gap-3">
                <div class="text-muted-foreground flex flex-wrap items-center gap-3 text-xs">
                  <div class="flex items-center gap-1.5">
                    <span class="bg-info size-2.5 rounded-full" />
                    <span class="text-foreground font-medium">Training Loss ({{ trainingLoss.toFixed(3) }})</span>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <span class="bg-success size-2.5 rounded-full" />
                    <span class="text-foreground font-medium">Validation Loss ({{ validationLoss.toFixed(3) }})</span>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <span class="border-destructive bg-destructive h-0.5 w-3 border-b border-dashed" />
                    <span class="text-muted-foreground">Target (&lt; 0.400)</span>
                  </div>
                </div>
              </div>
            </div>
          </CardHeader>
    
          <CardContent class="space-y-4">
            <!-- SVG Chart Canvas Area -->
            <div class="bg-muted/20 border-border/60 relative w-full overflow-x-auto rounded-lg border p-2 sm:p-4">
              <svg class="h-64 w-full max-w-[600px] min-w-full" :viewBox="`0 0 ${svgWidth} 220`" preserveAspectRatio="none">
                <defs>
                  <linearGradient id="grad-train-loss-vue" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.28" />
                    <stop offset="100%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.0" />
                  </linearGradient>
                </defs>
    
                <!-- Shaded Target Convergence Zone (Loss 0.00 to 0.40 -> Y: 190 to 157) -->
                <rect
                  :x="padLeft"
                  :y="targetLossY"
                  :width="usableW"
                  :height="padBottom - targetLossY"
                  fill="var(--color-emerald-500, #10b981)"
                  fill-opacity="0.05"
                  rx="4"
                />
    
                <!-- Horizontal Grid Lines -->
                <line
                  :x1="padLeft"
                  y1="25"
                  :x2="padRight"
                  y2="25"
                  stroke="currentColor"
                  class="text-border/40"
                  stroke-width="1"
                />
                <line
                  :x1="padLeft"
                  y1="66"
                  :x2="padRight"
                  y2="66"
                  stroke="currentColor"
                  class="text-border/40"
                  stroke-width="1"
                />
                <line
                  :x1="padLeft"
                  y1="107"
                  :x2="padRight"
                  y2="107"
                  stroke="currentColor"
                  class="text-border/40"
                  stroke-width="1"
                />
                <line
                  :x1="padLeft"
                  y1="148"
                  :x2="padRight"
                  y2="148"
                  stroke="currentColor"
                  class="text-border/40"
                  stroke-width="1"
                />
                <line
                  :x1="padLeft"
                  :y1="targetLossY"
                  :x2="padRight"
                  :y2="targetLossY"
                  stroke="var(--color-rose-500, #f43f5e)"
                  stroke-width="1.5"
                  stroke-dasharray="4 4"
                />
                <line
                  :x1="padLeft"
                  :y1="padBottom"
                  :x2="padRight"
                  :y2="padBottom"
                  stroke="currentColor"
                  class="text-border/40"
                  stroke-width="1"
                />
    
                <!-- Y-Axis Scale Labels -->
                <text :x="padLeft - 8" y="29" text-anchor="end" class="fill-muted-foreground font-mono text-xs">2.00</text>
                <text :x="padLeft - 8" y="70" text-anchor="end" class="fill-muted-foreground font-mono text-xs">1.50</text>
                <text :x="padLeft - 8" y="111" text-anchor="end" class="fill-muted-foreground font-mono text-xs">1.00</text>
                <text :x="padLeft - 8" y="152" text-anchor="end" class="fill-muted-foreground font-mono text-xs">0.50</text>
                <text
                  :x="padLeft - 8"
                  :y="targetLossY + 4"
                  text-anchor="end"
                  class="fill-destructive font-mono text-xs font-semibold"
                >
                  0.40
                </text>
                <text :x="padLeft - 8" :y="padBottom + 4" text-anchor="end" class="fill-muted-foreground font-mono text-xs">
                  0.00
                </text>
    
                <!-- Current Step 320 Vertical Indicator Line -->
                <line
                  :x1="currentStepX"
                  y1="20"
                  :x2="currentStepX"
                  :y2="padBottom"
                  stroke="var(--color-sky-500, #0ea5e9)"
                  stroke-width="1.5"
                  stroke-dasharray="2 3"
                />
    
                <!-- Training Loss Area & Solid Line (Observed) -->
                <path :d="trainAreaHistoric" fill="url(#grad-train-loss-vue)" />
                <path
                  :d="trainPathHistoric"
                  fill="none"
                  stroke="var(--color-sky-500, #0ea5e9)"
                  stroke-width="2.5"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
    
                <!-- Training Loss Projected Line (Dashed) -->
                <path
                  :d="trainPathProjected"
                  fill="none"
                  stroke="var(--color-sky-500, #0ea5e9)"
                  stroke-width="2"
                  stroke-dasharray="4 4"
                  stroke-opacity="0.6"
                  stroke-linecap="round"
                />
    
                <!-- Validation Loss Curve (Observed Solid Emerald) -->
                <path
                  :d="valPathHistoric"
                  fill="none"
                  stroke="var(--color-emerald-500, #10b981)"
                  stroke-width="2.5"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
    
                <!-- Validation Loss Projected Line (Dashed Emerald) -->
                <path
                  :d="valPathProjected"
                  fill="none"
                  stroke="var(--color-emerald-500, #10b981)"
                  stroke-width="2"
                  stroke-dasharray="4 4"
                  stroke-opacity="0.6"
                  stroke-linecap="round"
                />
    
                <!-- Validation Loss Checkpoint Nodes (Observed) -->
                <g v-for="pt in valPointsHistoric" :key="'val-' + pt.step">
                  <circle
                    :cx="mapX(pt.step)"
                    :cy="mapY(pt.valLoss!)"
                    r="4"
                    class="stroke-background fill-success cursor-pointer stroke-2 transition-transform hover:scale-125"
                    @mouseenter="selectedStepIndex = activeLossData.findIndex((d) => d.step === pt.step)"
                  />
                </g>
    
                <!-- Training Loss Interactive Point Markers -->
                <g v-for="(pt, idx) in historicalPoints" :key="'train-' + pt.step">
                  <circle
                    :cx="mapX(pt.step)"
                    :cy="mapY(pt.trainLoss)"
                    :r="pt.step === currentStep ? 5 : 3"
                    :class="[
                      'stroke-background cursor-pointer stroke-2 transition-transform hover:scale-150',
                      pt.step === currentStep ? 'fill-info ring-info ring-2' : 'fill-info',
                    ]"
                    @mouseenter="selectedStepIndex = idx"
                  />
                </g>
    
                <!-- Current Step Marker Pin Top Flag -->
                <g :transform="`translate(${currentStepX - 44}, 10)`">
                  <rect width="88" height="18" rx="4" class="dark:fill-info fill-sky-600" />
                  <text x="44" y="13" text-anchor="middle" class="fill-white font-mono text-xs font-semibold">
                    Step 320 / 500
                  </text>
                </g>
              </svg>
    
              <!-- X-Axis Step Milestones -->
              <div class="mt-2 grid grid-cols-2 gap-2 text-xs sm:grid-cols-4 lg:grid-cols-7">
                <div
                  v-for="(pt, idx) in activeLossData.filter((d) => d.label)"
                  :key="idx"
                  :class="[
                    'rounded p-1.5 text-center transition-colors',
                    selectedStepIndex === activeLossData.indexOf(pt)
                      ? 'bg-muted border-border/80 border shadow-2xs'
                      : 'bg-muted/30 border border-transparent',
                  ]"
                >
                  <p class="text-foreground font-mono text-xs font-semibold tabular-nums">Step {{ pt.step }}</p>
                  <p class="text-muted-foreground truncate text-xs">{{ pt.label }}</p>
                  <div class="mt-0.5 flex items-center justify-center gap-1 font-mono text-xs tabular-nums">
                    <span class="text-info">{{ pt.trainLoss.toFixed(3) }}</span>
                    <span v-if="pt.valLoss" class="text-success"> / {{ pt.valLoss.toFixed(3) }} </span>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Metric Summary Strip -->
            <div class="grid grid-cols-2 gap-3 text-xs sm:grid-cols-3 lg:grid-cols-5">
              <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                <span class="text-muted-foreground">Step Progress</span>
                <p class="text-foreground font-mono text-sm font-semibold tabular-nums">
                  {{ currentStep }} / {{ totalSteps }} ({{ progressPercent }}%)
                </p>
                <div class="mt-1">
                  <Progress :model-value="progressPercent" class="[&_[data-slot=progress-indicator]]:bg-info h-1" />
                </div>
              </div>
    
              <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                <span class="text-muted-foreground">Tokens Processed</span>
                <p class="text-foreground font-mono text-sm font-semibold tabular-nums">98.4M / 153.8M</p>
                <span class="text-muted-foreground text-xs">64.0% of corpus</span>
              </div>
    
              <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                <span class="text-muted-foreground">Cluster Throughput</span>
                <p class="text-foreground font-mono text-sm font-semibold tabular-nums">
                  {{ tokensPerSec.toLocaleString() }} tok/s
                </p>
                <span class="text-success text-xs font-medium">Zero pipeline bubbles</span>
              </div>
    
              <div class="border-border bg-muted/30 rounded-lg border p-2.5">
                <span class="text-muted-foreground">Current Epoch</span>
                <p class="text-foreground font-mono text-sm font-semibold tabular-nums">
                  Epoch {{ currentEpoch }} of {{ totalEpochs }}
                </p>
                <span class="text-muted-foreground text-xs">100 steps / epoch</span>
              </div>
    
              <div class="border-border bg-muted/30 col-span-2 rounded-lg border p-2.5 sm:col-span-3 lg:col-span-1">
                <span class="text-muted-foreground">Convergence Projection</span>
                <p class="text-success text-success font-mono text-sm font-semibold tabular-nums">0.355 at Step 500</p>
                <span class="text-muted-foreground text-xs">Expected final loss</span>
              </div>
            </div>
          </CardContent>
        </Card>
    
        <!-- Lower Section: Checkpoint History & Hyperparameters Configuration -->
        <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
          <!-- Left Column (2 cols on lg): Checkpoint History Table -->
          <Card class="border-border bg-card text-card-foreground shadow-xs lg:col-span-2">
            <CardHeader class="pb-3">
              <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
                <div class="space-y-0.5">
                  <div class="flex items-center gap-2">
                    <Save class="text-primary size-4" />
                    <CardTitle class="text-base font-semibold">Checkpoint History & Weight Exporter</CardTitle>
                  </div>
                  <CardDescription class="text-xs">
                    Saved LoRA adapter snapshots. Deploy directly to test playground or download safe tensors.
                  </CardDescription>
                </div>
                <Badge wrap variant="outline" class="w-fit font-mono text-xs font-normal tabular-nums">
                  {{ activeCheckpoints.length }} Saved Snapshots
                </Badge>
              </div>
            </CardHeader>
            <CardContent class="p-0 sm:p-6 sm:pt-0">
              <div class="overflow-x-auto">
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead class="min-w-[140px] text-xs">Checkpoint</TableHead>
                      <TableHead class="min-w-[80px] text-xs">Step</TableHead>
                      <TableHead class="min-w-[90px] text-xs">Train Loss</TableHead>
                      <TableHead class="min-w-[90px] text-xs">Val Loss</TableHead>
                      <TableHead class="min-w-[110px] text-xs">Size & Type</TableHead>
                      <TableHead class="min-w-[100px] text-xs">Created</TableHead>
                      <TableHead class="min-w-[150px] text-right text-xs">Actions</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    <TableRow
                      v-for="ckpt in activeCheckpoints"
                      :key="ckpt.id"
                      :class="ckpt.isBest ? 'bg-success/[0.04]' : ''"
                    >
                      <TableCell class="text-xs whitespace-nowrap">
                        <div class="space-y-0.5">
                          <div class="flex items-center gap-1.5">
                            <span class="text-foreground font-mono font-medium">Epoch {{ ckpt.epoch }}.0</span>
                            <Badge wrap v-if="ckpt.isBest" variant="success" class="h-4 px-1.5 text-xs font-medium">
                              Best Val Loss
                            </Badge>
                          </div>
                          <p class="text-muted-foreground font-mono text-xs">{{ ckpt.filename }}</p>
                        </div>
                      </TableCell>
                      <TableCell class="font-mono text-xs whitespace-nowrap tabular-nums">
                        {{ ckpt.step }} / {{ ckpt.totalSteps }}
                      </TableCell>
                      <TableCell class="text-xs whitespace-nowrap">
                        <span class="text-info text-info font-mono font-semibold tabular-nums">
                          {{ ckpt.trainLoss.toFixed(3) }}
                        </span>
                      </TableCell>
                      <TableCell class="text-xs whitespace-nowrap">
                        <span class="text-success text-success font-mono font-semibold tabular-nums">
                          {{ ckpt.valLoss.toFixed(3) }}
                        </span>
                      </TableCell>
                      <TableCell class="text-muted-foreground font-mono text-xs whitespace-nowrap tabular-nums">
                        {{ ckpt.size }}
                      </TableCell>
                      <TableCell class="text-muted-foreground text-xs whitespace-nowrap">
                        {{ ckpt.createdAgo }}
                      </TableCell>
                      <TableCell class="text-right whitespace-nowrap">
                        <div class="flex items-center justify-end gap-1.5">
                          <Button
                            variant="outline"
                            size="xs"
                            :disabled="deployingCkptId === ckpt.id"
                            class="gap-1 text-xs"
                            @click="handleDeployToPlayground(ckpt)"
                          >
                            <Loader2 v-if="deployingCkptId === ckpt.id" class="size-3 animate-spin" />
                            <Rocket v-else class="text-info size-3" />
                            <span>{{ deployingCkptId === ckpt.id ? 'Deploying...' : 'Deploy to Playground' }}</span>
                          </Button>
                          <Button
                            variant="ghost"
                            size="icon-xs"
                            class="text-muted-foreground hover:text-foreground size-7"
                            title="Download LoRA .safetensors"
                            @click="handleDownloadWeights"
                          >
                            <Download class="size-3.5" />
                          </Button>
                        </div>
                      </TableCell>
                    </TableRow>
                  </TableBody>
                </Table>
              </div>
            </CardContent>
          </Card>
    
          <!-- Right Column (1 col on lg): Hyperparameters & Dataset Configuration Card -->
          <Card class="border-border bg-card text-card-foreground shadow-xs lg:col-span-1">
            <CardHeader class="pb-3">
              <div class="flex items-center justify-between gap-x-2">
                <div class="flex items-center gap-2">
                  <Sliders class="text-primary size-4" />
                  <CardTitle class="text-base font-semibold">Hyperparameters & Dataset</CardTitle>
                </div>
                <Badge wrap variant="outline" class="font-mono text-xs">LoRA PEFT</Badge>
              </div>
              <CardDescription class="text-xs">
                Fine-tuning recipe manifest and dataset partition metadata.
              </CardDescription>
            </CardHeader>
            <CardContent class="space-y-4">
              <!-- Training Dataset Box -->
              <div class="border-border/80 bg-muted/40 space-y-2 rounded-lg border p-3 text-xs">
                <div class="flex items-center justify-between gap-x-2">
                  <div class="flex items-center gap-1.5">
                    <Database class="text-primary size-3.5" />
                    <span class="text-foreground font-semibold">Training Dataset</span>
                  </div>
                  <button
                    type="button"
                    class="text-muted-foreground hover:text-foreground transition-colors"
                    title="Copy dataset filename"
                    @click="copyDatasetName"
                  >
                    <Check v-if="copiedDataset" class="text-success size-3" />
                    <Copy v-else class="size-3" />
                  </button>
                </div>
                <p class="text-foreground font-mono text-xs font-medium break-all">{{ trainingDataset }}</p>
                <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-1 text-xs">
                  <span>{{ trainingExamples.toLocaleString() }} train · {{ validationExamples.toLocaleString() }} val</span>
                  <span class="font-mono tabular-nums">{{ totalTokens }}</span>
                </div>
              </div>
    
              <!-- Key Parameters Grid -->
              <div class="space-y-2.5 text-xs">
                <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5">
                  <span class="text-muted-foreground">Effective Batch Size:</span>
                  <span class="text-foreground font-mono font-semibold tabular-nums">
                    {{ batchSize }}
                    <span class="text-muted-foreground font-normal"
                      >({{ microBatchSize }} × {{ gradAccumSteps }} accum)</span
                    >
                  </span>
                </div>
    
                <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5">
                  <span class="text-muted-foreground">Context Length:</span>
                  <span class="text-foreground font-mono font-semibold tabular-nums">
                    {{ contextLength.toLocaleString() }} tokens
                  </span>
                </div>
    
                <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5">
                  <span class="text-muted-foreground">Optimizer:</span>
                  <span class="text-foreground font-mono font-medium">{{ optimizer }}</span>
                </div>
    
                <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5">
                  <span class="text-muted-foreground">LoRA Rank & Alpha:</span>
                  <span class="text-foreground font-mono font-semibold tabular-nums">
                    r={{ loraRank }}, α={{ loraAlpha }} (dropout={{ loraDropout }})
                  </span>
                </div>
    
                <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5">
                  <span class="text-muted-foreground">Target Modules:</span>
                  <span class="text-foreground font-mono text-xs">All Linear (q, k, v, o, gate, up, down)</span>
                </div>
    
                <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5">
                  <span class="text-muted-foreground">Precision & Kernel:</span>
                  <span class="text-foreground font-mono">bfloat16 · FlashAttention-2</span>
                </div>
    
                <div class="flex items-center justify-between gap-x-2">
                  <span class="text-muted-foreground">Parallelism Strategy:</span>
                  <span class="text-foreground font-medium">PyTorch FSDP-2 (Hybrid Sharded)</span>
                </div>
              </div>
    
              <Separator />
    
              <!-- Export & Integration Note -->
              <div class="border-border bg-muted/20 space-y-1.5 rounded-lg border p-2.5 text-xs">
                <div class="text-foreground flex items-center gap-1.5 font-medium">
                  <Sparkles class="text-info size-3.5" />
                  <span>HuggingFace Hub & vLLM Ready</span>
                </div>
                <p class="text-muted-foreground leading-relaxed">
                  Checkpoints are stored in standard SafeTensors format with tokenizer configs ready for instant serverless
                  vLLM / TensorRT-LLM deployment.
                </p>
              </div>
            </CardContent>
          </Card>
        </div>
      </div>
    </template>
    

Raw manifest:https://uipkge.dev/r/vue/fine-tuning-job-monitor.json