UIPackage
Menu

Framework

Change language

Boilerplate repo

Synthetic Data Generator

blockai

Gretel 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.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/synthetic-data-generator.json
Named registry:npx shadcn-vue@latest add @uipkge/synthetic-data-generatorInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
classHTMLAttributes['class']optional
initialTemplatestring'customer-transactions'optional
initialRowCountnumber1000optional
initialEpsilonnumber1.2optional

Schema

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

SchemaField
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
  }
}
TemplatePreset
interface TemplatePreset {
  id: string
  name: string
  description: string
  category: string
  fields: SchemaField[]
}
GeneratedRecord
interface GeneratedRecord {
  id: string
  [key: string]: string | number
}
FidelityMetrics
interface 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 &amp; 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 &amp; 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>&bull;</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; = {{ 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">&epsilon;=0.1 (High Privacy)</span>
                    <span class="text-warning font-medium">&epsilon;=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"
                        >&sigma;=&plusmn;{{ fidelityMetrics.laplaceNoiseStd }}</span
                      >
                      &bull; Delta: <span class="text-foreground font-mono font-medium">&delta;=10⁻⁵</span> &bull; 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">&epsilon; = 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">&epsilon; = 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">&epsilon; = 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 &amp; 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
  • app/components/blocks/synthetic-data-types.ts1.1 kB
  • app/components/blocks/synthetic-data-templates.ts8.9 kB

Raw manifest:https://uipkge.dev/r/vue/synthetic-data-generator.json