File size: 5,040 Bytes
33962ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | "use client";
import {AccessToken} from "@/components/AccessToken";
import {ImportMedia} from "@/components/ImportMedia";
import Link from "next/link";
import { useEffect, useState } from "react";
import { getJSON, type Stats, type VideoMeta } from "@/lib/api";
import { bytes, compact, fmtDuration } from "@/lib/format";
import { VIEW_INFO, VIEW_ORDER } from "@/lib/views";
import { ViewSwatch } from "@/components/Controls";
function Tile({ label, value, sub }: { label: string; value: string; sub?: string }) {
return (
<div className="rounded-xl border border-line bg-surface p-4">
<div className="text-xs text-muted">{label}</div>
<div className="mt-1 text-2xl font-semibold">{value}</div>
{sub && <div className="mt-0.5 text-xs text-ink-2">{sub}</div>}
</div>
);
}
export function Library() {
const [videos, setVideos] = useState<VideoMeta[] | null>(null);
const [stats, setStats] = useState<Stats | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
getJSON<VideoMeta[]>("/api/videos").then(setVideos).catch((e) => setError(String(e)));
getJSON<Stats>("/api/stats").then(setStats).catch(() => {});
}, []);
if (error) return <p className="text-ink-2">Library unavailable ({error}). Check access and service status.</p>;
const primaryDim = stats?.dims[0] ?? 768;
const s768 = stats?.storage.find((s) => s.dim === primaryDim)?.bytes_per_view ?? 0;
const s128 = stats?.storage.find((s) => s.dim === 128)?.bytes_per_view ?? 0;
return (
<div className="space-y-8">
<section>
<h1 className="text-2xl font-semibold">Library</h1>
<p className="mt-1 max-w-3xl text-sm text-ink-2">
Browse timestamped clips, transcripts and evidence. Remote text indexes search transcripts; image, video and audio views require a compatible multimodal index.
</p>
</section>
<AccessToken/><ImportMedia/>
{stats && (
<section className="grid grid-cols-2 gap-3 md:grid-cols-4">
<Tile label="Videos" value={String(stats.videos ?? videos?.length ?? "–")} sub={fmtDuration(stats.total_duration_s) + " of footage"} />
<Tile label="Segments" value={String(stats.segments ?? "–")} sub={`${stats.views.length} retrieval views`} />
<Tile label="Vectors" value={compact((stats.segments ?? 0) * Object.keys(stats.views_built ?? {}).length)} sub="segments × embedding views" />
<Tile label="Index size per view" value={bytes(s768)} sub={stats?.dims.includes(128)&&primaryDim!==128?`${bytes(s128)} at 128d (MRL)`:`${primaryDim}d · only declared dimensions`} />
</section>
)}
{stats?.views_built && (
<section className="rounded-xl border border-line bg-surface p-4">
<h2 className="text-sm font-medium">Embedding views</h2>
<div className="mt-3 grid gap-x-6 gap-y-2 text-sm md:grid-cols-2">
{VIEW_ORDER.filter((v) => stats.views.includes(v)).map((v) => {
const b = stats.views_built?.[v];
return (
<div key={v} className="flex items-center gap-2">
<ViewSwatch view={v} />
<span className="w-14 font-medium">{VIEW_INFO[v].label}</span>
<span className="flex-1 text-ink-2">{stats.view_descriptions[v]}</span>
{b && <span className="text-xs text-muted tabular">{b.ms_per_segment} ms/seg</span>}
</div>
);
})}
</div>
</section>
)}
{videos?.length===0&&<p className="rounded-xl border border-line bg-surface p-5 text-ink-2">Your library is ready for its first video. Import a clip above; missing inference dependencies appear in its job status.</p>}
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{(videos ?? []).map((v) => (
<Link key={v.id} href={`/videos/${v.id}`} className="group overflow-hidden rounded-xl border border-line bg-surface transition-colors hover:bg-surface-2">
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={v.thumb} alt="" className="aspect-video w-full object-cover" loading="lazy" />
<span className="absolute bottom-2 right-2 rounded bg-black/80 px-1.5 py-0.5 text-xs tabular">{fmtDuration(v.duration)}</span>
</div>
<div className="space-y-1 p-3">
<div className="line-clamp-2 text-sm font-medium">{v.title}</div>
<div className="flex flex-wrap gap-x-3 text-xs text-muted">
<span>{v.num_segments} segments</span>
{v.view_count != null && <span>{compact(v.view_count)} views</span>}
{v.id.startsWith("sample-") && <span className="text-accent">Synthetic sample</span>}
</div>
</div>
</Link>
))}
{!videos && <p className="text-sm text-muted">Loading…</p>}
</section>
</div>
);
}
|