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:
parent
dc1cfd01dc
commit
aba43a67d7
2 changed files with 111 additions and 0 deletions
104
frontend/src/components/VttEditor/VttDiffView.tsx
Normal file
104
frontend/src/components/VttEditor/VttDiffView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue