import { useCallback, useEffect, useRef, useState, type FormEvent, type ReactNode } from "react"; import { Link, useNavigate } from "react-router-dom"; import { api } from "../api/client"; import { artifactsApi, docsApi, jobsApi, launchpadApi, notebooksApi, resurfaceApi, sourcesApi, type GenerateBody } from "../api/endpoints"; import type { Artifact, CalendarItem, Doc, Job, NotebookSummary, ResurfaceItem, Source } from "../api/types"; import { LaunchWindowIcon, PlanetIcon, RocketIcon, TelescopeIcon } from "../components/icons/Icons"; import { errorMessage, formatDate, JobProgress, StatusPill } from "../components/ui"; import { useAuth } from "../hooks/useAuth"; import { useJobMap } from "../hooks/useJob"; import { AddSource } from "./Sources"; import { PENDING_SOURCE_KEY } from "./Landing"; import { ONBOARDED_KEY } from "./Welcome"; function onboarded(): boolean { try { return localStorage.getItem(ONBOARDED_KEY) === "1"; } catch { return true; } } type Plan = { name: string }; const JOB_LABELS: Record = { ingest: "Syncing a source", import_url: "Importing an article", import_upload: "Importing a file", topics: "Mapping topics", voice_profile: "Building your writing voice", voice_clone: "Cloning your voice", resurface_scan: "Finding posts worth resharing", summary: "Writing a summary", audio_overview: "Recording an audio overview", video: "Making a video", quote_card: "Rendering a quote card", carousel: "Rendering a carousel", launch_kit: "Building a Launch Kit", render: "Rendering", }; /** Home: ask the archive, one-click creation, and only the next thing worth doing. */ export default function MissionControl() { const { user } = useAuth(); const [sources, setSources] = useState(null); const [notebooks, setNotebooks] = useState([]); const [recent, setRecent] = useState([]); const [upcoming, setUpcoming] = useState([]); const [newest, setNewest] = useState(null); const [reshare, setReshare] = useState([]); const [plan, setPlan] = useState(null); const [question, setQuestion] = useState(""); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const started = useRef(false); const navigate = useNavigate(); const refresh = useCallback(async () => { const [s, n, p, a, c, d, r] = await Promise.all([ sourcesApi.list(), notebooksApi.list(), api<{ plan: Plan }>("/api/billing/me"), artifactsApi.list({ limit: 8 }), launchpadApi.items({ start: new Date().toISOString(), status: "scheduled,draft" }), docsApi.list({ limit: 20 }), resurfaceApi.get().catch(() => null), ]); setSources(s); // First visit with nothing connected: run the guided setup once. if (s.length === 0 && !onboarded()) { navigate("/welcome", { replace: true }); return; } setNotebooks(n); setPlan(p.plan); setRecent(a.items); setUpcoming(c.slice(0, 5)); setNewest(d.items.find((x) => !x.locked) ?? null); setReshare((r?.evergreen ?? []).slice(0, 3)); }, [navigate]); const { jobs, watch } = useJobMap(() => refresh()); useEffect(() => { refresh().catch(() => setError("Could not load your dashboard. Refresh to try again.")); jobsApi.active().then((active) => active.forEach((j) => watch(j.id, j))); if (started.current) return; started.current = true; let pending: string | null = null; try { pending = sessionStorage.getItem(PENDING_SOURCE_KEY); sessionStorage.removeItem(PENDING_SOURCE_KEY); } catch { /* ignore */ } if (pending) { sourcesApi.connect(pending).then( (res) => { watch(res.job.id, res.job); refresh(); }, (e) => setError(errorMessage(e, "Could not add that source.")), ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [refresh]); const run = async (key: string, fn: () => Promise) => { setBusy(key); setError(null); try { await fn(); } catch (e) { setError(errorMessage(e)); } finally { setBusy(null); } }; const ask = (e: FormEvent) => { e.preventDefault(); void run("ask", async () => { const nb = await notebooksApi.archive(); navigate(`/app/notebooks/${nb.id}${question.trim() ? `?q=${encodeURIComponent(question.trim())}` : ""}`); }); }; const create = (key: string, body: GenerateBody) => run(key, async () => { await artifactsApi.generate(body); navigate("/app/studio"); }); const launch = (doc: Doc) => run(`kit-${doc.id}`, async () => { const kit = await artifactsApi.generate({ type: "launch_kit", document_id: doc.id }); navigate(`/app/launchpad/kits/${kit.id}`); }); const totalDocs = (sources ?? []).reduce((n, s) => n + s.document_count, 0); const hasPosts = totalDocs > 0; const activeJobs = Object.values(jobs).filter((j: Job) => j.status === "queued" || j.status === "running" || j.status === "failed"); const lastKit = recent.find((a) => a.type === "launch_kit" && a.status === "ready"); const next: { text: string; action: ReactNode } | null = !hasPosts ? null : !lastKit && newest ? { text: `Launch your newest post, "${newest.title}", with hooks, threads and SEO in your voice.`, action: ( ), } : lastKit && upcoming.length === 0 ? { text: `Your Launch Kit for "${lastKit.title}" is ready. Put it on the calendar.`, action: ( Schedule it ), } : null; const first = (user?.name ?? user?.email ?? "").split(/[ @]/)[0]; return (

Mission Control

Good to see you, {first}

{plan && {plan.name} plan}
{error &&

{error}

} {sources && !hasPosts ? (

Bring in your writing

Connect a blog or newsletter, import an article or drop in files. Everything else starts from here.

{ setSources((list) => [...(list ?? []).filter((s) => s.id !== source.id), source]); watch(job.id, job); }} />
) : ( <>
setQuestion(e.target.value)} placeholder="Ask anything about your writing: what have I said about pricing?" aria-label="Ask your archive" />
Video Turn a post or link into a narrated video More formats Explainers, quote cards, audiograms
{next && (

{next.text}

{next.action}
)} )} {activeJobs.length > 0 && (

In flight

{activeJobs.map((j) => (

{JOB_LABELS[j.kind] ?? j.kind}

))}
)} {hasPosts && (
{totalDocs} posts in {sources?.length} {sources?.length === 1 ? "source" : "sources"} {notebooks.length} notebooks {recent.length} recent launches {upcoming.length} scheduled
)} {(recent.length > 0 || upcoming.length > 0 || reshare.length > 0) && (
{recent.length > 0 && (

Recent launches

Library
    {recent.map((a) => (
  • {a.title} {formatDate(a.created_at)}
  • ))}
)}
{reshare.length > 0 && (

Worth resharing

More ideas
    {reshare.map((i) => (
  • {i.title} {i.angle && ยท {i.angle}}
  • ))}
)} {upcoming.length > 0 && (

Coming up

Calendar
    {upcoming.map((i) => (
  • {i.platform_label} {i.content.slice(0, 70)} {formatDate(i.scheduled_at, true)}
  • ))}
)}
)}
); }