import { useState } from "react"; import { copy, type Language } from "../copy"; import type { TranscriptTurn } from "../types"; type TranscriptProps = { language?: Language; turns: TranscriptTurn[]; onFeedback?: (turnId: string, reason: string, comment: string) => Promise; onRepeat?: (turn: TranscriptTurn) => void; onType?: (turn: TranscriptTurn) => void; }; export function Transcript({ language = "vi", turns, onFeedback, onRepeat, onType }: TranscriptProps) { const text = copy[language].workspace; const [openFeedback, setOpenFeedback] = useState(null); const [reason, setReason] = useState("wrong_term"); const [comment, setComment] = useState(""); const [savedTurn, setSavedTurn] = useState(null); async function submit(turnId: string) { if (!onFeedback) { return; } await onFeedback(turnId, reason, comment); setSavedTurn(turnId); setOpenFeedback(null); setComment(""); } return (

{text.transcript}

{turns.length} {text.turns}
{turns.length === 0 ?

{text.noTurns}

: null} {turns.map((turn) => { const blocked = turn.requires_confirmation || turn.status === "awaiting_confirm" || turn.status === "blocked"; return (

{turn.seq}. {turn.speaker === "doctor" ? text.doctor : text.patient} · {turn.src_lang === "vi" ? text.vietnamese : text.english} → {turn.tgt_lang === "vi" ? text.vietnamese : text.english}

{blocked ? text.patientSafeMask : highlightText(turn.source_text, turn, language)}

{riskText(text, turn.risk_tier)} · {statusText(text, turn.status)} {turn.low_confidence ? ` · ${text.lowConfidence}` : ""} {turn.requires_confirmation ? ` · ${text.confirmationRequiredLabel}` : ""}

{blocked ? text.blocked : highlightText(turn.corrected_text || turn.translation, turn, language)}

{turn.low_confidence ? (

{text.lowConfidence}

{onRepeat ? : null} {onType ? : null}
) : null} {!blocked && turn.risk_spans.length ? (
    {turn.risk_spans.map((span, index) => (
  • {riskText(text, span.severity)}: {riskKindText(text, span.kind)}
  • ))}
) : null} {onFeedback ? (
{savedTurn === turn.id ? {text.feedbackSaved} : null} {openFeedback === turn.id ? (
{ event.preventDefault(); void submit(turn.id); }} >
) : null}
) : null}
); })}
); } function fold(value: string): string { return value .normalize("NFD") .replace(/\p{Diacritic}/gu, "") .toLocaleLowerCase(); } function highlightText(text: string, turn: TranscriptTurn, language: Language) { const span = turn.risk_spans.find((candidate) => fold(text).includes(fold(candidate.term))); if (!span) { return text; } const foldedText = fold(text); const foldedTerm = fold(span.term); const start = foldedText.indexOf(foldedTerm); const end = start + span.term.length; return ( <> {text.slice(0, start)} {riskText(copy[language].workspace, span.severity)}: {text.slice(start, end)} {text.slice(end)} ); } function riskText(text: (typeof copy)[Language]["workspace"], value: string) { return text.risk[value as keyof typeof text.risk] ?? text.risk.other; } function statusText(text: (typeof copy)[Language]["workspace"], value: string) { return text.status[value as keyof typeof text.status] ?? text.status.other; } function riskKindText(text: (typeof copy)[Language]["workspace"], value: string) { return text.riskKind[value as keyof typeof text.riskKind] ?? text.riskKind.other; }