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