import { useCallback, useEffect, useRef, useState } from "react"; import { Link, Navigate, useSearchParams } from "react-router-dom"; import { artifactsApi } from "../api/endpoints"; import type { Artifact } from "../api/types"; import { ArtifactRow } from "../components/ArtifactRow"; import { Reader } from "../components/Reader"; import { EmptyState, Loading, PageHeader, Tabs } from "../components/ui"; import Videos from "./Videos"; import { canPost, PostButton } from "../components/launchpad/PostButton"; import { AudioOnlyModal } from "../components/launchpad/Attachment"; type Filter = "all" | "report" | "summary" | "quiz" | "flashcards" | "mind_map" | "infographic" | "slide_deck" | "audio_overview"; /** The Library's tabs: kinds of things made (a filter on one list), plus your videos (their own page). Voices moved to * the sidebar (Manage voices, /app/voices); an old ?tab=voices link goes there. */ type Tab = Filter | "videos"; const TABS: { id: Tab; label: string }[] = [ { id: "all", label: "All" }, { id: "report", label: "Reports" }, { id: "summary", label: "Summaries" }, { id: "quiz", label: "Quizzes" }, { id: "flashcards", label: "Flashcards" }, { id: "mind_map", label: "Mind Constellations" }, { id: "infographic", label: "Infographics" }, { id: "slide_deck", label: "Slide decks" }, { id: "audio_overview", label: "Audio overviews" }, { id: "videos", label: "Videos" }, ]; const PAGE = 30; export default function Archive() { const [params, setParams] = useSearchParams(); if (params.get("tab") === "voices") return ; const tab: Tab = TABS.find((t) => t.id === params.get("tab"))?.id ?? "all"; // The tab lives in the URL (?tab=videos), so the sidebar, redirects and Back land on it. const pickTab = (t: Tab) => setParams(t === "all" ? {} : { tab: t }); return (
tabs={TABS} value={tab} onChange={pickTab} /> {tab === "videos" ? : }
); } /** "Post" on an audio overview: X and LinkedIn take no audio files, so it explains that and offers the download. */ function AudioPostButton({ artifact }: { artifact: Artifact }) { const [open, setOpen] = useState(false); return ( <> {open && setOpen(false)} />} ); } /** What was made, of one kind (or all), searchable. */ function MadeList({ filter }: { filter: Filter }) { const [q, setQ] = useState(""); const [items, setItems] = useState(null); const [total, setTotal] = useState(0); const [failed, setFailed] = useState(false); const [reading, setReading] = useState<{ id: string; start: number; end: number } | null>(null); // The tab and search the list is showing: an answer for any other (a tab just left) is dropped when it arrives late. const showing = useRef(""); showing.current = `${filter}|${q}`; const load = useCallback( (offset = 0) => { const asked = `${filter}|${q}`; return artifactsApi .list({ type: filter === "all" ? undefined : filter, q, limit: PAGE, offset }) .then((p) => { if (showing.current !== asked) return; setFailed(false); setTotal(p.total); setItems((cur) => (offset ? [...(cur ?? []), ...p.items] : p.items)); }) .catch(() => showing.current === asked && setFailed(true)); }, [filter, q], ); // Another tab: the loader until its items are fetched, never the last tab's items meanwhile. useEffect(() => { setItems(null); setFailed(false); }, [filter]); useEffect(() => { const t = setTimeout(() => load(0), 200); return () => clearTimeout(t); }, [load]); return (
setQ(e.target.value)} aria-label="Search" />
{!items && !failed &&
} {!items && failed && ( load(0)}>Try again} /> )} {items?.length === 0 && Create something } />}
    {items?.map((a) => ( (canPost(art) ? : art.type === "audio_overview" && art.status === "ready" ? : null)} onCite={(c) => c.document_id && setReading({ id: c.document_id, start: c.line_start, end: c.line_end })} onRemoved={(id) => { setItems((list) => (list ?? []).filter((x) => x.id !== id)); setTotal((t) => t - 1); }} /> ))}
{items && items.length < total && ( )} {reading && setReading(null)} />}
); }