diff --git a/frontend/src/components/VttEditor/VttDiffView.tsx b/frontend/src/components/VttEditor/VttDiffView.tsx new file mode 100644 index 0000000..7a1f46a --- /dev/null +++ b/frontend/src/components/VttEditor/VttDiffView.tsx @@ -0,0 +1,104 @@ +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { apiClient } from '../../lib/api'; +import type { VttKind } from '../../types/api'; + +interface VttDiffViewProps { + jobId: string; + lang: string; + kind: VttKind; +} + +export function VttDiffView({ jobId, lang, kind }: VttDiffViewProps) { + const [open, setOpen] = useState(false); + + const { data: versions } = useQuery({ + queryKey: ['vtt-versions', jobId, lang, kind], + queryFn: () => apiClient.listVttVersions(jobId, lang, kind, 0, 200), + enabled: open, + staleTime: 30_000, + }); + + const versionList = versions?.versions ?? []; + const baseline = versionList.length > 0 ? versionList[versionList.length - 1] : null; // oldest = version 1 + const latest = versionList.length > 0 ? versionList[0] : null; // newest first + + const canShowDiff = baseline && latest && baseline.version !== latest.version; + + const { data: diffData, isLoading: diffLoading } = useQuery({ + queryKey: ['vtt-diff', jobId, lang, kind, baseline?.version, latest?.version], + queryFn: () => apiClient.diffVttVersions(jobId, lang, kind, baseline!.version, latest!.version), + enabled: open && !!canShowDiff, + staleTime: 30_000, + }); + + if (!open) { + return ( + + ); + } + + return ( +
+
+
+ Diff vs AI baseline + {diffData && ( + + +{diffData.added_count} + {' / '} + -{diffData.removed_count} + {` lines changed from v${baseline?.version} → v${latest?.version}`} + + )} +
+ +
+ +
+ {diffLoading && ( +
Loading diff…
+ )} + + {!diffLoading && !canShowDiff && versionList.length > 0 && ( +
No changes from baseline — content is identical to AI output.
+ )} + + {!diffLoading && versionList.length === 0 && ( +
No version history yet.
+ )} + + {diffData?.lines.map((line, i) => { + if (line.type === 'unchanged') return null; + return ( +
+ + {line.type === 'added' ? '+' : '-'} + + {line.content} +
+ ); + })} + + {diffData && diffData.added_count === 0 && diffData.removed_count === 0 && ( +
No changes from AI baseline.
+ )} +
+
+ ); +} diff --git a/frontend/src/routes/admin/QCDetail.tsx b/frontend/src/routes/admin/QCDetail.tsx index b6d62fb..8756b01 100644 --- a/frontend/src/routes/admin/QCDetail.tsx +++ b/frontend/src/routes/admin/QCDetail.tsx @@ -11,6 +11,7 @@ import { } from '../../hooks/useAccessibleVideoEdit'; import { StatusBadge } from '../../components/StatusBadge'; import { VttEditor } from '../../components/VttEditor/VttEditor'; +import { VttDiffView } from '../../components/VttEditor/VttDiffView'; import { VideoWithCaptions } from '../../components/VideoWithCaptions'; import { VoiceSelector } from '../../components/VoiceSelector'; import { TimelinePreview } from '../../components/TimelinePreview'; @@ -1584,6 +1585,9 @@ export function QCDetail() { glossaryTerms={glossaryTerms} language={selectedLanguage} /> +
+ +
)} @@ -1611,6 +1615,9 @@ export function QCDetail() { glossaryTerms={glossaryTerms} language={selectedLanguage} /> +
+ +
)}