Spaces:
Runtime error
Runtime error
File size: 5,932 Bytes
fa6e4a3 34b0226 070c931 34b0226 e1222dc bdd1640 fa6e4a3 34b0226 fa6e4a3 e1222dc 0649f1b fa6e4a3 e1222dc 34b0226 e1222dc fa6e4a3 e1222dc fa6e4a3 e1222dc fa6e4a3 e1222dc 34b0226 0649f1b 34b0226 fa6e4a3 34b0226 fa6e4a3 3c705c5 34b0226 fa6e4a3 0649f1b 34b0226 0649f1b fa6e4a3 3c705c5 34b0226 fa6e4a3 34b0226 e1222dc 3c705c5 34b0226 9cee3e3 0649f1b fa6e4a3 3c705c5 070c931 34b0226 070c931 34b0226 fa6e4a3 34b0226 070c931 34b0226 | 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | 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 <Navigate to="/app/voices" replace />;
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 (
<div className="page-wrap">
<PageHeader eyebrow="Library" title="Everything you have made" />
<Tabs<Tab> tabs={TABS} value={tab} onChange={pickTab} />
{tab === "videos" ? <Videos /> : <MadeList filter={tab} />}
</div>
);
}
/** "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 (
<>
<button type="button" className="btn btn-small" onClick={() => setOpen(true)}>Post</button>
{open && <AudioOnlyModal title={artifact.title} downloadUrl={artifact.download_url} onClose={() => setOpen(false)} />}
</>
);
}
/** What was made, of one kind (or all), searchable. */
function MadeList({ filter }: { filter: Filter }) {
const [q, setQ] = useState("");
const [items, setItems] = useState<Artifact[] | null>(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 (
<div className="stack">
<div className="row end wrap">
<input className="input input-sm search" placeholder={`Search ${total} items by post or notebook`} value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search" />
</div>
{!items && !failed && <div className="loading-center"><Loading label="Loading your Library" /></div>}
{!items && failed && (
<EmptyState title="Could not load your library" body="Something went wrong reaching the server."
action={<button className="btn btn-primary" onClick={() => load(0)}>Try again</button>} />
)}
{items?.length === 0 && <EmptyState title="Nothing here yet" body="Reports, summaries, quizzes, flashcards, Mind Constellations, slide decks, audio and more that you create show up here."
action={
<Link className="btn btn-primary" to="/app/studio">
Create something
</Link>
}
/>}
<ul className="vw-list">
{items?.map((a) => (
<ArtifactRow
key={a.id}
artifact={a}
actions={(art) => (canPost(art) ? <PostButton artifactId={art.id} />
: art.type === "audio_overview" && art.status === "ready" ? <AudioPostButton artifact={art} /> : 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);
}}
/>
))}
</ul>
{items && items.length < total && (
<button className="btn" onClick={() => load(items.length)}>
Load more
</button>
)}
{reading && <Reader documentId={reading.id} highlight={{ start: reading.start, end: reading.end }} onClose={() => setReading(null)} />}
</div>
);
}
|