import { useQuery } from "@tanstack/react-query"; import { useLocation } from "@tanstack/react-router"; import { useServerFn } from "@tanstack/react-start"; import { useState } from "react"; import { AppShell } from "#/components/app-shell"; import { Dialog } from "#/components/dialog"; import { Icon } from "#/components/icon"; import { Button } from "#/components/ui/button"; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "#/components/ui/empty"; import { Input } from "#/components/ui/input"; import { Skeleton } from "#/components/ui/skeleton"; import { loadConnections } from "#/features/connections/server-functions"; import { syncResearchDeck } from "#/features/research/research-deck-sync"; import { ResearchPanel } from "#/features/research/research-panel"; import { sameCitationSource, useCitationNavigation, } from "#/features/research/use-citation-navigation"; import { ColumnEditor } from "./column-editor"; import { type ColumnRegistry, useColumnTools } from "./column-tools"; import { ResearchColumn } from "./deck-column"; import { DeckSwitcher } from "./deck-switcher"; import { type DeckColumn, MAX_COLUMNS, moveColumn } from "./model"; import { ResearchDeckSkeleton } from "./research-deck-skeleton"; import { useDeck } from "./use-deck"; import { useDeckTools } from "./webmcp-tools"; export function DeckPage() { const oauthResult = useLocation({ select: (location) => new URLSearchParams(location.searchStr).get("mastodon"), }); const [registry] = useState(() => new Map()); const [researchDeckBindings] = useState(() => new Map()); const [editing, setEditing] = useState<{ id: string } | "new" | null>(null); const [renaming, setRenaming] = useState(false); const [deckDialog, setDeckDialog] = useState<"create" | "delete" | null>(null); const [newDeckTitle, setNewDeckTitle] = useState(""); const switcherEditing = deckDialog !== null; const { deck, workspace, ready, saving, persist, createTemporary, refresh, importLegacy, legacyAvailable, storageError, save, create, select, remove, getWorkspace, } = useDeck(editing !== null || renaming || switcherEditing); const citationNavigation = useCitationNavigation({ registry, getWorkspace, select, createTemporary, }); const fetchProfiles = useServerFn(loadConnections); const profiles = useQuery({ queryKey: ["connections"], queryFn: () => fetchProfiles(), retry: false, }); useColumnTools(registry, ready); const [removed, setRemoved] = useState<{ column: DeckColumn; index: number; }>(); useDeckTools({ ready, saving, storageError, save, select, remove, getWorkspace, createTemporary, persist, connections: profiles.data?.connections, onApplied: clearEditors, }); function clearEditors() { setDeckDialog(null); setNewDeckTitle(""); setEditing(null); setRenaming(false); setRemoved(undefined); } async function saveColumn(column: DeckColumn) { const saved = await save({ ...deck, columns: editing === "new" ? [...deck.columns, column] : deck.columns.map((current) => (current.id === column.id ? column : current)), }); if (saved) setEditing(null); return saved; } const storageErrorNotice = storageError ? (

{storageError}

) : null; return ( { await syncResearchDeck(plan, activate, researchDeckBindings, { getWorkspace, createTemporary, save, select, }); if (activate) clearEditors(); }} > {(renderResearchPanel) => ( !deck.persisted)} researchChat={renderResearchPanel} sidebar={ <> { setDeckDialog(dialog); if (dialog !== "create") setNewDeckTitle(""); }} onSelect={(id) => { select(id); clearEditors(); }} onCreate={async (title) => { const saved = await create(title); if (saved) clearEditors(); return saved; }} onDelete={async () => { const saved = await remove(deck.id); if (saved) clearEditors(); return saved; }} />

{deck.persisted ? "Saved to server · Shared across devices" : "Temporary view · This tab only"}

} >
{ready ? ( <>

{deck.title}

{deck.columns.length} / {MAX_COLUMNS} columns ) : ( )}
{deck.persisted ? ( ) : ( )}
{saving ? (

Saving…

) : null} {legacyAvailable ? (

Legacy decks are available in this browser.

) : null} {profiles.isError ? (

Unable to load connection profiles.

) : null} {oauthResult === "failed" ? (

Could not complete Mastodon authorization. Try again from Connected accounts.

) : null} {oauthResult === "connected" ? (

Mastodon account connected. You can now use it in a column.

) : null} {profiles.data?.relayError ? (

{profiles.data.relayError}

) : null} {!renaming && editing === null && !switcherEditing ? storageErrorNotice : null} {renaming ? ( setRenaming(false)}>
{ event.preventDefault(); const rawTitle = new FormData(event.currentTarget).get("title"); const title = typeof rawTitle === "string" ? rawTitle.trim() : ""; if (!title) return; if (await save({ ...deck, title })) setRenaming(false); }} >
{storageErrorNotice}
) : null} {editing !== null ? ( setEditing(null)} > column.id === editing.id) } onSave={saveColumn} onCancel={() => setEditing(null)} /> {storageErrorNotice} ) : null} {removed ? (
Deleted “{removed.column.title}”.
) : null} {!ready && !storageError ? : null} {ready && deck.columns.length === 0 && editing === null ? (

Start a deck with a question

Tell the assistant what you want to research to create relevant columns. You can also add search, user posts, and list columns yourself.
) : null} {ready && deck.columns.length > 0 ? (
{deck.columns.map((column, index) => ( connection.id === column.connectionId, )?.displayName } first={index === 0} last={index === deck.columns.length - 1} onEdit={() => setEditing({ id: column.id })} onMove={(direction) => save(moveColumn(deck, column.id, direction))} onRemove={async () => { const saved = await save({ ...deck, columns: deck.columns.filter((current) => current.id !== column.id), }); if (saved) setRemoved({ column, index }); if (typeof editing === "object" && editing?.id === column.id) setEditing(null); }} /> ))}
) : null}
)}
); }