ui(video-studio): 💄 Add InvisibleProtectionsDemo component and update visual assets for redaction/obfuscation demo

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-19 21:55:46 -07:00
parent 501ef82c39
commit cbd62335e6
3 changed files with 22 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

View file

@ -38,7 +38,7 @@ export function InvisibleProtectionsDemo({
const [outputMode, setOutputMode] = useState<'default' | 'lossless'>('lossless');
const [jobs, setJobs] = useState<ProtectJobResult[]>([]);
const [jobMeta, setJobMeta] = useState<
ReadonlyMap<string, { label: string; ops: string[]; submittedAt: number }>
ReadonlyMap<string, { label: string; ops: string[]; submittedAt: number; photoId: string | null }>
>(new Map());
const [expandedJobId, setExpandedJobId] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
@ -177,6 +177,7 @@ export function InvisibleProtectionsDemo({
label: activeLabel ?? activeVideoPath,
ops: [...selectedOps],
submittedAt: Date.now(),
photoId: selectedPhoto?.id ?? null,
});
return next;
});
@ -187,6 +188,16 @@ export function InvisibleProtectionsDemo({
}
}, [activeVideoPath, selectedOps, outputMode]);
const protectedPhotoOps = new Map<string, string[]>();
for (const job of jobs) {
if (job.status !== 'done') continue;
const meta = jobMeta.get(job.job_id);
if (meta?.photoId) {
const existing = protectedPhotoOps.get(meta.photoId) ?? [];
protectedPhotoOps.set(meta.photoId, [...new Set([...existing, ...meta.ops])]);
}
}
return (
<div style={s.root}>
{/* Top section: gallery + operation picker */}
@ -254,6 +265,7 @@ export function InvisibleProtectionsDemo({
setSelectedRecording(null);
onVideoSelect?.(photo.id);
}}
protectedOps={protectedPhotoOps.get(photo.id) ?? null}
/>
))}
</div>
@ -352,11 +364,13 @@ interface VideoCardProps {
photo: PhotoItem;
selected: boolean;
onClick: () => void;
protectedOps: string[] | null;
}
function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
function VideoCard({ photo, selected, onClick, protectedOps }: VideoCardProps): ReactElement {
const [hovered, setHovered] = useState(false);
const isDone = photo.processing_stage === 'done' || photo.processing_stage === 'failed';
const isProtected = protectedOps !== null;
return (
<button
@ -366,7 +380,7 @@ function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
onMouseLeave={() => setHovered(false)}
style={{
...s.photoCard,
...(selected ? s.photoCardSelected : {}),
...(selected ? s.photoCardSelected : isProtected ? { border: '2px solid #2e7d32' } : {}),
}}
title={`${photo.filename} · ${photo.duration_seconds.toFixed(1)}s`}
>
@ -400,6 +414,11 @@ function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
/>
</div>
)}
{isProtected && (
<div style={s.protectedBadge} title={protectedOps.join(', ')}>
🛡
</div>
)}
</div>
<span style={s.photoName}>{photo.filename}</span>
<span style={s.photoDuration}>{photo.duration_seconds.toFixed(1)}s</span>