
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
LLM fine-tuning job telemetry, loss curve convergence tracking, GPU cluster utilization, hyperparameter manifests, and LoRA checkpoint exporter.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/fine-tuning-job-monitor.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/fine-tuning-job-monitor.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/fine-tuning-job-monitor.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/fine-tuning-job-monitor.jsonnpx shadcn-vue@latest add @uipkge/fine-tuning-job-monitorInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
jobId | string | '#ft-job-2026-0842' | optional |
baseModel | string | 'Meta Llama 3.3 70B Instruct' | optional |
fineTunedModelName | string | 'llama-3.3-70b-uipkge-expert-v2' | optional |
status | string | 'Training in Progress · Epoch 3 of 5 · 64% Complete' | optional |
currentEpoch | number | 3 | optional |
totalEpochs | number | 5 | optional |
progressPercent | number | 64 | optional |
currentStep | number | 320 | optional |
totalSteps | number | 500 | optional |
trainingLoss | number | 0.412 | optional |
initialLoss | number | 1.84 | optional |
validationLoss | number | 0.458 | optional |
learningRate | string | '1.5e-5' | optional |
gpuCluster | string | '8× NVIDIA H100 80GB SXM5' | optional |
gpuUtilization | number | 100 | optional |
vramUsage | string | '76.4 GB / 80 GB' | optional |
tokensPerSec | number | 3480 | optional |
elapsedTime | string | '4h 18m 22s' | optional |
etaRemaining | string | '2h 25m' | optional |
trainingDataset | string | 'uipkge_synthetic_sfc_pairs.jsonl' | optional |
trainingExamples | number | 42500 | optional |
validationExamples | number | 4250 | optional |
totalTokens | string | '153.8M tokens' | optional |
batchSize | number | 32 | optional |
microBatchSize | number | 4 | optional |
gradAccumSteps | number | 8 | optional |
contextLength | number | 4096 | optional |
optimizer | string | 'AdamW (beta1=0.9, beta2=0.95)' | optional |
loraRank | number | 64 | optional |
loraAlpha | number | 128 | optional |
loraDropout | number | 0.05 | optional |
checkpoints | CheckpointRecord[] | — | optional |
lossData | LossReading[] | — | optional |
class | HTMLAttributes['class'] | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
CheckpointRecordinterface 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'
}LossReadinginterface LossReading {
step: number
trainLoss: number
valLoss?: number
isProjected?: boolean
label?: string
}<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: < 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 (< 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