import { useEffect, useState } from 'react' import { anonymizeText, deidentifyText, detectText, redactText, type DeidStrategy, type PseudonymMapping, type TextPIISpan, } from '../lib/api' import { l1Rgb, l1Tint } from '../lib/colors' import Legend from './Legend' import MappingTable from './MappingTable' type Action = 'detect' | 'redact' | 'deidentify' | 'anonymize' const ACTION_BUTTON_LABELS: Record = { detect: 'Detect PII', redact: 'Redact', deidentify: 'De-identify', anonymize: 'Anonymize', } const ACTION_HINTS: Record = { detect: 'Finds PII spans and highlights them in place — nothing is changed.', redact: 'Replaces each span with a [category] token. One-way, nothing kept.', deidentify: 'Replaces each entity with a consistent pseudonym (Person_1) and returns the mapping for authorized re-linking.', anonymize: 'One-way: generalizes ages/dates/geography, collapses names and IDs to unnumbered tokens. No mapping exists.', } const EXAMPLE_TEXT = `DISCHARGE SUMMARY Patient: Mr. John Doe (Male, 45 yrs), DOB 12-03-1979, Blood group O+. Address: 14 MG Road, Indiranagar, Bangalore, Karnataka 560038. Aadhaar: 1234 5678 9012 | PAN: ABCDE1234F | ABHA: 14-1234-5678-9012. MRN/UHID: UH00219834. Insurance policy: TPA-IND-99213. Contact: +91 98765 43210, email john.doe@example.com. Treating physician: Dr. Asha Menon (NMC Reg. 2011/04/1123). Payment received to UPI johndoe@okhdfcbank, A/C 50100123456789.` function renderHighlighted(text: string, spans: TextPIISpan[]) { const sorted = [...spans].sort((a, b) => a.start - b.start) const parts: React.ReactNode[] = [] let prev = 0 for (const [i, sp] of sorted.entries()) { if (sp.start < prev) continue parts.push(text.slice(prev, sp.start)) parts.push( {text.slice(sp.start, sp.end)} {sp.category} , ) prev = sp.end } parts.push(text.slice(prev)) return parts } export default function TextTab({ entities, ready }: { entities: string[] | null; ready: boolean }) { const [text, setText] = useState(EXAMPLE_TEXT) const [action, setAction] = useState('detect') const [strategy, setStrategy] = useState('counter') const [selected, setSelected] = useState>(new Set()) const [spans, setSpans] = useState([]) const [outputText, setOutputText] = useState(null) const [outputTitle, setOutputTitle] = useState('') const [mapping, setMapping] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [ranOnce, setRanOnce] = useState(false) // All categories selected by default, once the taxonomy arrives. useEffect(() => { if (entities) setSelected(new Set(entities)) }, [entities]) function toggle(value: string) { const next = new Set(selected) if (next.has(value)) next.delete(value) else next.add(value) setSelected(next) } const allSelected = entities != null && selected.size === entities.length // Omit the param entirely when everything is selected (= server default). const categories = allSelected ? undefined : [...selected] async function run() { if (selected.size === 0) return setLoading(true) setError(null) setMapping(null) setOutputText(null) try { if (action === 'detect') { setSpans(await detectText(text, categories)) } else { setSpans([]) if (action === 'redact') { setOutputText(await redactText(text, categories)) setOutputTitle('Redacted') } else if (action === 'deidentify') { const result = await deidentifyText(text, categories, strategy) setOutputText(result.text) setMapping(result.mapping) setOutputTitle('De-identified') } else { setOutputText(await anonymizeText(text, categories)) setOutputTitle('Anonymized') } } setRanOnce(true) } catch (err) { setError(err instanceof Error ? err.message : String(err)) } finally { setLoading(false) } } const groups = [...new Set(spans.map((s) => s.l1))].sort() return (

Detects PII in raw text and highlights each span, color-coded by group.

Input

{entities && (
Categories to detect ({selected.size}/{entities.length} selected)
{entities.map((c) => ( ))}
)}