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 Vue ->

Installation

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

Variants

Loading interactive previews…

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)

  • components/blocks/SyntheticDataGenerator.tsx32.4 kB
    'use client'
    
    import * as React from 'react'
    import {
      Check,
      Code2,
      Cpu,
      Database,
      Download,
      FileSpreadsheet,
      Info,
      Layers,
      Lock,
      Plus,
      RefreshCw,
      ShieldCheck,
      Sliders,
      Sparkles,
      Trash2,
    } from 'lucide-react'
    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'
    
    export type {
      FieldDataType,
      FidelityMetrics,
      GeneratedRecord,
      SchemaField,
      SyntheticDataGeneratorProps,
      TemplatePreset,
    }
    
    export function SyntheticDataGenerator({
      className,
      initialTemplate = 'customer-transactions',
      initialRowCount = 1000,
      initialEpsilon = 1.2,
    }: SyntheticDataGeneratorProps) {
      // --- STATE ---
      const [selectedTemplateKey, setSelectedTemplateKey] = React.useState<string>(initialTemplate)
      const [outputFormat, setOutputFormat] = React.useState<'csv' | 'jsonl'>('csv')
      const [rowCount, setRowCount] = React.useState<number>(initialRowCount)
      const [epsilon, setEpsilon] = React.useState<number>(initialEpsilon)
      const [kAnonymity, setKAnonymity] = React.useState<boolean>(true)
      const [saltedHash, setSaltedHash] = React.useState<boolean>(true)
      const [noiseMechanism, setNoiseMechanism] = React.useState<boolean>(true)
    
      const [activeFields, setActiveFields] = React.useState<SchemaField[]>(() => {
        return TEMPLATES[initialTemplate]?.fields ? JSON.parse(JSON.stringify(TEMPLATES[initialTemplate].fields)) : []
      })
    
      const [isGenerating, setIsGenerating] = React.useState<boolean>(false)
      const [copied, setCopied] = React.useState<boolean>(false)
      const [downloadToast, setDownloadToast] = React.useState<string | null>(null)
      const [previewTab, setPreviewTab] = React.useState<'table' | 'json' | 'metrics'>('table')
      const [generationSeed, setGenerationSeed] = React.useState<number>(48291)
    
      // Field addition state
      const [showAddField, setShowAddField] = React.useState<boolean>(false)
      const [newFieldName, setNewFieldName] = React.useState<string>('')
      const [newFieldType, setNewFieldType] = React.useState<FieldDataType>('gaussian_float')
    
      // Handle template selection switch
      const handleTemplateChange = (key: string) => {
        setSelectedTemplateKey(key)
        const tpl = TEMPLATES[key]
        if (tpl) {
          setActiveFields(JSON.parse(JSON.stringify(tpl.fields)))
          triggerRegeneration()
        }
      }
    
      // Generation trigger
      const triggerRegeneration = () => {
        setIsGenerating(true)
        setGenerationSeed(Math.floor(Math.random() * 90000) + 10000)
        setTimeout(() => {
          setIsGenerating(false)
        }, 450)
      }
    
      // Box-Muller Gaussian random generator with differential privacy noise
      const boxMullerRandom = (mean: number, stdDev: number, seedOffset: number): number => {
        return boxMullerRandomHelper(mean, stdDev, seedOffset, generationSeed, noiseMechanism, epsilon)
      }
    
      // Dynamic preview records computation
      const previewRecords = React.useMemo<GeneratedRecord[]>(() => {
        const rows: GeneratedRecord[] = []
        const count = 6
    
        for (let i = 0; i < count; i++) {
          const fn = FIRST_NAMES[(i * 3 + generationSeed) % FIRST_NAMES.length]
          const ln = LAST_NAMES[(i * 5 + generationSeed + 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
              ? `syn_${((generationSeed * 9301 + i * 49297) % 233280).toString(16).padStart(6, '0')}`
              : `usr_${1000 + i}`,
          }
    
          for (const field of activeFields) {
            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 * 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 / 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
      }, [activeFields, generationSeed, saltedHash, noiseMechanism, epsilon])
    
      // Statistical fidelity metrics
      const fidelityMetrics = React.useMemo(() => {
        const rawSimilarity = Math.min(99.4, Math.max(82.0, 99.2 - ((5.0 - epsilon) / 4.9) * 9.6))
        const similarity = parseFloat(rawSimilarity.toFixed(1))
        const leakageRisk = kAnonymity && saltedHash && epsilon <= 3.5 ? 0.0 : Math.max(0, (epsilon - 3.5) * 0.04)
        const correlationRetention = parseFloat(Math.min(99.1, 98.6 - ((5.0 - epsilon) / 4.9) * 6.2).toFixed(1))
        const wassersteinDistance = parseFloat((0.032 + ((5.0 - epsilon) / 4.9) * 0.024).toFixed(3))
        const laplaceNoiseStd = (0.12 / Math.max(0.1, epsilon)).toFixed(3)
    
        let privacyTier = 'Balanced Privacy & Utility'
        let privacyBadgeColor = 'bg-info/10 text-info border-info/30'
    
        if (epsilon <= 0.5) {
          privacyTier = 'Maximum Privacy / High DP Noise'
          privacyBadgeColor = 'bg-success/10 text-success border-success/30'
        } else if (epsilon > 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,
        }
      }, [epsilon, kAnonymity, saltedHash])
    
      // Estimated file size calculation
      const estimatedFileSize = React.useMemo(() => {
        const bytesPerRow = outputFormat === 'csv' ? 180 : 380
        const totalKb = (rowCount * bytesPerRow) / 1024
        if (totalKb < 1024) {
          return `~${totalKb.toFixed(1)} KB`
        }
        return `~${(totalKb / 1024).toFixed(2)} MB`
      }, [outputFormat, rowCount])
    
      // Add field handler
      const handleAddCustomField = () => {
        if (!newFieldName.trim()) return
    
        const cleanName = newFieldName.trim().toLowerCase().replace(/\s+/g, '_')
        const newField: SchemaField = {
          id: `f-custom-${Date.now()}`,
          name: cleanName,
          label: newFieldName.trim(),
          type: newFieldType,
          description: `Custom ${newFieldType} schema column`,
          params:
            newFieldType === 'gaussian_float'
              ? { mean: 500, stdDev: 120, currency: '$' }
              : newFieldType === 'category_weights'
                ? { categories: ['Alpha', 'Beta', 'Gamma'], weights: [0.5, 0.3, 0.2] }
                : {},
        }
    
        setActiveFields((prev) => [...prev, newField])
        setNewFieldName('')
        setShowAddField(false)
        triggerRegeneration()
      }
    
      // Remove field handler
      const removeField = (fieldId: string) => {
        if (activeFields.length <= 2) return
        setActiveFields((prev) => prev.filter((f) => f.id !== fieldId))
        triggerRegeneration()
      }
    
      // Copy JSON lines
      const copyJsonLines = async () => {
        try {
          const jsonLinesText = previewRecords.map((r) => JSON.stringify(r)).join('\n')
          if (navigator?.clipboard?.writeText) {
            await navigator.clipboard.writeText(jsonLinesText)
          }
          setCopied(true)
          setTimeout(() => {
            setCopied(false)
          }, 2000)
        } catch (err) {
          console.error('Failed to copy to clipboard', err)
        }
      }
    
      // Download simulation
      const handleDownloadDataset = () => {
        const filename = `synthetic_${selectedTemplateKey}_${rowCount}_rows.${outputFormat === 'csv' ? 'csv' : 'jsonl'}`
        setDownloadToast(`Downloaded ${rowCount.toLocaleString()} synthetic records (${filename})`)
        setTimeout(() => {
          setDownloadToast(null)
        }, 3500)
      }
    
      return (
        <div data-slot="synthetic-data-generator" className={cn('mx-auto w-full max-w-7xl space-y-6', className)}>
          {/* TOP HEADER HERO */}
          <Card className="border-border overflow-hidden shadow-xs">
            <CardHeader className="border-border/60 bg-muted/20 border-b pb-4">
              <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
                <div className="space-y-1">
                  <div className="flex flex-wrap items-center gap-2">
                    <div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg">
                      <Cpu className="size-4.5" />
                    </div>
                    <CardTitle className="text-lg font-bold tracking-tight md:text-xl">
                      Synthetic Dataset Generator &amp; Privacy Engine
                    </CardTitle>
                    <Badge variant="outline" className="gap-1 text-xs font-normal">
                      <span className="bg-success size-1.5 animate-pulse rounded-full" />
                      Differential Privacy Enabled
                    </Badge>
                  </div>
                  <CardDescription className="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 className="flex flex-wrap items-center gap-2.5">
                  {/* Format Selector Segmented Control */}
                  <div className="bg-muted/60 border-border inline-flex items-center rounded-lg border p-0.5 text-xs">
                    <button
                      type="button"
                      className={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',
                      )}
                      onClick={() => setOutputFormat('csv')}
                    >
                      <FileSpreadsheet className="size-3.5" />
                      <span>Tabular CSV</span>
                    </button>
                    <button
                      type="button"
                      className={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',
                      )}
                      onClick={() => setOutputFormat('jsonl')}
                    >
                      <Code2 className="size-3.5" />
                      <span>JSON Lines</span>
                    </button>
                  </div>
    
                  {/* Download Button */}
                  <Button
                    aria-label="Download attachment"
                    variant="outline"
                    size="sm"
                    className="h-9 gap-1.5 text-xs font-medium"
                    onClick={handleDownloadDataset}
                  >
                    <Download className="size-3.5" />
                    <span>Download Dataset</span>
                  </Button>
    
                  {/* Primary Generate Batch Button */}
                  <Button
                    variant="default"
                    size="sm"
                    className="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}
                    onClick={triggerRegeneration}
                  >
                    {isGenerating ? <RefreshCw className="size-3.5 animate-spin" /> : <Sparkles className="size-3.5" />}
                    <span>{isGenerating ? 'Synthesizing...' : 'Generate Synthetic Batch'}</span>
                  </Button>
                </div>
              </div>
    
              {/* Template Selector Bar */}
              <div className="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 className="flex items-center gap-2">
                  <Layers className="text-muted-foreground size-4" />
                  <span className="text-foreground text-xs font-semibold">Schema Template:</span>
                </div>
    
                <div className="w-full min-w-0 sm:max-w-md sm:flex-1">
                  <Select value={selectedTemplateKey} onValueChange={handleTemplateChange}>
                    <SelectTrigger className="bg-background h-8.5 w-full text-xs font-medium [&>span]:truncate [&>svg]:shrink-0">
                      <SelectValue placeholder="Select Schema Template" />
                    </SelectTrigger>
                    <SelectContent>
                      {Object.entries(TEMPLATES).map(([key, tpl]) => (
                        <SelectItem key={key} value={key} className="text-xs">
                          {tpl.name}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                </div>
    
                <div className="text-muted-foreground hidden items-center gap-2 text-xs sm:flex">
                  <Badge variant="secondary" className="px-2 py-0.5 text-xs font-normal">
                    {TEMPLATES[selectedTemplateKey]?.category}
                  </Badge>
                  <span></span>
                  <span className="font-mono tabular-nums">{activeFields.length} fields configured</span>
                </div>
              </div>
            </CardHeader>
          </Card>
    
          {/* DOWNLOAD SUCCESS NOTIFICATION */}
          {downloadToast && (
            <div className="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 className="flex items-center gap-2">
                <Check className="size-4 stroke-[2.5]" />
                <span>{downloadToast}</span>
              </div>
              <Badge variant="outline" className="border-success/40 font-mono text-xs">
                {estimatedFileSize}
              </Badge>
            </div>
          )}
    
          {/* 2-COLUMN STUDIO GENERATOR */}
          <div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
            {/* LEFT COLUMN: SCHEMA & PRIVACY CONFIGURATION (40% / 5 Cols) */}
            <div className="space-y-6 lg:col-span-5">
              {/* CARD 1: ROW COUNT & BATCH SCALE */}
              <Card className="border-border shadow-xs">
                <CardHeader className="border-border/50 border-b pb-3">
                  <div className="flex items-center justify-between">
                    <div className="flex items-center gap-2">
                      <div className="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md">
                        <Database className="size-3.5" />
                      </div>
                      <CardTitle className="text-sm font-semibold">Dataset Generation Volume</CardTitle>
                    </div>
                    <Badge variant="secondary" className="font-mono text-xs font-semibold tabular-nums">
                      {rowCount.toLocaleString()} rows
                    </Badge>
                  </div>
                </CardHeader>
    
                <CardContent className="space-y-4 pt-4">
                  {/* Row Slider */}
                  <div className="space-y-2">
                    <div className="text-muted-foreground flex items-center justify-between text-xs">
                      <span>100 rows</span>
                      <span className="text-foreground font-mono font-medium">Estimated: {estimatedFileSize}</span>
                      <span>50,000 rows</span>
                    </div>
    
                    <Slider
                      value={[rowCount]}
                      min={100}
                      max={50000}
                      step={100}
                      className="w-full py-1"
                      onValueChange={(val) => setRowCount(val[0])}
                    />
                  </div>
    
                  {/* Quick Preset Chips */}
                  <div className="flex flex-wrap items-center gap-1.5 pt-1">
                    <span className="text-muted-foreground mr-1 text-xs">Presets:</span>
                    {[100, 1000, 5000, 10000, 50000].map((preset) => (
                      <button
                        key={preset}
                        type="button"
                        className={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',
                        )}
                        onClick={() => setRowCount(preset)}
                      >
                        {preset >= 1000 ? `${preset / 1000}k` : preset}
                      </button>
                    ))}
                  </div>
                </CardContent>
              </Card>
    
              {/* CARD 2: DIFFERENTIAL PRIVACY EPSILON (ε) CONTROLS */}
              <Card className="border-border shadow-xs">
                <CardHeader className="border-border/50 border-b pb-3">
                  <div className="flex items-center justify-between">
                    <div className="flex items-center gap-2">
                      <div className="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                        <ShieldCheck className="size-3.5" />
                      </div>
                      <div>
                        <CardTitle className="text-sm font-semibold">Differential Privacy Budget</CardTitle>
                        <CardDescription className="text-xs">Laplace (ε, δ)-DP noise calibration</CardDescription>
                      </div>
                    </div>
                    <Badge variant="outline" className="font-mono text-xs font-bold tabular-nums">
                      ε = {epsilon.toFixed(1)}
                    </Badge>
                  </div>
                </CardHeader>
    
                <CardContent className="space-y-4 pt-4">
                  {/* Epsilon Slider */}
                  <div className="space-y-2">
                    <div className="flex items-center justify-between text-xs">
                      <span className="text-success font-medium">ε=0.1 (High Privacy)</span>
                      <span className="text-warning font-medium">ε=5.0 (High Fidelity)</span>
                    </div>
    
                    <Slider
                      value={[epsilon]}
                      min={0.1}
                      max={5.0}
                      step={0.1}
                      className="w-full py-1"
                      onValueChange={(val) => setEpsilon(val[0])}
                    />
                  </div>
    
                  {/* Epsilon Interpretation Banner */}
                  <div
                    className={cn(
                      'flex items-start gap-2.5 rounded-lg border p-3 text-xs',
                      fidelityMetrics.privacyBadgeColor,
                    )}
                  >
                    <Info className="mt-0.5 size-4 shrink-0" />
                    <div className="space-y-1">
                      <div className="font-semibold">{fidelityMetrics.privacyTier}</div>
                      <p className="text-muted-foreground text-xs leading-relaxed">
                        Laplace noise scale:{' '}
                        <span className="text-foreground font-mono font-medium">σ=±{fidelityMetrics.laplaceNoiseStd}</span>{' '}
                        • Delta: <span className="text-foreground font-mono font-medium">δ=10⁻⁵</span> • Zero memorization
                        guarantee.
                      </p>
                    </div>
                  </div>
    
                  {/* Quick Epsilon Presets */}
                  <div className="grid grid-cols-3 gap-2 pt-1">
                    {[
                      { eps: 0.5, label: 'Healthcare' },
                      { eps: 1.2, label: 'Standard' },
                      { eps: 3.0, label: 'Analytics' },
                    ].map(({ eps, label }) => (
                      <button
                        key={eps}
                        type="button"
                        className={cn(
                          'cursor-pointer rounded-lg border p-2 text-center text-xs transition-colors',
                          epsilon === eps
                            ? 'border-primary bg-primary/10 text-primary font-semibold'
                            : 'border-border bg-background text-muted-foreground hover:bg-muted/40',
                        )}
                        onClick={() => setEpsilon(eps)}
                      >
                        <div className="font-bold">ε = {eps.toFixed(1)}</div>
                        <div className="text-muted-foreground text-xs">{label}</div>
                      </button>
                    ))}
                  </div>
                </CardContent>
              </Card>
    
              {/* CARD 3: FIELD SCHEMAS GENERATOR */}
              <Card className="border-border shadow-xs">
                <CardHeader className="border-border/50 border-b pb-3">
                  <div className="flex items-center justify-between">
                    <div className="flex items-center gap-2">
                      <div className="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md">
                        <Sliders className="size-3.5" />
                      </div>
                      <div>
                        <CardTitle className="text-sm font-semibold">Schema Fields Generator</CardTitle>
                        <CardDescription className="text-xs">
                          {activeFields.length} active synthesizer distributions
                        </CardDescription>
                      </div>
                    </div>
                    <Button
                      variant="outline"
                      size="sm"
                      className="h-7.5 gap-1 px-2.5 text-xs"
                      onClick={() => setShowAddField(!showAddField)}
                    >
                      <Plus className="size-3.5" />
                      <span>{showAddField ? 'Cancel' : 'Add Field'}</span>
                    </Button>
                  </div>
                </CardHeader>
    
                <CardContent className="space-y-3 pt-4">
                  {/* Add Field Form */}
                  {showAddField && (
                    <div className="border-border/80 bg-muted/30 space-y-3 rounded-lg border p-3.5 text-xs">
                      <div className="text-foreground font-semibold">Configure New Synthetic Field</div>
                      <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
                        <div className="space-y-1">
                          <label className="text-muted-foreground text-xs">Field Identifier</label>
                          <Input
                            value={newFieldName}
                            onChange={(e) => setNewFieldName(e.target.value)}
                            placeholder="e.g. account_balance"
                            className="h-8 text-xs"
                          />
                        </div>
                        <div className="space-y-1">
                          <label className="text-muted-foreground text-xs">Data Distribution Type</label>
                          <Select value={newFieldType} onValueChange={(val) => setNewFieldType(val as FieldDataType)}>
                            <SelectTrigger className="bg-background h-8 w-full min-w-0 text-xs">
                              <SelectValue placeholder="Type" />
                            </SelectTrigger>
                            <SelectContent>
                              <SelectItem value="gaussian_float" className="text-xs">
                                Float Gaussian Distribution
                              </SelectItem>
                              <SelectItem value="name" className="text-xs">
                                Full Name Faker
                              </SelectItem>
                              <SelectItem value="email" className="text-xs">
                                Email Faker
                              </SelectItem>
                              <SelectItem value="credit_card" className="text-xs">
                                Credit Card Faker
                              </SelectItem>
                              <SelectItem value="category_weights" className="text-xs">
                                Category Weights
                              </SelectItem>
                              <SelectItem value="date_range" className="text-xs">
                                Date Range
                              </SelectItem>
                            </SelectContent>
                          </Select>
                        </div>
                      </div>
    
                      <div className="flex justify-end gap-2 pt-1">
                        <Button variant="ghost" size="sm" className="h-7.5 text-xs" onClick={() => setShowAddField(false)}>
                          Cancel
                        </Button>
                        <Button
                          variant="default"
                          size="sm"
                          className="h-7.5 text-xs"
                          disabled={!newFieldName.trim()}
                          onClick={handleAddCustomField}
                        >
                          Add to Schema
                        </Button>
                      </div>
                    </div>
                  )}
    
                  {/* Fields List */}
                  <div className="divide-border/60 max-h-[320px] space-y-2 overflow-y-auto pr-1">
                    {activeFields.map((field) => (
                      <div
                        key={field.id}
                        className="group border-border/70 bg-card hover:bg-muted/20 flex items-center justify-between rounded-lg border p-2.5 transition-colors"
                      >
                        <div className="min-w-0 space-y-0.5">
                          <div className="flex items-center gap-2">
                            <span className="text-foreground font-mono text-xs font-semibold">{field.name}</span>
                            <Badge
                              variant="outline"
                              className={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 className="text-muted-foreground truncate text-xs">
                            {field.description || 'Configured synthesizer parameter'}
                          </p>
                        </div>
    
                        <Button
                          variant="ghost"
                          size="sm"
                          className="text-muted-foreground hover:text-destructive h-7 w-7 p-0 opacity-80 group-hover:opacity-100"
                          aria-label="Delete field"
                          disabled={activeFields.length <= 2}
                          onClick={() => removeField(field.id)}
                        >
                          <Trash2 className="size-3.5" />
                        </Button>
                      </div>
                    ))}
                  </div>
                </CardContent>
              </Card>
    
              {/* CARD 4: PII ANONYMIZATION POLICY SWITCHES */}
              <Card className="border-border shadow-xs">
                <CardHeader className="border-border/50 border-b pb-3">
                  <div className="flex items-center gap-2">
                    <div className="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md">
                      <Lock className="size-3.5" />
                    </div>
                    <div>
                      <CardTitle className="text-sm font-semibold">PII Anonymization Policy</CardTitle>
                      <CardDescription className="text-xs">Zero linkage &amp; re-identification guarantees</CardDescription>
                    </div>
                  </div>
                </CardHeader>
    
                <CardContent className="space-y-4 pt-4">
                  {/* K-Anonymity Switch */}
                  <div className="flex items-start justify-between gap-3">
                    <div className="space-y-0.5">
                      <div className="text-foreground text-xs font-semibold">K-Anonymity (k = 5 Cohort Enforcement)</div>
                      <p className="text-muted-foreground text-xs leading-relaxed">
                        Generalizes quasi-identifiers so each demographic bucket contains ≥ 5 records.
                      </p>
                    </div>
                    <Switch checked={kAnonymity} onCheckedChange={setKAnonymity} />
                  </div>
    
                  <Separator />
    
                  {/* Salted Hash Switch */}
                  <div className="flex items-start justify-between gap-3">
                    <div className="space-y-0.5">
                      <div className="text-foreground text-xs font-semibold">Salted HMAC-SHA256 for Primary Keys</div>
                      <p className="text-muted-foreground text-xs leading-relaxed">
                        Deterministically pseudonymizes primary keys with ephemeral cryptographic salt.
                      </p>
                    </div>
                    <Switch checked={saltedHash} onCheckedChange={setSaltedHash} />
                  </div>
    
                  <Separator />
    
                  {/* Noise Injection Switch */}
                  <div className="flex items-start justify-between gap-3">
                    <div className="space-y-0.5">
                      <div className="text-foreground text-xs font-semibold">Differential Privacy Gaussian Noise</div>
                      <p className="text-muted-foreground text-xs leading-relaxed">
                        Injects calibrated noise calibrated to global sensitivity (Δf / ε).
                      </p>
                    </div>
                    <Switch checked={noiseMechanism} onCheckedChange={setNoiseMechanism} />
                  </div>
                </CardContent>
              </Card>
            </div>
    
            {/* RIGHT COLUMN: LIVE GENERATED SYNTHETIC DATA PREVIEW (60% / 7 Cols) */}
            <div className="space-y-6 lg:col-span-7">
              <SyntheticDataPreview
                previewTab={previewTab}
                onPreviewTabChange={setPreviewTab}
                previewRecords={previewRecords}
                activeFields={activeFields}
                fidelityMetrics={fidelityMetrics}
                epsilon={epsilon}
                rowCount={rowCount}
                outputFormat={outputFormat}
                generationSeed={generationSeed}
                copied={copied}
                onCopyJsonLines={copyJsonLines}
              />
            </div>
          </div>
        </div>
      )
    }
    
    export default SyntheticDataGenerator
    
  • components/blocks/SyntheticDataPreview.tsx14.9 kB
  • components/blocks/synthetic-data-types.ts1 kB
  • components/blocks/synthetic-data-templates.ts8.9 kB

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