Synthetic Data Generator
blockaiGretel and Tonic-style LLM and mathematical synthetic dataset generator with differential privacy epsilon controls, custom schema field builders, K-anonymity policy toggles, live tabular CSV preview, and streaming JSON Lines export.
Installation
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/synthetic-data-generator.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/synthetic-data-generator.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/synthetic-data-generator.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/synthetic-data-generator.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/synthetic-data-generatorInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
initialTemplate | string | 'customer-transactions' | optional |
initialRowCount | number | 1000 | optional |
initialEpsilon | number | 1.2 | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SchemaFieldinterface SchemaField {
id: string
name: string
label: string
type: FieldDataType
description?: string
params: {
mean?: number
stdDev?: number
currency?: string
unit?: string
categories?: string[]
weights?: number[]
domain?: string
dateStart?: string
dateEnd?: string
}
}TemplatePresetinterface TemplatePreset {
id: string
name: string
description: string
category: string
fields: SchemaField[]
}GeneratedRecordinterface GeneratedRecord {
id: string
[key: string]: string | number
}FidelityMetricsinterface FidelityMetrics {
similarity: number
leakageRisk: string
correlationRetention: number
wassersteinDistance: number
laplaceNoiseStd: string
privacyTier: string
privacyBadgeColor: string
}Files installed (4)
app/components/blocks/SyntheticDataGenerator.vue31.1 kB
<script setup lang="ts"> import { computed, ref, watch, type HTMLAttributes } from 'vue' import { Check, Code2, Cpu, Database, Download, FileSpreadsheet, Info, Layers, Lock, Plus, RefreshCw, ShieldCheck, Sliders, Sparkles, Trash2, } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Slider } from '@/components/ui/slider' import { Switch } from '@/components/ui/switch' import type { FieldDataType, FidelityMetrics, GeneratedRecord, SchemaField, SyntheticDataGeneratorProps, TemplatePreset, } from './synthetic-data-types' import { boxMullerRandom as boxMullerRandomHelper, CARD_BRANDS, FIRST_NAMES, LAST_NAMES, TEMPLATES, } from './synthetic-data-templates' import SyntheticDataPreview from './SyntheticDataPreview.vue' export type { FieldDataType, FidelityMetrics, GeneratedRecord, SchemaField, SyntheticDataGeneratorProps, TemplatePreset, } interface Props { class?: HTMLAttributes['class'] initialTemplate?: string initialRowCount?: number initialEpsilon?: number } const props = withDefaults(defineProps<Props>(), { initialTemplate: 'customer-transactions', initialRowCount: 1000, initialEpsilon: 1.2, }) // --- STATE --- const selectedTemplateKey = ref<string>(props.initialTemplate) const outputFormat = ref<'csv' | 'jsonl'>('csv') const rowCount = ref<number>(props.initialRowCount) const epsilon = ref<number>(props.initialEpsilon) const kAnonymity = ref<boolean>(true) const saltedHash = ref<boolean>(true) const noiseMechanism = ref<boolean>(true) const activeFields = ref<SchemaField[]>([]) const isGenerating = ref<boolean>(false) const copied = ref<boolean>(false) const downloadToast = ref<string | null>(null) const previewTab = ref<'table' | 'json' | 'metrics'>('table') const generationSeed = ref<number>(48291) // Field addition form state const showAddField = ref<boolean>(false) const newFieldName = ref<string>('') const newFieldType = ref<FieldDataType>('gaussian_float') // Load template fields function loadTemplate(key: string) { const tpl = TEMPLATES[key] if (tpl) { activeFields.value = JSON.parse(JSON.stringify(tpl.fields)) triggerRegeneration() } } // Watch template changes watch(selectedTemplateKey, (newKey) => { loadTemplate(newKey) }) // Initialize fields loadTemplate(selectedTemplateKey.value) function boxMullerRandom(mean: number, stdDev: number, seedOffset: number): number { return boxMullerRandomHelper(mean, stdDev, seedOffset, generationSeed.value, noiseMechanism.value, epsilon.value) } const previewRecords = computed<GeneratedRecord[]>(() => { const rows: GeneratedRecord[] = [] const count = 6 // Show first 6 realistic rows in preview for (let i = 0; i < count; i++) { const fn = FIRST_NAMES[(i * 3 + generationSeed.value) % FIRST_NAMES.length] const ln = LAST_NAMES[(i * 5 + generationSeed.value + 2) % LAST_NAMES.length] const fullName = `${fn} ${ln}` const baseSlug = `${fn.charAt(0).toLowerCase()}.${ln.toLowerCase().replace(/[^a-z]/g, '')}` const record: GeneratedRecord = { id: saltedHash.value ? `syn_${((generationSeed.value * 9301 + i * 49297) % 233280).toString(16).padStart(6, '0')}` : `usr_${1000 + i}`, } for (const field of activeFields.value) { if (field.name.includes('id') || field.name.includes('uuid') || field.name.includes('token')) { record[field.name] = record.id continue } switch (field.type) { case 'name': record[field.name] = fullName break case 'email': { const dom = field.params.domain || 'synthetic-vault.io' record[field.name] = `${baseSlug}@${dom}` break } case 'credit_card': { const last4 = (((generationSeed.value * 7 + i * 1337 + 1000) % 9000) + 1000).toString() const brand = CARD_BRANDS[i % CARD_BRANDS.length] record[field.name] = `${brand} •••• ${last4}` break } case 'gaussian_float': { const m = field.params.mean ?? 100 const s = field.params.stdDev ?? 20 const rawVal = boxMullerRandom(m, s, i) const val = Math.max(m * 0.1, rawVal) if (field.params.currency) { record[field.name] = `${field.params.currency}${val.toFixed(2)}` } else if (field.params.unit) { record[field.name] = `${val.toFixed(1)} ${field.params.unit}` } else { record[field.name] = parseFloat(val.toFixed(2)) } break } case 'category_weights': { const cats = field.params.categories || ['Standard', 'Premium'] const catIndex = (i + Math.floor(generationSeed.value / 100)) % cats.length record[field.name] = cats[catIndex] break } case 'date_range': { const day = ((10 + i * 4) % 28) + 1 const month = (i % 8) + 1 const dayStr = day.toString().padStart(2, '0') const monthStr = month.toString().padStart(2, '0') const hour = (8 + i * 3) % 24 const hourStr = hour.toString().padStart(2, '0') record[field.name] = `2025-${monthStr}-${dayStr} ${hourStr}:24:18 UTC` break } default: record[field.name] = `value_${i}` } } rows.push(record) } return rows }) // --- STATISTICAL FIDELITY & DP METRICS --- const fidelityMetrics = computed(() => { const eps = epsilon.value // Higher epsilon -> higher similarity, lower privacy const rawSimilarity = Math.min(99.4, Math.max(82.0, 99.2 - ((5.0 - eps) / 4.9) * 9.6)) const similarity = parseFloat(rawSimilarity.toFixed(1)) // PII Leakage Risk is 0% when kAnonymity & Salted hash are enabled and eps <= 3.5 const leakageRisk = kAnonymity.value && saltedHash.value && eps <= 3.5 ? 0.0 : Math.max(0, (eps - 3.5) * 0.04) const correlationRetention = parseFloat(Math.min(99.1, 98.6 - ((5.0 - eps) / 4.9) * 6.2).toFixed(1)) const wassersteinDistance = parseFloat((0.032 + ((5.0 - eps) / 4.9) * 0.024).toFixed(3)) const laplaceNoiseStd = (0.12 / Math.max(0.1, eps)).toFixed(3) let privacyTier = 'Balanced Privacy & Utility' let privacyBadgeColor = 'bg-info/10 text-info border-info/30' if (eps <= 0.5) { privacyTier = 'Maximum Privacy / High DP Noise' privacyBadgeColor = 'bg-success/10 text-success border-success/30' } else if (eps > 2.5) { privacyTier = 'High Statistical Utility / Minimal Noise' privacyBadgeColor = 'bg-warning/10 text-warning border-warning/30' } return { similarity, leakageRisk: leakageRisk.toFixed(2), correlationRetention, wassersteinDistance, laplaceNoiseStd, privacyTier, privacyBadgeColor, } }) // Estimated file size calculation const estimatedFileSize = computed(() => { const bytesPerRow = outputFormat.value === 'csv' ? 180 : 380 const totalKb = (rowCount.value * bytesPerRow) / 1024 if (totalKb < 1024) { return `~${totalKb.toFixed(1)} KB` } return `~${(totalKb / 1024).toFixed(2)} MB` }) // --- ACTIONS --- function triggerRegeneration() { isGenerating.value = true generationSeed.value = Math.floor(Math.random() * 90000) + 10000 setTimeout(() => { isGenerating.value = false }, 450) } function handleAddCustomField() { if (!newFieldName.value.trim()) return const cleanName = newFieldName.value.trim().toLowerCase().replace(/\s+/g, '_') const newField: SchemaField = { id: `f-custom-${Date.now()}`, name: cleanName, label: newFieldName.value.trim(), type: newFieldType.value, description: `Custom ${newFieldType.value} schema column`, params: newFieldType.value === 'gaussian_float' ? { mean: 500, stdDev: 120, currency: '$' } : newFieldType.value === 'category_weights' ? { categories: ['Alpha', 'Beta', 'Gamma'], weights: [0.5, 0.3, 0.2] } : {}, } activeFields.value.push(newField) newFieldName.value = '' showAddField.value = false triggerRegeneration() } function removeField(fieldId: string) { if (activeFields.value.length <= 2) return // Keep at least 2 fields activeFields.value = activeFields.value.filter((f) => f.id !== fieldId) triggerRegeneration() } async function copyJsonLines() { try { const jsonLinesText = previewRecords.value.map((r) => JSON.stringify(r)).join('\n') if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(jsonLinesText) } copied.value = true setTimeout(() => { copied.value = false }, 2000) } catch (err) { console.error('Failed to copy to clipboard', err) } } function handleDownloadDataset() { const filename = `synthetic_${selectedTemplateKey.value}_${rowCount.value}_rows.${outputFormat.value === 'csv' ? 'csv' : 'jsonl'}` downloadToast.value = `Downloaded ${rowCount.value.toLocaleString()} synthetic records (${filename})` setTimeout(() => { downloadToast.value = null }, 3500) } // Watch row count / epsilon to retrigger subtle noise updates watch([epsilon, kAnonymity, saltedHash, noiseMechanism], () => { // Update without full spinning state }) </script> <template> <div data-slot="synthetic-data-generator" :class="cn('mx-auto w-full max-w-7xl space-y-6', props.class)"> <!-- TOP HEADER HERO --> <Card class="border-border overflow-hidden shadow-xs"> <CardHeader class="border-border/60 bg-muted/20 border-b pb-4"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <Cpu class="size-4.5" /> </div> <CardTitle class="text-lg font-bold tracking-tight md:text-xl"> Synthetic Dataset Generator & Privacy Engine </CardTitle> <Badge variant="outline" class="gap-1 text-xs font-normal"> <span class="bg-success size-1.5 animate-pulse rounded-full" /> Differential Privacy Enabled </Badge> </div> <CardDescription class="text-xs md:text-sm"> Mathematical & LLM-guided synthetic tabular dataset synthesizer with configurable (ε, δ)-DP noise injection. </CardDescription> </div> <!-- Header Right Quick Actions --> <div class="flex flex-wrap items-center gap-2.5"> <!-- Format Selector Segmented Control --> <div class="bg-muted/60 border-border inline-flex items-center rounded-lg border p-0.5 text-xs"> <button type="button" :class=" cn( 'flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1.5 font-medium transition-colors', outputFormat === 'csv' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) " @click="outputFormat = 'csv'" > <FileSpreadsheet class="size-3.5" /> <span>Tabular CSV</span> </button> <button type="button" :class=" cn( 'flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1.5 font-medium transition-colors', outputFormat === 'jsonl' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) " @click="outputFormat = 'jsonl'" > <Code2 class="size-3.5" /> <span>JSON Lines</span> </button> </div> <!-- Download Button --> <Button aria-label="Download attachment" variant="outline" size="sm" class="h-9 gap-1.5 text-xs font-medium" @click="handleDownloadDataset" > <Download class="size-3.5" /> <span>Download Dataset</span> </Button> <!-- Primary Generate Batch Button --> <Button variant="default" size="sm" :class=" cn( 'h-9 gap-1.5 px-4 text-xs font-semibold shadow-xs transition-[color,background-color,border-color,box-shadow,opacity,transform,scale,translate,rotate] active:scale-95', ) " :disabled="isGenerating" @click="triggerRegeneration" > <RefreshCw v-if="isGenerating" class="size-3.5 animate-spin" /> <Sparkles v-else class="size-3.5" /> <span>{{ isGenerating ? 'Synthesizing...' : 'Generate Synthetic Batch' }}</span> </Button> </div> </div> <!-- Template Selector Bar --> <div class="border-border/50 bg-background/50 mt-3 flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3.5 py-2" > <div class="flex items-center gap-2"> <Layers class="text-muted-foreground size-4" /> <span class="text-foreground text-xs font-semibold">Schema Template:</span> </div> <div class="w-full min-w-0 sm:max-w-md sm:flex-1"> <Select v-model="selectedTemplateKey"> <SelectTrigger class="bg-background h-8.5 w-full text-xs font-medium [&>span]:truncate [&>svg]:shrink-0"> <SelectValue placeholder="Select Schema Template" /> </SelectTrigger> <SelectContent> <SelectItem v-for="(tpl, key) in TEMPLATES" :key="key" :value="key" class="text-xs"> {{ tpl.name }} </SelectItem> </SelectContent> </Select> </div> <div class="text-muted-foreground hidden items-center gap-2 text-xs sm:flex"> <Badge variant="secondary" class="px-2 py-0.5 text-xs font-normal"> {{ TEMPLATES[selectedTemplateKey]?.category }} </Badge> <span>•</span> <span class="font-mono tabular-nums">{{ activeFields.length }} fields configured</span> </div> </div> </CardHeader> </Card> <!-- DOWNLOAD SUCCESS NOTIFICATION --> <div v-if="downloadToast" class="border-success/30 bg-success/10 text-success text-success flex items-center justify-between rounded-lg border px-4 py-2.5 text-xs font-medium shadow-xs" > <div class="flex items-center gap-2"> <Check class="size-4 stroke-[2.5]" /> <span>{{ downloadToast }}</span> </div> <Badge variant="outline" class="border-success/40 font-mono text-xs"> {{ estimatedFileSize }} </Badge> </div> <!-- 2-COLUMN STUDIO GENERATOR --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- LEFT COLUMN: SCHEMA & PRIVACY CONFIGURATION (40% / 5 Cols) --> <div class="space-y-6 lg:col-span-5"> <!-- CARD 1: ROW COUNT & BATCH SCALE --> <Card class="border-border shadow-xs"> <CardHeader class="border-border/50 border-b pb-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <Database class="size-3.5" /> </div> <CardTitle class="text-sm font-semibold">Dataset Generation Volume</CardTitle> </div> <Badge variant="secondary" class="font-mono text-xs font-semibold tabular-nums"> {{ rowCount.toLocaleString() }} rows </Badge> </div> </CardHeader> <CardContent class="space-y-4 pt-4"> <!-- Row Slider --> <div class="space-y-2"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>100 rows</span> <span class="text-foreground font-mono font-medium">Estimated: {{ estimatedFileSize }}</span> <span>50,000 rows</span> </div> <Slider :model-value="rowCount" :min="100" :max="50000" :step="100" class="w-full py-1" @update:model-value="(val) => (rowCount = Array.isArray(val) ? val[0] : val)" /> </div> <!-- Quick Preset Chips --> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground mr-1 text-xs">Presets:</span> <button v-for="preset in [100, 1000, 5000, 10000, 50000]" :key="preset" type="button" :class=" cn( 'cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors', rowCount === preset ? 'bg-primary text-primary-foreground border-primary shadow-xs' : 'bg-muted/40 text-muted-foreground border-border hover:bg-muted hover:text-foreground', ) " @click="rowCount = preset" > {{ preset >= 1000 ? `${preset / 1000}k` : preset }} </button> </div> </CardContent> </Card> <!-- CARD 2: DIFFERENTIAL PRIVACY EPSILON (ε) CONTROLS --> <Card class="border-border shadow-xs"> <CardHeader class="border-border/50 border-b pb-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <ShieldCheck class="size-3.5" /> </div> <div> <CardTitle class="text-sm font-semibold">Differential Privacy Budget</CardTitle> <CardDescription class="text-xs"> Laplace (ε, δ)-DP noise calibration </CardDescription> </div> </div> <Badge variant="outline" class="font-mono text-xs font-bold tabular-nums"> ε = {{ epsilon.toFixed(1) }} </Badge> </div> </CardHeader> <CardContent class="space-y-4 pt-4"> <!-- Epsilon Slider --> <div class="space-y-2"> <div class="flex items-center justify-between text-xs"> <span class="text-success font-medium">ε=0.1 (High Privacy)</span> <span class="text-warning font-medium">ε=5.0 (High Fidelity)</span> </div> <Slider :model-value="epsilon" :min="0.1" :max="5.0" :step="0.1" class="w-full py-1" @update:model-value="(val) => (epsilon = Array.isArray(val) ? val[0] : val)" /> </div> <!-- Epsilon Interpretation Banner --> <div :class="cn('flex items-start gap-2.5 rounded-lg border p-3 text-xs', fidelityMetrics.privacyBadgeColor)" > <Info class="mt-0.5 size-4 shrink-0" /> <div class="space-y-1"> <div class="font-semibold">{{ fidelityMetrics.privacyTier }}</div> <p class="text-muted-foreground text-xs leading-relaxed"> Laplace noise scale: <span class="text-foreground font-mono font-medium" >σ=±{{ fidelityMetrics.laplaceNoiseStd }}</span > • Delta: <span class="text-foreground font-mono font-medium">δ=10⁻⁵</span> • Zero memorization guarantee. </p> </div> </div> <!-- Quick Epsilon Presets --> <div class="grid grid-cols-3 gap-2 pt-1"> <button type="button" :class=" cn( 'cursor-pointer rounded-lg border p-2 text-center text-xs transition-colors', epsilon === 0.5 ? 'border-primary bg-primary/10 text-primary font-semibold' : 'border-border bg-background text-muted-foreground hover:bg-muted/40', ) " @click="epsilon = 0.5" > <div class="font-bold">ε = 0.5</div> <div class="text-muted-foreground text-xs">Healthcare</div> </button> <button type="button" :class=" cn( 'cursor-pointer rounded-lg border p-2 text-center text-xs transition-colors', epsilon === 1.2 ? 'border-primary bg-primary/10 text-primary font-semibold' : 'border-border bg-background text-muted-foreground hover:bg-muted/40', ) " @click="epsilon = 1.2" > <div class="font-bold">ε = 1.2</div> <div class="text-muted-foreground text-xs">Standard</div> </button> <button type="button" :class=" cn( 'cursor-pointer rounded-lg border p-2 text-center text-xs transition-colors', epsilon === 3.0 ? 'border-primary bg-primary/10 text-primary font-semibold' : 'border-border bg-background text-muted-foreground hover:bg-muted/40', ) " @click="epsilon = 3.0" > <div class="font-bold">ε = 3.0</div> <div class="text-muted-foreground text-xs">Analytics</div> </button> </div> </CardContent> </Card> <!-- CARD 3: FIELD SCHEMAS GENERATOR --> <Card class="border-border shadow-xs"> <CardHeader class="border-border/50 border-b pb-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <Sliders class="size-3.5" /> </div> <div> <CardTitle class="text-sm font-semibold">Schema Fields Generator</CardTitle> <CardDescription class="text-xs"> {{ activeFields.length }} active synthesizer distributions </CardDescription> </div> </div> <Button variant="outline" size="sm" class="h-7.5 gap-1 px-2.5 text-xs" @click="showAddField = !showAddField" > <Plus class="size-3.5" /> <span>{{ showAddField ? 'Cancel' : 'Add Field' }}</span> </Button> </div> </CardHeader> <CardContent class="space-y-3 pt-4"> <!-- Add Field Drawer / Form --> <div v-if="showAddField" class="border-border/80 bg-muted/30 space-y-3 rounded-lg border p-3.5 text-xs"> <div class="text-foreground font-semibold">Configure New Synthetic Field</div> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"> <div class="space-y-1"> <label class="text-muted-foreground text-xs">Field Identifier</label> <Input v-model="newFieldName" placeholder="e.g. account_balance" class="h-8 text-xs" /> </div> <div class="space-y-1"> <label class="text-muted-foreground text-xs">Data Distribution Type</label> <Select v-model="newFieldType"> <SelectTrigger class="bg-background h-8 w-full min-w-0 text-xs"> <SelectValue placeholder="Type" /> </SelectTrigger> <SelectContent> <SelectItem value="gaussian_float" class="text-xs">Float Gaussian Distribution</SelectItem> <SelectItem value="name" class="text-xs">Full Name Faker</SelectItem> <SelectItem value="email" class="text-xs">Email Faker</SelectItem> <SelectItem value="credit_card" class="text-xs">Credit Card Faker</SelectItem> <SelectItem value="category_weights" class="text-xs">Category Weights</SelectItem> <SelectItem value="date_range" class="text-xs">Date Range</SelectItem> </SelectContent> </Select> </div> </div> <div class="flex justify-end gap-2 pt-1"> <Button variant="ghost" size="sm" class="h-7.5 text-xs" @click="showAddField = false"> Cancel </Button> <Button variant="default" size="sm" class="h-7.5 text-xs" :disabled="!newFieldName.trim()" @click="handleAddCustomField" > Add to Schema </Button> </div> </div> <!-- Fields List --> <div class="divide-border/60 max-h-[320px] space-y-2 overflow-y-auto pr-1"> <div v-for="field in activeFields" :key="field.id" class="group border-border/70 bg-card hover:bg-muted/20 flex items-center justify-between rounded-lg border p-2.5 transition-colors" > <div class="min-w-0 space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground font-mono text-xs font-semibold">{{ field.name }}</span> <Badge variant="outline" :class=" cn( 'px-1.5 py-0 text-xs font-semibold tracking-wider uppercase', field.type === 'gaussian_float' && 'border-info/20 bg-info/10 text-info', field.type === 'name' && 'border-chart-1/20 bg-chart-1/10 text-chart-1', field.type === 'email' && 'border-chart-2/20 bg-chart-2/10 text-chart-2', field.type === 'credit_card' && 'border-warning/20 bg-warning/10 text-warning', field.type === 'category_weights' && 'border-success/20 bg-success/10 text-success', field.type === 'date_range' && 'border-destructive/20 bg-destructive/10 text-destructive', ) " > {{ field.type.replace('_', ' ') }} </Badge> </div> <p class="text-muted-foreground truncate text-xs"> {{ field.description || 'Configured synthesizer parameter' }} </p> </div> <Button aria-label="Delete field" variant="ghost" size="sm" class="text-muted-foreground hover:text-destructive h-7 w-7 p-0 opacity-80 group-hover:opacity-100" :disabled="activeFields.length <= 2" @click="removeField(field.id)" > <Trash2 class="size-3.5" /> </Button> </div> </div> </CardContent> </Card> <!-- CARD 4: PII ANONYMIZATION POLICY SWITCHES --> <Card class="border-border shadow-xs"> <CardHeader class="border-border/50 border-b pb-3"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <Lock class="size-3.5" /> </div> <div> <CardTitle class="text-sm font-semibold">PII Anonymization Policy</CardTitle> <CardDescription class="text-xs"> Zero linkage & re-identification guarantees </CardDescription> </div> </div> </CardHeader> <CardContent class="space-y-4 pt-4"> <!-- K-Anonymity Switch --> <div class="flex items-start justify-between gap-3"> <div class="space-y-0.5"> <div class="text-foreground text-xs font-semibold">K-Anonymity (k = 5 Cohort Enforcement)</div> <p class="text-muted-foreground text-xs leading-relaxed"> Generalizes quasi-identifiers so each demographic bucket contains ≥ 5 records. </p> </div> <Switch :model-value="kAnonymity" @update:model-value="(v) => (kAnonymity = v)" /> </div> <Separator /> <!-- Salted Hash Switch --> <div class="flex items-start justify-between gap-3"> <div class="space-y-0.5"> <div class="text-foreground text-xs font-semibold">Salted HMAC-SHA256 for Primary Keys</div> <p class="text-muted-foreground text-xs leading-relaxed"> Deterministically pseudonymizes primary keys with ephemeral cryptographic salt. </p> </div> <Switch :model-value="saltedHash" @update:model-value="(v) => (saltedHash = v)" /> </div> <Separator /> <!-- Noise Injection Switch --> <div class="flex items-start justify-between gap-3"> <div class="space-y-0.5"> <div class="text-foreground text-xs font-semibold">Differential Privacy Gaussian Noise</div> <p class="text-muted-foreground text-xs leading-relaxed"> Injects calibrated noise calibrated to global sensitivity (Δf / ε). </p> </div> <Switch :model-value="noiseMechanism" @update:model-value="(v) => (noiseMechanism = v)" /> </div> </CardContent> </Card> </div> <!-- RIGHT COLUMN: LIVE GENERATED SYNTHETIC DATA PREVIEW (60% / 7 Cols) --> <div class="space-y-6 lg:col-span-7"> <SyntheticDataPreview v-model:preview-tab="previewTab" :preview-records="previewRecords" :active-fields="activeFields" :fidelity-metrics="fidelityMetrics" :epsilon="epsilon" :row-count="rowCount" :output-format="outputFormat" :generation-seed="generationSeed" :copied="copied" @copy-json-lines="copyJsonLines" /> </div> </div> </div> </template>app/components/blocks/SyntheticDataPreview.vue13.8 kB
<script setup lang="ts"> import { BarChart3, Binary, Check, Code2, Copy, Shield, ShieldCheck, Table as TableIcon, Zap } from 'lucide-vue-next' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import type { FidelityMetrics, GeneratedRecord, SchemaField } from './synthetic-data-types' interface Props { previewTab: 'table' | 'json' | 'metrics' previewRecords: GeneratedRecord[] activeFields: SchemaField[] fidelityMetrics: FidelityMetrics epsilon: number rowCount: number outputFormat: 'csv' | 'jsonl' generationSeed: number copied: boolean } const props = defineProps<Props>() const emit = defineEmits<{ 'update:previewTab': [val: 'table' | 'json' | 'metrics'] copyJsonLines: [] }>() </script> <template> <div class="space-y-6"> <!-- TOP GAUGE BANNER: STATISTICAL FIDELITY VS PRIVACY --> <Card class="border-border overflow-hidden shadow-xs"> <div class="border-border/60 bg-success/10 dark:bg-success/10 border-b px-4 py-3"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-center gap-2"> <ShieldCheck class="text-success size-4.5" /> <span class="text-foreground font-mono text-sm font-bold tracking-tight"> {{ fidelityMetrics.similarity }}% Distribution Similarity • {{ fidelityMetrics.leakageRisk }}% PII Leakage Risk </span> </div> <Badge variant="outline" class="bg-background border-success/40 text-success text-xs font-semibold"> ε-DP Bound: ≤ {{ epsilon.toFixed(1) }} </Badge> </div> </div> <CardContent class="grid grid-cols-1 gap-4 pt-4 sm:grid-cols-3"> <!-- Metric 1: Fidelity Score --> <div class="border-border/60 bg-muted/10 space-y-1.5 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>Fidelity Score</span> <Zap class="text-warning size-3.5" /> </div> <div class="text-foreground font-mono text-lg font-bold tabular-nums">{{ fidelityMetrics.similarity }}%</div> <Progress :model-value="fidelityMetrics.similarity" class="h-1.5" /> </div> <!-- Metric 2: Correlation Preservation --> <div class="border-border/60 bg-muted/10 space-y-1.5 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>Covariance Retention</span> <BarChart3 class="text-info size-3.5" /> </div> <div class="text-foreground font-mono text-lg font-bold tabular-nums"> {{ fidelityMetrics.correlationRetention }}% </div> <Progress :model-value="fidelityMetrics.correlationRetention" class="h-1.5" /> </div> <!-- Metric 3: Wasserstein Distance --> <div class="border-border/60 bg-muted/10 space-y-1.5 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>Wasserstein Distance</span> <Binary class="text-chart-1 size-3.5" /> </div> <div class="text-foreground font-mono text-lg font-bold tabular-nums"> {{ fidelityMetrics.wassersteinDistance }} </div> <div class="text-muted-foreground text-xs">Low error bound (< 0.05)</div> </div> </CardContent> </Card> <!-- PREVIEW TABS CARD --> <Card class="border-border shadow-xs"> <CardHeader class="border-border/50 border-b pb-3"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <TableIcon class="size-3.5" /> </div> <div> <CardTitle class="text-sm font-semibold">Live Generated Synthetic Data Preview</CardTitle> <CardDescription class="text-xs"> Real-time sampler showing first {{ previewRecords.length }} synthesized records </CardDescription> </div> </div> <!-- View Switcher --> <Tabs :model-value="previewTab" class="w-auto" @update:model-value="(val) => emit('update:previewTab', val as 'table' | 'json' | 'metrics')" > <TabsList class="h-8"> <TabsTrigger value="table" class="h-7 px-2.5 text-xs"> <TableIcon class="mr-1 size-3" /> <span>Table</span> </TabsTrigger> <TabsTrigger value="json" class="h-7 px-2.5 text-xs"> <Code2 class="mr-1 size-3" /> <span>JSONL</span> </TabsTrigger> <TabsTrigger value="metrics" class="h-7 px-2.5 text-xs"> <BarChart3 class="mr-1 size-3" /> <span>Marginals</span> </TabsTrigger> </TabsList> </Tabs> </div> </CardHeader> <CardContent class="pt-4"> <!-- TAB 1: TABULAR PREVIEW --> <div v-if="previewTab === 'table'" class="space-y-3"> <div class="border-border overflow-hidden rounded-lg border"> <div class="overflow-x-auto"> <Table class="min-w-full text-xs"> <TableHeader class="bg-muted/40"> <TableRow> <TableHead v-for="f in activeFields" :key="f.id" class="text-foreground font-mono text-xs font-semibold whitespace-nowrap" > {{ f.name }} </TableHead> </TableRow> </TableHeader> <TableBody> <TableRow v-for="(row, idx) in previewRecords" :key="idx" class="hover:bg-muted/30 transition-colors"> <TableCell v-for="f in activeFields" :key="f.id" class="py-2.5 whitespace-nowrap"> <!-- Special cell styling based on data type --> <template v-if=" f.name === 'id' || f.name.includes('id') || f.name.includes('uuid') || f.name.includes('token') " > <span class="text-muted-foreground font-mono font-medium">{{ row[f.name] }}</span> </template> <template v-else-if="f.type === 'category_weights'"> <Badge variant="secondary" class="font-mono text-xs font-medium"> {{ row[f.name] }} </Badge> </template> <template v-else-if="f.type === 'gaussian_float'"> <span class="text-foreground font-mono font-semibold tabular-nums"> {{ row[f.name] }} </span> </template> <template v-else-if="f.type === 'credit_card'"> <span class="text-foreground font-mono text-xs font-medium"> {{ row[f.name] }} </span> </template> <template v-else-if="f.type === 'email'"> <span class="text-primary font-mono text-xs font-normal"> {{ row[f.name] }} </span> </template> <template v-else> <span class="text-foreground font-medium">{{ row[f.name] }}</span> </template> </TableCell> </TableRow> </TableBody> </Table> </div> </div> <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-2 px-1 text-xs"> <span class="flex items-center gap-1.5 font-mono tabular-nums"> <span class="bg-success size-1.5 rounded-full" /> Showing {{ previewRecords.length }} sample records of {{ rowCount.toLocaleString() }} generated rows </span> <span class="text-muted-foreground font-mono text-xs">Seed: #{{ generationSeed }}</span> </div> </div> <!-- TAB 2: JSON LINES PREVIEW --> <div v-else-if="previewTab === 'json'" class="space-y-3"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Streaming JSON Lines formatted output:</span> <Button variant="outline" size="sm" class="h-7.5 gap-1.5 px-2.5 text-xs font-medium" @click="emit('copyJsonLines')" > <Check v-if="copied" class="text-success size-3 stroke-[2.5]" /> <Copy v-else class="size-3" /> <span>{{ copied ? 'Copied JSONL' : 'Copy JSON Lines' }}</span> </Button> </div> <div class="border-border/80 bg-muted/40 max-h-[380px] overflow-x-auto overflow-y-auto rounded-lg border p-4 font-mono text-xs leading-relaxed" > <div v-for="(rec, idx) in previewRecords" :key="idx" class="hover:bg-muted/60 rounded px-1.5 py-1 whitespace-pre transition-colors" > <span class="text-muted-foreground select-none">{{ idx + 1 }} </span> <span class="text-foreground">{ </span> <template v-for="(val, key, kIdx) in rec" :key="key"> <span class="text-info font-semibold">"{{ key }}"</span> <span class="text-muted-foreground">: </span> <span :class="typeof val === 'number' ? 'text-warning font-medium' : 'text-success'"> {{ typeof val === 'number' ? val : `"${val}"` }} </span> <span v-if="kIdx < Object.keys(rec).length - 1" class="text-muted-foreground">, </span> </template> <span class="text-foreground"> }</span> </div> </div> </div> <!-- TAB 3: MARGINALS & FIDELITY DENSITY --> <div v-else class="space-y-4"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <!-- Density Gauge 1 --> <div class="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-3.5"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-semibold">Gaussian Distribution Overlap</span> <Badge variant="outline" class="font-mono text-xs">96.2%</Badge> </div> <p class="text-muted-foreground text-xs"> Kolmogorov-Smirnov distance between synthetic batch and continuous Gaussian ground truth. </p> <Progress :model-value="96.2" class="h-2" /> </div> <!-- Density Gauge 2 --> <div class="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-3.5"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-semibold">Categorical Frequency Prior</span> <Badge variant="outline" class="font-mono text-xs">98.4%</Badge> </div> <p class="text-muted-foreground text-xs"> Total variation distance across categorical weight assignments. </p> <Progress :model-value="98.4" class="h-2" /> </div> </div> <!-- Mathematical Guarantees Breakdown --> <div class="border-border/60 bg-muted/10 space-y-2.5 rounded-lg border p-4 text-xs"> <div class="text-foreground flex items-center gap-1.5 font-semibold"> <Shield class="text-primary size-4" /> <span>Mathematical Privacy Guarantees • Formal Specs</span> </div> <div class="text-muted-foreground grid grid-cols-1 gap-2 sm:grid-cols-2"> <div class="flex items-center gap-1.5"> <Check class="text-success size-3.5" /> <span>Differential Privacy (ε = {{ epsilon.toFixed(1) }}, δ = 10⁻⁵)</span> </div> <div class="flex items-center gap-1.5"> <Check class="text-success size-3.5" /> <span>K-Anonymity (k = 5) Cohort Clamping</span> </div> <div class="flex items-center gap-1.5"> <Check class="text-success size-3.5" /> <span>Zero Direct Row Memorization</span> </div> <div class="flex items-center gap-1.5"> <Check class="text-success size-3.5" /> <span>Ephemeral In-Memory Compute</span> </div> </div> </div> </div> </CardContent> <CardFooter class="border-border/40 text-muted-foreground flex flex-wrap items-center justify-between border-t py-3 text-xs" > <span class="flex items-center gap-1.5"> <Zap class="text-warning size-3.5" /> Powered by LLM Semantic Synthesis & Laplace Noise Engine </span> <span class="font-mono tabular-nums"> Output: {{ outputFormat.toUpperCase() }} • {{ activeFields.length }} fields • {{ rowCount.toLocaleString() }} records </span> </CardFooter> </Card> </div> </template>app/components/blocks/synthetic-data-types.ts1.1 kB
import type { HTMLAttributes } from 'vue' export type FieldDataType = 'name' | 'email' | 'credit_card' | 'gaussian_float' | 'date_range' | 'category_weights' export interface SchemaField { id: string name: string label: string type: FieldDataType description?: string params: { mean?: number stdDev?: number currency?: string unit?: string categories?: string[] weights?: number[] domain?: string dateStart?: string dateEnd?: string } } export interface TemplatePreset { id: string name: string description: string category: string fields: SchemaField[] } export interface GeneratedRecord { id: string [key: string]: string | number } export interface FidelityMetrics { similarity: number leakageRisk: string correlationRetention: number wassersteinDistance: number laplaceNoiseStd: string privacyTier: string privacyBadgeColor: string } export interface SyntheticDataGeneratorProps { class?: HTMLAttributes['class'] initialTemplate?: string initialRowCount?: number initialEpsilon?: number }app/components/blocks/synthetic-data-templates.ts8.9 kB
import type { TemplatePreset } from './synthetic-data-types' export const TEMPLATES: Record<string, TemplatePreset> = { 'customer-transactions': { id: 'customer-transactions', name: 'Customer Transactions & Demographics Dataset', description: 'High-dimensional e-commerce customer cohort, synthetic PII, spending distribution, and country codes.', category: 'E-Commerce & Retail', fields: [ { id: 'f-id', name: 'cust_id', label: 'Customer ID', type: 'name', description: 'Salted pseudonymized primary key', params: {}, }, { id: 'f-name', name: 'full_name', label: 'Full Name', type: 'name', description: 'Synthetic person name generator', params: {}, }, { id: 'f-email', name: 'email_address', label: 'Synthetic Email', type: 'email', description: 'Faker email with domain isolation', params: { domain: 'syn-corp.io' }, }, { id: 'f-card', name: 'masked_card', label: 'Card PAN', type: 'credit_card', description: 'Luhn-compliant masked payment token', params: {}, }, { id: 'f-spend', name: 'spending_amount', label: 'Spending Amount', type: 'gaussian_float', description: 'Gaussian spending curve (μ=$342.50, σ=$85.20)', params: { mean: 342.5, stdDev: 85.2, currency: '$' }, }, { id: 'f-country', name: 'country_code', label: 'Country Region', type: 'category_weights', description: 'Categorical distribution [US, DE, GB, FR, JP]', params: { categories: ['US', 'DE', 'GB', 'FR', 'JP'], weights: [0.45, 0.2, 0.15, 0.1, 0.1], }, }, { id: 'f-created', name: 'created_at', label: 'Registration Date', type: 'date_range', description: 'ISO-8601 timestamp range (2024-2025)', params: { dateStart: '2024-01-01', dateEnd: '2025-08-20' }, }, ], }, 'healthcare-ehr': { id: 'healthcare-ehr', name: 'Healthcare Patient Clinical Records (EHR)', description: 'HIPAA-compliant patient cohort with biomarker telemetry, blood pressure, and triage severity tiers.', category: 'Healthcare & Life Sciences', fields: [ { id: 'f-h-id', name: 'patient_id', label: 'Patient ID', type: 'name', description: 'Deterministic HIPAA salted identifier', params: {}, }, { id: 'f-h-name', name: 'patient_name', label: 'Patient Name', type: 'name', description: 'Synthetic medical record moniker', params: {}, }, { id: 'f-h-email', name: 'contact_email', label: 'Contact Email', type: 'email', description: 'Synthetic provider contact', params: { domain: 'med-synthetic.org' }, }, { id: 'f-h-glucose', name: 'blood_glucose', label: 'Glucose (mg/dL)', type: 'gaussian_float', description: 'Fast fasting blood glucose (μ=108.4, σ=22.1)', params: { mean: 108.4, stdDev: 22.1, unit: 'mg/dL' }, }, { id: 'f-h-bp', name: 'systolic_bp', label: 'Systolic BP', type: 'gaussian_float', description: 'Arterial blood pressure (μ=124.0, σ=15.5)', params: { mean: 124.0, stdDev: 15.5, unit: 'mmHg' }, }, { id: 'f-h-tier', name: 'triage_tier', label: 'Triage Severity', type: 'category_weights', description: 'Clinical priority classification', params: { categories: ['Routine', 'Elevated', 'Urgent', 'Critical'], weights: [0.52, 0.28, 0.14, 0.06], }, }, { id: 'f-h-date', name: 'admission_date', label: 'Admission Date', type: 'date_range', description: 'Encounter timestamps', params: { dateStart: '2024-06-01', dateEnd: '2025-08-20' }, }, ], }, 'fintech-fraud': { id: 'fintech-fraud', name: 'Fintech Fraud Detection & Risk Scoring', description: 'Transaction monitoring telemetry for anti-fraud classifiers with skewed anomalous patterns.', category: 'Fintech & Risk', fields: [ { id: 'f-ff-id', name: 'tx_token', label: 'Transaction ID', type: 'name', description: 'Cryptographic ledger token', params: {}, }, { id: 'f-ff-name', name: 'merchant_name', label: 'Merchant Name', type: 'name', description: 'Synthetic counterparty vendor', params: {}, }, { id: 'f-ff-email', name: 'auth_email', label: 'Auth Email', type: 'email', description: 'Tokenized transaction origin email', params: { domain: 'syn-pay.net' }, }, { id: 'f-ff-card', name: 'card_pan', label: 'Card PAN', type: 'credit_card', description: 'Tokenized 16-digit card surrogate', params: {}, }, { id: 'f-ff-amt', name: 'settlement_usd', label: 'Settlement ($)', type: 'gaussian_float', description: 'Transaction gross volume (μ=$1420.00, σ=$480.00)', params: { mean: 1420.0, stdDev: 480.0, currency: '$' }, }, { id: 'f-ff-verdict', name: 'risk_verdict', label: 'Risk Verdict', type: 'category_weights', description: 'Ground truth classifier target', params: { categories: ['Legitimate', 'Suspicious', 'Flagged_Fraud'], weights: [0.89, 0.08, 0.03], }, }, { id: 'f-ff-ts', name: 'settled_at', label: 'Settlement Time', type: 'date_range', description: 'High-precision event timestamp', params: { dateStart: '2025-01-01', dateEnd: '2025-08-20' }, }, ], }, 'saas-telemetry': { id: 'saas-telemetry', name: 'SaaS Product Telemetry & Usage Metrics', description: 'B2B subscription telemetry with active seat distribution, recurring revenue, and subscription tiers.', category: 'Cloud Software & B2B', fields: [ { id: 'f-st-id', name: 'org_uuid', label: 'Organization UUID', type: 'name', description: 'Tenant identifier', params: {}, }, { id: 'f-st-name', name: 'tenant_name', label: 'Tenant Name', type: 'name', description: 'Synthetic enterprise tenant moniker', params: {}, }, { id: 'f-st-email', name: 'admin_email', label: 'Admin Email', type: 'email', description: 'Primary owner email', params: { domain: 'syn-saas.io' }, }, { id: 'f-st-seats', name: 'active_seats', label: 'Active Seats', type: 'gaussian_float', description: 'Provisioned enterprise seats (μ=48.0, σ=18.0)', params: { mean: 48.0, stdDev: 18.0, unit: 'seats' }, }, { id: 'f-st-mrr', name: 'mrr_usd', label: 'Monthly Revenue', type: 'gaussian_float', description: 'MRR ledger (μ=$2850.00, σ=$950.00)', params: { mean: 2850.0, stdDev: 950.0, currency: '$' }, }, { id: 'f-st-plan', name: 'plan_tier', label: 'Plan Tier', type: 'category_weights', description: 'Subscription entitlement level', params: { categories: ['Starter', 'Growth', 'Enterprise'], weights: [0.48, 0.36, 0.16], }, }, { id: 'f-st-date', name: 'provisioned_at', label: 'Provisioned Date', type: 'date_range', description: 'Cohort subscription activation', params: { dateStart: '2024-01-01', dateEnd: '2025-08-20' }, }, ], }, } export const FIRST_NAMES = [ 'Elena', 'Malik', 'Siddharth', 'Claire', 'Liam', 'Amara', 'Kenji', 'Sofia', 'Devon', 'Maya', 'Tobias', 'Fatima', 'Mateo', 'Astrid', 'Zara', ] export const LAST_NAMES = [ 'Rostova', 'Al-Mansoor', 'Patel', 'Chen', "O'Connor", 'Okafor', 'Sato', 'Alvarez', 'Vance', 'Lin', 'Richter', 'Zahra', 'Silva', 'Lindqvist', 'Novak', ] export const CARD_BRANDS = ['Visa', 'Mastercard', 'Amex'] export function boxMullerRandom( mean: number, stdDev: number, seedOffset: number, seed: number, noiseMechanism: boolean, epsilon: number, ): number { const u1 = Math.max(1e-6, Math.abs(Math.sin(seed + seedOffset * 17.3))) const u2 = Math.max(1e-6, Math.abs(Math.cos(seed + seedOffset * 31.7))) const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2) const dpNoiseScale = noiseMechanism ? stdDev / (Math.max(0.1, epsilon) * 12) : 0 const dpNoise = Math.sin(seed * 3.7 + seedOffset * 5.1) * dpNoiseScale return mean + z0 * stdDev + dpNoise }
Raw manifest:https://uipkge.dev/r/vue/synthetic-data-generator.json