feat(l7): diff AI baseline vs current VTT in QCDetail

VttDiffView component (frontend/src/components/VttEditor/VttDiffView.tsx):
- Lazy-loads VTT version list (newest-first) and diffs version 1 (AI baseline)
  against the latest version
- Renders unified diff: green lines = added, red lines = removed (unchanged hidden)
- Collapsed by default; expand with "↔ Diff vs AI baseline" button
- Shows +N/-N change summary in header

QCDetail integration:
- VttDiffView added below both Captions and Audio Description VttEditors
  (only appears for the selected language)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-04-29 19:03:25 +01:00
parent dc1cfd01dc
commit aba43a67d7
2 changed files with 111 additions and 0 deletions

View file

@ -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 (
<button
onClick={() => setOpen(true)}
className="text-xs px-2 py-1 text-indigo-600 border border-indigo-200 rounded hover:bg-indigo-50"
>
Diff vs AI baseline
</button>
);
}
return (
<div className="border border-indigo-200 rounded-lg overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 bg-indigo-50 border-b border-indigo-200">
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-indigo-900">Diff vs AI baseline</span>
{diffData && (
<span className="text-xs text-gray-500">
<span className="text-green-600">+{diffData.added_count}</span>
{' / '}
<span className="text-red-500">-{diffData.removed_count}</span>
{` lines changed from v${baseline?.version} → v${latest?.version}`}
</span>
)}
</div>
<button
onClick={() => setOpen(false)}
className="text-xs text-indigo-500 hover:text-indigo-700"
>
Hide
</button>
</div>
<div className="max-h-72 overflow-y-auto font-mono text-xs bg-white">
{diffLoading && (
<div className="p-4 text-gray-400 text-center">Loading diff</div>
)}
{!diffLoading && !canShowDiff && versionList.length > 0 && (
<div className="p-4 text-gray-400 text-center">No changes from baseline content is identical to AI output.</div>
)}
{!diffLoading && versionList.length === 0 && (
<div className="p-4 text-gray-400 text-center">No version history yet.</div>
)}
{diffData?.lines.map((line, i) => {
if (line.type === 'unchanged') return null;
return (
<div
key={i}
className={`flex px-2 py-0.5 ${
line.type === 'added' ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'
}`}
>
<span className="w-6 shrink-0 select-none text-gray-400">
{line.type === 'added' ? '+' : '-'}
</span>
<span className="whitespace-pre-wrap break-all">{line.content}</span>
</div>
);
})}
{diffData && diffData.added_count === 0 && diffData.removed_count === 0 && (
<div className="p-4 text-gray-400 text-center">No changes from AI baseline.</div>
)}
</div>
</div>
);
}

View file

@ -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}
/>
<div className="mt-2">
<VttDiffView jobId={id!} lang={selectedLanguage} kind="captions" />
</div>
</div>
)}
@ -1611,6 +1615,9 @@ export function QCDetail() {
glossaryTerms={glossaryTerms}
language={selectedLanguage}
/>
<div className="mt-2">
<VttDiffView jobId={id!} lang={selectedLanguage} kind="ad" />
</div>
</div>
)}
</div>