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>
  );
}