"use client"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import { EditorContent, useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "@tiptap/markdown"; type Phase = "ideation" | "drafting" | "finalization"; type View = Phase | "project" | "history" | "provenance" | "export" | "settings"; type IdeaStatus = "suggested" | "accepted" | "rejected" | "archived"; type Actor = "user" | "assistant" | "system"; interface Turn { id: string; speaker: "user" | "assistant"; text: string; createdAt: string } interface Idea { id: string; title: string; summary: string; detail: string; status: IdeaStatus; sourceTurnIds: string[]; parentIdeaIds: string[]; tags: string[]; pinned: boolean; selected: boolean; group?: string; createdBy: "user" | "assistant" | "mixed" } interface Event { id: string; type: string; actor: Actor; at: string; summary: string; previousHash: string | null; hash: string; provider?: string } interface Checkpoint { id: string; name: string; at: string; manuscript: string; words: number } interface Provider { kind: "ollama" | "openai" | "compatible"; name: string; endpoint: string; model: string; mode: "local" | "cloud"; connected: boolean } interface State { id: string; title: string; subtitle: string; phase: Phase; privacy: "Local" | "Cloud" | "Mixed"; cloudApproved: boolean; challenge: "Gentle" | "Balanced" | "Rigorous"; spokenReplies: boolean; turns: Turn[]; ideas: Idea[]; manuscript: string; generation: { id: string; state: "idle" | "streaming" | "staged" | "accepted" | "rejected" | "failed"; text: string; prompt: string; relation: string }; events: Event[]; checkpoints: Checkpoint[]; provider: Provider; styleTraits: string[]; disallowedHabits: string[]; finalized: boolean; updatedAt: string } const now = () => new Date().toISOString(); const uid = (prefix: string) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`; const countWords = (value: string) => value.trim() ? value.trim().split(/\s+/).length : 0; const fmt = (value: string) => new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(value)); const hash = (value: string) => { let n = 2166136261; for (let i = 0; i < value.length; i += 1) n = Math.imul(n ^ value.charCodeAt(i), 16777619); return Math.abs(n).toString(16).padStart(8, "0"); }; const clean = (md: string) => md.replace(/^#{1,6}\s+/gm, "").replace(/[*_`>]/g, ""); const escapeHtml = (v: string) => v.replace(/&/g, "&").replace(//g, ">"); const toHtml = (md: string) => md.split("\n").map((line) => line.startsWith("# ") ? `

${escapeHtml(line.slice(2))}

` : line.startsWith("## ") ? `

${escapeHtml(line.slice(3))}

` : line.trim() ? `

${escapeHtml(line)}

` : "").join("\n"); const initial: State = { id: "project_attention_commons", title: "The Attention Commons", subtitle: "An essay on reclaiming shared focus", phase: "ideation", privacy: "Local", cloudApproved: false, challenge: "Balanced", spokenReplies: false, turns: [ { id: "turn_1", speaker: "user", text: "I keep returning to the idea that attention is treated as a private resource, even though its erosion changes public life.", createdAt: "2026-07-16T17:42:00.000Z" }, { id: "turn_2", speaker: "assistant", text: "That tension feels central: we experience distraction individually, but the consequences accumulate socially. Where do you see that shared cost most clearly?", createdAt: "2026-07-16T17:43:00.000Z" }, { id: "turn_3", speaker: "user", text: "In classrooms, meetings, and even public debate. We have fewer places where sustained attention is expected and protected.", createdAt: "2026-07-16T17:44:00.000Z" }, ], ideas: [ { id: "idea_1", title: "Attention as civic infrastructure", summary: "Treat sustained attention like a shared condition that institutions can protect or degrade.", detail: "Move beyond individual self-control and ask what healthy attention-supporting institutions look like.", status: "accepted", sourceTurnIds: ["turn_1", "turn_3"], parentIdeaIds: [], tags: ["thesis", "public life"], pinned: true, selected: true, group: "Core argument", createdBy: "mixed" }, { id: "idea_2", title: "The vanishing expectation", summary: "Public spaces increasingly assume interruption instead of sustained presence.", detail: "Contrast spaces across expectations rather than generations.", status: "suggested", sourceTurnIds: ["turn_3"], parentIdeaIds: [], tags: ["observation"], pinned: false, selected: false, createdBy: "assistant" }, { id: "idea_3", title: "Beyond digital minimalism", summary: "Personal habits matter, but they cannot repair systems designed around extraction.", detail: "A counterpoint to purely individual advice about focus.", status: "suggested", sourceTurnIds: ["turn_1"], parentIdeaIds: [], tags: ["counterpoint"], pinned: false, selected: false, createdBy: "assistant" }, ], manuscript: "# The Attention Commons\n\nWe have learned to talk about attention as if it were a private possession. We protect it with timers, muted notifications, and carefully chosen routines. Yet the places where attention matters most—classrooms, meetings, courts, and public debate—are shared. When sustained focus becomes rare, the loss does not remain private.\n\n## A public condition\n\nAttention is not simply something an individual brings into a room. It is also something the room can make possible. Norms, incentives, architecture, and technology all determine whether concentration is supported or quietly auctioned away.\n", generation: { id: "", state: "idle", text: "", prompt: "", relation: "synthesize" }, events: [ { id: "event_1", type: "PROJECT_CREATED", actor: "user", at: "2026-07-16T17:42:00.000Z", summary: "Created The Attention Commons", previousHash: null, hash: "1c4b790d" }, { id: "event_2", type: "IDEA_ACCEPTED", actor: "user", at: "2026-07-16T17:45:00.000Z", summary: "Accepted “Attention as civic infrastructure”", previousHash: "1c4b790d", hash: "783fa0b2" }, ], checkpoints: [{ id: "version_1", name: "Opening premise", at: "2026-07-16T17:50:00.000Z", manuscript: "# The Attention Commons\n\nWe have learned to talk about attention as if it were a private possession.", words: 14 }], provider: { kind: "ollama", name: "Ollama", endpoint: "http://127.0.0.1:11434", model: "llama3.2", mode: "local", connected: false }, styleTraits: ["Clear, reflective sentences", "Concrete institutional examples", "Measured, invitational argument"], disallowedHabits: ["Overstated certainty", "Generic scene-setting"], finalized: false, updatedAt: "2026-07-16T17:42:00.000Z", }; async function invokeNative(command: string, args: Record = {}): Promise { const host = window as unknown as { __TAURI_INTERNALS__?: { invoke: (name: string, payload: Record) => Promise } }; return host.__TAURI_INTERNALS__ ? host.__TAURI_INTERNALS__.invoke(command, args) : null; } function download(name: string, contents: string, type = "text/plain") { const href = URL.createObjectURL(new Blob([contents], { type })); const a = document.createElement("a"); a.href = href; a.download = name; a.click(); URL.revokeObjectURL(href); } function Mark({ children, tone = "neutral" }: { children: React.ReactNode; tone?: string }) { return {children}; } function Empty({ eyebrow, title, body }: { eyebrow: string; title: string; body: string }) { return
{eyebrow}

{title}

{body}

; } interface StructuredEditorHandle { focus: () => void; insert: (markdown: string, replace: boolean) => string; } const StructuredEditor = forwardRef void; onCommit: () => void }>(function StructuredEditor({ value, onChange, onCommit }, ref) { const instance = useEditor({ extensions: [StarterKit, Markdown], content: value, contentType: "markdown", immediatelyRender: false, editorProps: { attributes: { class: "tiptap-document", "aria-label": "Structured manuscript editor" } }, onUpdate: ({ editor }) => onChange(editor.getMarkdown()), onBlur: onCommit, }); useEffect(() => { if (instance && instance.getMarkdown() !== value) instance.commands.setContent(value, { contentType: "markdown", emitUpdate: false }); }, [instance, value]); useImperativeHandle(ref, () => ({ focus: () => { instance?.commands.focus(); }, insert: (markdown, replace) => { if (!instance) return value; if (replace) instance.chain().focus().deleteSelection().insertContent(markdown, { contentType: "markdown" }).run(); else instance.chain().focus().insertContent(markdown, { contentType: "markdown" }).run(); return instance.getMarkdown(); }, }), [instance, value]); if (!instance) return
Preparing structured editor…
; return
; }); export default function Thinkloom() { const [project, setProject] = useState(initial); const [hydrated, setHydrated] = useState(false); const [view, setView] = useState("ideation"); const [message, setMessage] = useState(""); const [notice, setNotice] = useState("Project ready. History is being recorded."); const [busy, setBusy] = useState(false); const [listening, setListening] = useState(false); const [showRejected, setShowRejected] = useState(false); const [editingIdea, setEditingIdea] = useState(null); const [newHabit, setNewHabit] = useState(""); const [sanitized, setSanitized] = useState(true); const [credential, setCredential] = useState(""); const [projectPath, setProjectPath] = useState(""); const editor = useRef(null); const finalEditor = useRef(null); const conversationEnd = useRef(null); useEffect(() => { queueMicrotask(() => { try { const raw = localStorage.getItem("thinkloom-project-v1"); if (raw) setProject({ ...initial, ...JSON.parse(raw) as State }); } catch {} setHydrated(true); }); }, []); useEffect(() => { if (hydrated) localStorage.setItem("thinkloom-project-v1", JSON.stringify(project)); }, [project, hydrated]); const mutate = useCallback((type: string, summary: string, update: (current: State) => State, actor: Actor = "user") => { setProject((current) => { const previousHash = current.events.at(-1)?.hash ?? null; const event: Event = { id: uid("event"), type, actor, at: now(), summary, previousHash, hash: hash(`${previousHash}|${type}|${summary}|${Date.now()}`), provider: actor === "assistant" ? current.provider.name : undefined }; const next = { ...update(current), events: [...current.events, event], updatedAt: event.at }; void invokeNative("persist_state", { appState: next }).catch(() => undefined); return next; }); setNotice(summary); }, []); const navigate = (next: View) => { setView(next); if (["ideation", "drafting", "finalization"].includes(next) && project.phase !== next) mutate("PHASE_CHANGED", `Moved to ${next}`, (current) => ({ ...current, phase: next as Phase })); }; useEffect(() => { const shortcut = (e: KeyboardEvent) => { if (!e.altKey) return; if (e.key === "1") document.getElementById("ideas-panel")?.focus(); if (e.key === "2") editor.current?.focus(); if (e.key === "3") document.getElementById("assistant-panel")?.focus(); }; window.addEventListener("keydown", shortcut); return () => window.removeEventListener("keydown", shortcut); }, []); const visibleIdeas = useMemo(() => project.ideas.filter((idea) => showRejected || !["rejected", "archived"].includes(idea.status)), [project.ideas, showRejected]); const draftingIdeas = project.ideas.filter((idea) => idea.status === "accepted"); const selectedIdeas = draftingIdeas.filter((idea) => idea.selected); const sendMessage = async () => { const text = message.trim(); if (!text || busy) return; setMessage(""); setBusy(true); const userTurn: Turn = { id: uid("turn"), speaker: "user", text, createdAt: now() }; mutate("USER_TEXT_TURN_CREATED", "Saved your conversation turn", (current) => ({ ...current, turns: [...current.turns, userTurn] })); await new Promise((resolve) => setTimeout(resolve, 400)); const focus = text.replace(/[.!?].*$/, "").slice(0, 100); const reply = project.challenge === "Gentle" ? `There is something worth staying with in “${focus}.” What personal moment makes that feel true?` : project.challenge === "Rigorous" ? `If a skeptical reader challenged “${focus},” what evidence or distinction would make it hold?` : `“${focus}” suggests a useful tension. What changes when you see it as a shared condition?`; const assistantTurn: Turn = { id: uid("turn"), speaker: "assistant", text: reply, createdAt: now() }; const suggestion: Idea = { id: uid("idea"), title: focus.length > 46 ? `${focus.slice(0, 43)}…` : focus, summary: text, detail: "Explore how this supports or complicates the central argument.", status: "suggested", sourceTurnIds: [userTurn.id], parentIdeaIds: [], tags: ["from conversation"], pinned: false, selected: false, createdBy: "mixed" }; mutate("ASSISTANT_RESPONSE_GENERATED", "A response and suggested idea are ready", (current) => ({ ...current, turns: [...current.turns, assistantTurn], ideas: [...current.ideas, suggestion] }), "assistant"); if (project.spokenReplies && "speechSynthesis" in window) speechSynthesis.speak(new SpeechSynthesisUtterance(reply)); setBusy(false); requestAnimationFrame(() => conversationEnd.current?.scrollIntoView({ behavior: "smooth" })); }; const saveLastTurn = () => { const turn = [...project.turns].reverse().find((item) => item.speaker === "user"); if (!turn) return; const idea: Idea = { id: uid("idea"), title: turn.text.slice(0, 58), summary: turn.text, detail: "Saved directly from conversation.", status: "accepted", sourceTurnIds: [turn.id], parentIdeaIds: [], tags: ["saved directly"], pinned: false, selected: true, createdBy: "user" }; mutate("IDEA_ACCEPTED", `Saved “${idea.title}” as an idea`, (current) => ({ ...current, ideas: [...current.ideas, idea] })); }; const setIdeaStatus = (id: string, status: IdeaStatus) => { const idea = project.ideas.find((item) => item.id === id); if (!idea) return; mutate(`IDEA_${status.toUpperCase()}`, `${status === "accepted" ? "Accepted" : status === "rejected" ? "Rejected" : "Archived"} “${idea.title}”`, (current) => ({ ...current, ideas: current.ideas.map((item) => item.id === id ? { ...item, status, selected: status === "accepted" } : item) })); }; const updateIdea = (id: string, patch: Partial, summary = "Updated idea") => mutate("IDEA_EDITED", summary, (current) => ({ ...current, ideas: current.ideas.map((idea) => idea.id === id ? { ...idea, ...patch } : idea) })); const createVariant = (source: Idea) => { const variant: Idea = { ...source, id: uid("idea"), title: `${source.title} — another angle`, detail: `A deliberate counter-reading of: ${source.detail}`, parentIdeaIds: [source.id], status: "suggested", selected: false, pinned: false, createdBy: "mixed" }; mutate("IDEA_VARIANT_CREATED", `Created a variant of “${source.title}”`, (current) => ({ ...current, ideas: [...current.ideas, variant] })); }; const mergeSelected = () => { if (selectedIdeas.length < 2) { setNotice("Select at least two ideas to merge."); return; } const merged: Idea = { id: uid("idea"), title: selectedIdeas.map((idea) => idea.title).join(" + ").slice(0, 70), summary: selectedIdeas.map((idea) => idea.summary).join(" "), detail: "A synthesis retaining both parent ideas.", status: "accepted", sourceTurnIds: [...new Set(selectedIdeas.flatMap((idea) => idea.sourceTurnIds))], parentIdeaIds: selectedIdeas.map((idea) => idea.id), tags: [...new Set(selectedIdeas.flatMap((idea) => idea.tags))], pinned: true, selected: true, createdBy: "mixed" }; mutate("IDEAS_MERGED", `Merged ${selectedIdeas.length} ideas`, (current) => ({ ...current, ideas: [...current.ideas.map((idea) => selectedIdeas.some((selected) => selected.id === idea.id) ? { ...idea, selected: false } : idea), merged] })); }; const startGeneration = async (action = "draft") => { if (busy) return; if (action === "draft" && !selectedIdeas.length) { setNotice("Select at least one accepted idea first."); return; } setBusy(true); const id = uid("generation"); const source = selectedIdeas.map((idea) => idea.summary).join(" ") || "the current passage"; const prompt = action === "draft" ? `Draft a passage that ${project.generation.relation}s the selected ideas.` : `${action} the selected passage.`; mutate("GENERATION_REQUESTED", `Started ${action.toLowerCase()} preview`, (current) => ({ ...current, generation: { ...current.generation, id, state: "streaming", text: "", prompt } })); await new Promise((resolve) => setTimeout(resolve, 500)); let nativeText: string | null = null; try { nativeText = await invokeNative("generate_text", { profile: project.provider, prompt, context: source, cloudApproved: project.cloudApproved }); } catch (error) { mutate("GENERATION_FAILED", "Provider request failed; your request is preserved for retry", (current) => ({ ...current, generation: { ...current.generation, id, state: "failed", text: "", prompt } }), "system"); setNotice(`Provider request failed: ${String(error)}`); setBusy(false); return; } const variants: Record = { draft: `The language of personal discipline obscures a public design problem. ${source} A community’s capacity for sustained thought depends on the expectations it builds into ordinary places: whether a classroom protects silence long enough for uncertainty, whether a meeting rewards listening before reaction, and whether public argument leaves room for ideas that cannot arrive as slogans.`, Clarity: "Attention is personal in experience but public in consequence. Institutions shape whether people can sustain it together.", Rewrite: "We guard attention as private property, yet its most important work happens between us—in classrooms, meetings, and the slow exchange of public argument.", Shorten: "Attention feels private, but its loss reshapes public life.", Expand: "Attention feels private, but its loss reshapes public life. The change appears first as friction: a classroom that cannot settle, a meeting that repeats itself, a debate that rewards instant response. Over time, those moments become an institutional condition.", Transition: "That is why the question must move beyond individual habit and toward the environments we share.", Repetition: "Consider trimming repeated uses of “private,” “shared,” and “attention” in the opening two paragraphs.", Consistency: "The draft consistently frames attention as infrastructure; keep the institutional examples parallel to maintain that logic.", Tone: "A measured version can make the claim firmly without blaming readers for systems they did not design.", "User voice": "In your reflective voice: the room matters because attention is never brought into it alone; it is invited, protected, or spent there.", Proofread: "The selected passage is mechanically clean. Consider an em dash instead of the current parenthetical pause." }; mutate("GENERATION_COMPLETED", "Preview ready — your manuscript is unchanged", (current) => ({ ...current, generation: { ...current.generation, id, state: "staged", text: nativeText ?? variants[action] ?? variants.draft, prompt } }), "assistant"); setBusy(false); }; const acceptGeneration = (mode: "cursor" | "replace" | "append" | "section", partial = false) => { const full = project.generation.text; if (!full) return; const accepted = partial ? full.split(/(?<=[.!?])\s+/).slice(0, 2).join(" ") : full; let manuscript = project.manuscript; if ((mode === "replace" || mode === "cursor") && view === "drafting") { manuscript = editor.current?.insert(accepted, mode === "replace") ?? manuscript; } else if (mode === "replace") { const start = finalEditor.current?.selectionStart ?? manuscript.length; const end = finalEditor.current?.selectionEnd ?? start; manuscript = `${manuscript.slice(0, start)}${accepted}${manuscript.slice(end)}`; } else if (mode === "section") manuscript = `${manuscript.trimEnd()}\n\n## New section\n\n${accepted}\n`; else manuscript = `${manuscript.trimEnd()}\n\n${accepted}\n`; mutate(partial ? "GENERATION_PARTIALLY_ACCEPTED" : "GENERATION_ACCEPTED", `${partial ? "Partially accepted" : "Accepted"} generated text`, (current) => ({ ...current, manuscript, generation: { ...current.generation, state: "accepted" } })); }; const discardGeneration = () => mutate("GENERATION_REJECTED", "Discarded generated preview", (current) => ({ ...current, generation: { ...current.generation, state: "rejected", text: "" } })); const saveCheckpoint = (name = `Version ${project.checkpoints.length + 1}`) => { const point: Checkpoint = { id: uid("version"), name, at: now(), manuscript: project.manuscript, words: countWords(project.manuscript) }; mutate("CHECKPOINT_CREATED", `Saved version “${name}”`, (current) => ({ ...current, checkpoints: [...current.checkpoints, point] })); void invokeNative("create_checkpoint", { name }); }; const restoreCheckpoint = (point: Checkpoint) => mutate("VERSION_RESTORED", `Restored “${point.name}”`, (current) => ({ ...current, manuscript: point.manuscript })); const finalize = () => { const point: Checkpoint = { id: uid("release"), name: `Release ${project.checkpoints.filter((item) => item.name.startsWith("Release")).length + 1}`, at: now(), manuscript: project.manuscript, words: countWords(project.manuscript) }; mutate("RELEASE_FINALIZED", `Finalized ${point.name}`, (current) => ({ ...current, finalized: true, checkpoints: [...current.checkpoints, point] })); void invokeNative("finalize_release"); }; const exportFile = async (format: "markdown" | "html" | "text" | "evidence") => { const native = await invokeNative("export_project", { format, sanitized }); if (native) { setNotice(`Exported to ${native}`); return; } if (format === "markdown") download(`${project.title}.md`, project.manuscript, "text/markdown"); if (format === "text") download(`${project.title}.txt`, clean(project.manuscript)); if (format === "html") download(`${project.title}.html`, `${escapeHtml(project.title)}${toHtml(project.manuscript)}`, "text/html"); if (format === "evidence") download(`${project.title}-creative-process.json`, JSON.stringify({ schemaVersion: "1.0", projectId: project.id, createdAt: now(), sanitized, provenanceChainHead: project.events.at(-1)?.hash, manuscriptHash: hash(project.manuscript), events: sanitized ? project.events.filter((event) => !event.provider) : project.events }, null, 2), "application/json"); mutate("EXPORT_CREATED", `Created ${format} export`, (current) => current); }; const startVoice = () => { if (listening) { setListening(false); setNotice("Voice input stopped; no audio was retained."); return; } speechSynthesis?.cancel(); const host = window as unknown as { webkitSpeechRecognition?: new () => { lang: string; interimResults: boolean; start: () => void; onresult: (e: { results: ArrayLike<{ 0: { transcript: string } }> }) => void; onend: () => void; onerror: () => void } }; if (!host.webkitSpeechRecognition) { setNotice("Voice transcription is unavailable here. Typed input is ready."); return; } const recognition = new host.webkitSpeechRecognition(); recognition.lang = "en-US"; recognition.interimResults = true; recognition.onresult = (e) => setMessage(Array.from(e.results).map((result) => result[0].transcript).join("")); recognition.onend = () => { setListening(false); setNotice("Transcript ready to review. Audio was discarded."); }; recognition.onerror = () => { setListening(false); setNotice("Transcription failed. Audio was discarded; retry or type instead."); }; recognition.start(); setListening(true); setNotice("Listening… provisional transcript will appear below."); }; const verifyChain = () => { const valid = project.events.every((event, index) => event.previousHash === (index ? project.events[index - 1].hash : null)); setNotice(valid ? `History verified: ${project.events.length} linked events.` : "History needs repair. Your writing remains safely autosaved."); }; const createNativeProject = async () => { try { const folder = await invokeNative("choose_project_folder"); if (!folder) { setNotice("Choose a folder when you are ready to create the desktop project."); return; } const result = await invokeNative<{ path: string }>("create_project", { path: folder, title: project.title }); if (!result) { setNotice("Project folders are available in the installed desktop app."); return; } setProjectPath(result.path); await invokeNative("persist_state", { appState: project }); setNotice("Self-contained project created and ready."); } catch (error) { setNotice(`Project could not be created: ${String(error)}`); } }; const openNativeProject = async () => { try { const folder = await invokeNative("choose_project_folder"); if (!folder) return; const result = await invokeNative<{ path: string }>("open_project", { path: folder }); if (!result) { setNotice("Opening project folders is available in the installed desktop app."); return; } setProjectPath(result.path); const restored = await invokeNative("load_project_state"); if (restored) setProject(restored); setNotice("Project reopened and its history verified."); } catch (error) { setNotice(`Project could not be opened: ${String(error)}`); } }; if (!hydrated) return
T

Gathering your threads…

; return
Skip to workspace
{project.title}{project.subtitle}
{project.privacy}Saved
{notice}
{view === "ideation" &&
Open thread

Explore the thought

{(["Gentle", "Balanced", "Rigorous"] as const).map((level) => )}
Today · Session 1
{project.turns.map((turn) =>
{turn.speaker === "user" ? "You" : "Thinkloom"}

{turn.text}

{turn.speaker === "user" && }
)}{busy &&
Thinkloom

}
{listening &&
Listening — audio stays in memory and will be discarded
}