import { useCallback, useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { artifactsApi, notebooksApi, type GenerateBody } from "../api/endpoints"; import type { Artifact, NotebookSummary } from "../api/types"; import { ArtifactRow } from "../components/ArtifactRow"; import { DocPicker } from "../components/DocPicker"; import { Dropdown } from "../components/Dropdown"; import { LaunchWindowIcon, PlanetIcon, RocketIcon } from "../components/icons/Icons"; import { Reader } from "../components/Reader"; import { errorMessage, PageHeader } from "../components/ui"; import { canPost, PostButton } from "../components/launchpad/PostButton"; type Kind = "audio" | "video" | "quote"; const KINDS: { id: Kind; label: string; blurb: string; icon: typeof PlanetIcon }[] = [ { id: "audio", label: "Audio overview", blurb: "Two hosts talk through your posts, every line grounded in what you wrote.", icon: PlanetIcon }, { id: "video", label: "Video", blurb: "Turn a notebook or a post into a narrated video you can edit scene by scene, landscape or vertical.", icon: LaunchWindowIcon }, { id: "quote", label: "Quote card", blurb: "Your most quotable line, verified against the post, as a 1080 square.", icon: RocketIcon }, ]; const SINGLE_POST: Record = { audio: "Create an audio overview of a single post", video: "Create a video on a single post", quote: "Make a quote card from a single post", }; const ARCHIVE = "archive"; const ONE_POST = "post"; /** Create: pick a format, say what it is about (the whole archive by default), press Create. */ export default function Studio() { const navigate = useNavigate(); const [kind, setKind] = useState("audio"); const [about, setAbout] = useState(ARCHIVE); const [notebooks, setNotebooks] = useState([]); const [postIds, setPostIds] = useState([]); const [format, setFormat] = useState<"deep_dive" | "brief" | "debate">("deep_dive"); const [minutes, setMinutes] = useState(6); const [recent, setRecent] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [reading, setReading] = useState<{ id: string; start: number; end: number } | null>(null); const load = useCallback( () => artifactsApi.list({ type: "audio_overview,video,quote_card", limit: 6 }).then((p) => setRecent(p.items)), [], ); useEffect(() => { notebooksApi.list().then((n) => setNotebooks(n.filter((x) => !x.is_archive))); load(); }, [load]); const create = async () => { setError(null); if (kind === "video") { // Videos have their own three step editor (blog2video); the picked notebook or post comes along. if (about === ONE_POST) { if (!postIds[0]) return setError("Pick a post."); return navigate(`/app/videos/new?document_id=${postIds[0]}`); } try { const id = about === ARCHIVE ? (await notebooksApi.archive()).id : about; navigate(`/app/videos/new?notebook_id=${id}`); } catch (e) { setError(errorMessage(e)); } return; } const where: Partial = about === ARCHIVE ? { archive: true } : about === ONE_POST ? { document_id: postIds[0] } : { notebook_id: about }; if (about === ONE_POST && !postIds[0]) { setError("Pick a post."); return; } const body: GenerateBody = kind === "audio" ? { type: "audio_overview", format, minutes, ...where } : { type: "quote_card", ...where }; setBusy(true); try { const a = await artifactsApi.generate(body); setRecent((g) => [a, ...(g ?? [])].slice(0, 6)); } catch (e) { setError(errorMessage(e)); } finally { setBusy(false); } }; const meta = KINDS.find((k) => k.id === kind)!; return (
{KINDS.map((k) => ( ))} Launch Kit

{meta.blurb}

What is it about? ({ value: n.id, label: n.title, hint: `${n.document_count} posts`, group: "Notebooks" })), { value: ONE_POST, label: SINGLE_POST[kind], group: "Single post" }, ]} />
{kind === "audio" && ( <>
Format
Length setMinutes(Number(v))} options={[3, 6, 10, 15, 20].map((m) => ({ value: String(m), label: `${m} minutes` }))} />
)}
{about === ONE_POST && } {error &&

{error}

}
{recent && recent.length > 0 && (

Latest

Everything in Library
    {recent.map((a) => ( c.document_id && setReading({ id: c.document_id, start: c.line_start, end: c.line_end })} onRemoved={(id) => setRecent((g) => (g ?? []).filter((x) => x.id !== id))} onChanged={(next) => setRecent((g) => (g ?? []).map((x) => (x.id === next.id ? next : x)))} actions={(art) => canPost(art) ? : art.type === "audio_overview" && art.status === "ready" ? ( ) : null } /> ))}
)} {reading && setReading(null)} />}
); }