const form = document.querySelector("#turn-form"); const atlasView = document.querySelector("#atlas-view"); const advisorView = document.querySelector("#advisor-view"); const openAdvisorButton = document.querySelector("#open-advisor"); const openAtlasButton = document.querySelector("#open-atlas"); const refreshDashboardButton = document.querySelector("#refresh-dashboard"); const atlasStatusEl = document.querySelector("#atlas-status"); const atlasSearchForm = document.querySelector("#atlas-search-form"); const atlasSearchInput = document.querySelector("#atlas-search"); const atlasSearchClearButton = document.querySelector("#atlas-search-clear"); const atlasSearchSectionEl = document.querySelector("#atlas-search-section"); const atlasSearchSummaryEl = document.querySelector("#atlas-search-summary"); const atlasSearchResultsEl = document.querySelector("#atlas-search-results"); const atlasStatsEl = document.querySelector("#atlas-stats"); const atlasClustersEl = document.querySelector("#atlas-clusters"); const atlasQuestsEl = document.querySelector("#atlas-quests"); const atlasSvgEl = document.querySelector("#atlas-svg"); const atlasDetailEl = document.querySelector("#atlas-detail"); const atlasReportEl = document.querySelector("#atlas-report"); const atlasRefreshProgressEl = document.querySelector("#atlas-refresh-progress"); const input = document.querySelector("#message"); const submit = document.querySelector("#submit"); const ink = document.querySelector("#ink"); const corrections = document.querySelector("#corrections"); const projectsEl = document.querySelector("#projects"); const whitespaceEl = document.querySelector("#whitespace"); const ideasEl = document.querySelector("#ideas"); const goalsEl = document.querySelector("#goals"); const profileEl = document.querySelector("#profile"); const woodMapEl = document.querySelector("#wood-map"); const scoreEl = document.querySelector("#score"); const planEl = document.querySelector("#plan"); const provenanceEl = document.querySelector("#provenance"); const verdictEl = document.querySelector("#verdict"); const overallEl = document.querySelector("#overall"); const sealEl = document.querySelector("#seal"); const sealVerdictEl = document.querySelector("#seal-verdict"); const sealCopyEl = document.querySelector("#seal-copy"); const verdictStampEl = document.querySelector("#verdict-stamp"); const spreadEl = document.querySelector("#spread"); const ideaCountEl = document.querySelector("#idea-count"); const goalCountEl = document.querySelector("#goal-count"); const demoButton = document.querySelector("#load-demo"); const exportButton = document.querySelector("#export-artifact"); const exportNotesButton = document.querySelector("#export-notes"); const exportChapterButton = document.querySelector("#export-chapter"); const resetButton = document.querySelector("#reset-session"); const recordVoiceButton = document.querySelector("#record-voice"); const uploadVoiceButton = document.querySelector("#upload-voice"); const voiceFileInput = document.querySelector("#voice-file"); const turnProgressEl = document.querySelector("#turn-progress"); const turnStageIconEl = document.querySelector("#turn-stage-icon"); const turnStageTextEl = document.querySelector("#turn-stage-text"); const turnTokensEl = document.querySelector("#turn-tokens"); const turnEtaEl = document.querySelector("#turn-eta"); const turnBarFillEl = document.querySelector("#turn-bar-fill"); const toolChipsEl = document.querySelector("#tool-chips"); const SESSION_STORAGE_KEY = "hackathon-advisor-session-v2"; const STAGE_ICONS = { planning: "🪶", running_tool: "🔧", writing: "✍️" }; const FIELD_NOTES_FILENAME = "hackathon-advisor-field-notes.md"; const CHAPTER_FILENAME = "hackathon-advisor-chapter.md"; const PNG_EXPORT_LABEL = "PNG"; let session = {}; let currentArtifact = null; let goalOptions = []; let goalProfiles = []; let goalProfileById = new Map(); let profileFields = []; let turnWatchdog = null; let sawTurnToken = false; let bootstrapData = null; let sessionRevision = 0; let sessionControlsLocked = false; let voiceBusy = false; let voiceRecorder = null; let voiceStream = null; let voiceChunks = []; let voiceRecordingState = "idle"; let decodeStartedAt = 0; let turnProgressTimer = null; let dashboardData = null; const SELF_PROJECT_ID = "build-small-hackathon/hackathon-advisor"; let selectedClusterId = ""; let selectedQuestId = ""; let selectedProjectId = ""; let dashboardRefreshTimer = null; let atlasSearchQuery = ""; let atlasSearchResults = []; let atlasSearchResultIds = new Set(); let atlasSearchTimer = null; let atlasSearchController = null; let atlasSearchUnavailable = false; let atlasSearchBusy = false; setVoiceRecordingState("idle"); setupViewRouting(); loadDashboard().catch(handleDashboardError); bootstrap().catch(handleBootstrapError); form.addEventListener("submit", async (event) => { event.preventDefault(); if (sessionControlsLocked || submit.disabled || input.disabled) return; const message = input.value.trim(); if (!message) return; await runTurn(message); }); input.addEventListener("keydown", (event) => { if (event.key !== "Enter" || event.shiftKey) return; event.preventDefault(); form.requestSubmit(); }); document.querySelectorAll(".mobile-nav [data-tab]").forEach((button) => { button.addEventListener("click", () => setActiveTab(button.dataset.tab || "page")); }); document.querySelectorAll("[data-command]").forEach((button) => { button.addEventListener("click", async () => { await runCommand(button.dataset.command || ""); }); }); setupMenus(); demoButton.addEventListener("click", async () => { await loadDemoSession(); }); exportButton.addEventListener("click", () => { if (!currentArtifact) return; exportArtifact(currentArtifact); }); exportNotesButton.addEventListener("click", () => exportNotes()); exportChapterButton.addEventListener("click", () => exportChapter()); resetButton.addEventListener("click", () => { resetSession(); }); openAdvisorButton?.addEventListener("click", () => { window.location.hash = "advisor"; }); openAtlasButton?.addEventListener("click", () => { window.location.hash = "atlas"; }); refreshDashboardButton?.addEventListener("click", async () => { await startDashboardRefresh(); }); atlasSearchForm?.addEventListener("submit", (event) => { event.preventDefault(); runAtlasSearch(atlasSearchInput?.value || ""); }); atlasSearchInput?.addEventListener("input", () => { scheduleAtlasSearch(atlasSearchInput.value || ""); }); atlasSearchClearButton?.addEventListener("click", () => { clearAtlasSearch(); }); recordVoiceButton.addEventListener("click", async () => { await toggleVoiceRecording(); }); uploadVoiceButton.addEventListener("click", () => { if (uploadVoiceButton.disabled || voiceBusy || sessionControlsLocked || voiceRecordingState !== "idle") return; voiceFileInput.click(); }); voiceFileInput.addEventListener("change", async () => { const file = voiceFileInput.files?.[0] || null; voiceFileInput.value = ""; if (!file) return; await transcribeVoiceBlob(file, file.name || "voice-note.audio"); }); goalsEl.addEventListener("change", (event) => { const target = event.target; if (!(target instanceof HTMLInputElement) || !target.dataset.goal) return; bumpSessionRevision(); const checked = new Set( Array.from(goalsEl.querySelectorAll("input[data-goal]:checked")).map((input) => input.dataset.goal), ); session.goals = goalOptions.filter((option) => checked.has(option)); syncCurrentIdeaGoals(); invalidateCurrentSeal("Goals updated. Press Ink or Plan to refresh the score."); saveSession(); renderGoals(session.goals); renderIdeas(session.ideas || []); }); profileEl.addEventListener("input", (event) => { const target = event.target; if (!(target instanceof HTMLInputElement) || !target.dataset.profileField) return; bumpSessionRevision(); const profile = { ...(session.profile || {}) }; const value = target.value.trim(); if (value) { profile[target.dataset.profileField] = value; } else { delete profile[target.dataset.profileField]; } session.profile = profile; invalidateCurrentPlan("Profile updated. Press Plan to refresh the build path."); saveSession(); }); ideasEl.addEventListener("click", (event) => { const card = event.target.closest("[data-idea-id]"); if (!(card instanceof HTMLElement) || !ideasEl.contains(card)) return; selectIdea(card.dataset.ideaId || ""); }); whitespaceEl.addEventListener("click", async (event) => { const card = event.target.closest("[data-gap-prompt]"); if (!(card instanceof HTMLButtonElement) || !whitespaceEl.contains(card)) return; if (card.disabled) return; await runTurn(card.dataset.gapPrompt || ""); }); function setupViewRouting() { window.addEventListener("hashchange", applyCurrentView); applyCurrentView(); } function applyCurrentView() { const view = window.location.hash.replace(/^#/, "") === "advisor" ? "advisor" : "atlas"; document.body.dataset.view = view; if (atlasView) atlasView.hidden = view !== "atlas"; if (advisorView) advisorView.hidden = view !== "advisor"; if (view === "advisor" && input && !sessionControlsLocked) { window.setTimeout(() => input.focus(), 30); } } async function loadDashboard() { const response = await fetch("/api/dashboard"); if (!response.ok) throw new Error(`dashboard failed with ${response.status}`); const data = await response.json(); dashboardData = data; renderDashboard(data); renderRefreshState(data.refresh || {}); if (data.refresh?.status === "running") scheduleRefreshPoll(); } function handleDashboardError(error) { console.error("Atlas could not load.", error); dashboardData = null; if (atlasStatusEl) atlasStatusEl.textContent = "Atlas could not load."; if (atlasSvgEl) atlasSvgEl.innerHTML = ""; if (atlasStatsEl) atlasStatsEl.innerHTML = ""; if (atlasDetailEl) atlasDetailEl.innerHTML = `
Reload the page to try again.
`; } async function startDashboardRefresh() { if (!refreshDashboardButton || refreshDashboardButton.disabled) return; refreshDashboardButton.disabled = true; if (atlasStatusEl) atlasStatusEl.textContent = "Starting refresh."; try { const response = await fetch("/api/dashboard/refresh", { method: "POST" }); const data = await response.json(); if (!response.ok) throw new Error(data.detail || `refresh failed with ${response.status}`); renderRefreshState(data); scheduleRefreshPoll(); } catch (error) { console.error("Dashboard refresh could not start.", error); if (atlasStatusEl) atlasStatusEl.textContent = "Refresh could not start."; if (atlasRefreshProgressEl) atlasRefreshProgressEl.hidden = true; refreshDashboardButton.disabled = false; } } function scheduleRefreshPoll() { if (dashboardRefreshTimer) window.clearTimeout(dashboardRefreshTimer); dashboardRefreshTimer = window.setTimeout(pollDashboardRefresh, 1400); } async function pollDashboardRefresh() { try { const response = await fetch("/api/dashboard/refresh"); if (!response.ok) throw new Error(`refresh status failed with ${response.status}`); const state = await response.json(); renderRefreshState(state); if (state.status === "running") { scheduleRefreshPoll(); return; } if (state.status === "succeeded") { await loadDashboard(); } } catch (error) { console.error("Dashboard refresh status unavailable.", error); if (atlasStatusEl) atlasStatusEl.textContent = "Refresh status unavailable."; } finally { if (_refreshIsSettled()) refreshDashboardButton.disabled = false; } } function _refreshIsSettled() { const status = String(dashboardData?.refresh?.status || ""); return status !== "running"; } function renderRefreshState(state) { if (dashboardData) dashboardData.refresh = state || {}; const status = String(state?.status || "idle"); const stage = state?.stage_label || state?.stage || ""; if (atlasStatusEl) { if (status === "running") { atlasStatusEl.textContent = stage ? `Refresh running: ${stage}.` : "Refresh running."; } else if (status === "succeeded") { atlasStatusEl.textContent = `Atlas refreshed: ${state.result?.project_count || "current"} projects mapped.`; } else if (status === "failed") { if (state.error) console.error("Dashboard refresh failed.", state.error); atlasStatusEl.textContent = "Refresh did not complete; current map is unchanged."; } else if (dashboardData) { atlasStatusEl.textContent = atlasSearchQuery ? atlasSearchStatusCopy() : atlasProvenanceCopy(dashboardData); } } if (atlasRefreshProgressEl) { const show = status === "running"; const cacheCopy = refreshQuestCacheCopy(state?.quest_cache || {}); atlasRefreshProgressEl.hidden = !show; atlasRefreshProgressEl.textContent = status === "running" ? `${stage || "Working"}${cacheCopy ? ` · ${cacheCopy}` : ""} · run ${state.run_id || ""}` : ""; } if (refreshDashboardButton) refreshDashboardButton.disabled = status === "running"; } function refreshQuestCacheCopy(cache) { const total = Number(cache.project_count || 0); if (!total) return ""; const hits = Number(cache.hit_count || 0); const misses = Number(cache.miss_count || 0); const analyzed = Number(cache.analyzed_count || 0); const remaining = Number(cache.remaining_count || 0); if (!hits && !misses && !analyzed) return ""; if (remaining > 0) return `${hits} cached, ${analyzed}/${misses} analyzed`; return `${hits} cached, ${analyzed} analyzed`; } function scheduleAtlasSearch(rawQuery) { const query = String(rawQuery || "").trim(); if (atlasSearchTimer) window.clearTimeout(atlasSearchTimer); if (!query) { clearAtlasSearch(); return; } atlasSearchTimer = window.setTimeout(() => runAtlasSearch(query), 260); } async function runAtlasSearch(rawQuery) { const query = String(rawQuery || "").trim(); if (!query) { clearAtlasSearch(); return; } atlasSearchQuery = query; atlasSearchUnavailable = false; atlasSearchBusy = true; renderAtlasSearch(); if (atlasSearchController) atlasSearchController.abort(); atlasSearchController = new AbortController(); try { const response = await fetch(`/api/dashboard/search?q=${encodeURIComponent(query)}&limit=12`, { signal: atlasSearchController.signal, }); if (!response.ok) throw new Error(`search failed with ${response.status}`); const payload = await response.json(); if (query !== String(atlasSearchInput?.value || "").trim()) return; atlasSearchResults = payload.results || []; atlasSearchResultIds = new Set(atlasSearchResults.map((result) => result.project_id).filter(Boolean)); atlasSearchUnavailable = false; atlasSearchBusy = false; if (atlasSearchResults.length) selectedProjectId = atlasSearchResults[0].project_id || selectedProjectId; if (dashboardData) renderDashboard(dashboardData); } catch (error) { if (error.name === "AbortError") return; console.error("Atlas search failed.", error); atlasSearchResults = []; atlasSearchResultIds = new Set(); atlasSearchUnavailable = true; atlasSearchBusy = false; if (dashboardData) renderDashboard(dashboardData); } } function clearAtlasSearch() { if (atlasSearchTimer) window.clearTimeout(atlasSearchTimer); atlasSearchTimer = null; if (atlasSearchController) atlasSearchController.abort(); atlasSearchController = null; atlasSearchQuery = ""; atlasSearchResults = []; atlasSearchResultIds = new Set(); atlasSearchUnavailable = false; atlasSearchBusy = false; if (atlasSearchInput) atlasSearchInput.value = ""; if (dashboardData) renderDashboard(dashboardData); } function atlasSearchStatusCopy() { if (!atlasSearchQuery) return dashboardData ? atlasProvenanceCopy(dashboardData) : ""; if (atlasSearchBusy) return "Searching."; if (atlasSearchUnavailable) return "Search unavailable."; if (!atlasSearchResults.length) return `No matches for "${atlasSearchQuery}".`; return `${atlasSearchResults.length} matches for "${atlasSearchQuery}".`; } function renderAtlasSearch() { if (!atlasSearchSectionEl || !atlasSearchResultsEl || !atlasSearchSummaryEl) return; const active = Boolean(atlasSearchQuery); atlasSearchSectionEl.hidden = !active; if (atlasSearchClearButton) atlasSearchClearButton.hidden = !active; if (!active) { atlasSearchResultsEl.innerHTML = ""; atlasSearchSummaryEl.textContent = ""; return; } atlasSearchSummaryEl.textContent = atlasSearchStatusCopy(); atlasSearchResultsEl.innerHTML = ""; if (atlasSearchUnavailable || !atlasSearchResults.length) return; for (const result of atlasSearchResults.slice(0, 8)) { atlasSearchResultsEl.append(atlasSearchResultButton(result)); } } function atlasSearchResultButton(result) { const button = document.createElement("button"); button.type = "button"; button.className = `atlas-search-result ${result.project_id === selectedProjectId ? "active" : ""}`; const title = result.title || result.project?.title || result.project_id || "Untitled project"; const terms = (result.matched_terms || []).slice(0, 4).join(", "); const snippet = (result.snippets || [])[0]; const width = Math.max(8, Math.min(100, Number(result.score || 0) * 100)).toFixed(0); button.innerHTML = ` ${escapeHtml(title)} ${ snippet ? `${escapeHtml(snippet.source)}: ${escapeHtml(snippet.text)}` : "" } `; button.addEventListener("click", () => { selectedProjectId = result.project_id || selectedProjectId; if (dashboardData) renderDashboard(dashboardData); }); return button; } function renderDashboard(data) { if (!data?.points?.length) { handleDashboardError(new Error("empty dashboard payload")); return; } if (!selectedProjectId) { const selfPoint = data.points.find((point) => point.id === SELF_PROJECT_ID); selectedProjectId = selfPoint?.id || mostLikedPoint(data.points)?.id || data.points[0].id; } renderAtlasStats(data); renderAtlasClusters(data); renderAtlasQuests(data); renderAtlasSvg(data); renderAtlasDetail(currentAtlasPoint(data)); renderAtlasReport(data); renderAtlasSearch(); if (atlasStatusEl) atlasStatusEl.textContent = atlasSearchQuery ? atlasSearchStatusCopy() : atlasProvenanceCopy(data); } function atlasProvenanceCopy(data) { const count = Number(data.project_count || data.points?.length || 0); const updated = shortDate(data.provenance?.snapshot_generated_at || data.generated_at); return `${count} projects mapped · ${data.layout?.algorithm || "layout"} · updated ${updated}`; } function renderAtlasStats(data) { if (!atlasStatsEl) return; const analyzed = data.quest_report?.status === "analyzed"; const questCount = (data.quest_report?.quests || []).filter((quest) => Number(quest.project_count || 0) > 0).length; atlasStatsEl.innerHTML = `Select a project dot to inspect its cluster and quest matches.
`; return; } const quests = (point.quest_matches || []) .map((match) => { const confidence = (Number(match.confidence) * 100).toFixed(0); const label = atlasQuestLabel(match.quest); const hint = questBadgeHint(match, label, confidence); return ( `` + `${escapeHtml(label)} ${confidence}%` ); }) .join(""); const tags = [...(point.models || []).slice(0, 3), ...visibleProjectTags(point.tags || []).slice(0, 3)] .map((tag) => `${escapeHtml(tag)}`) .join(""); atlasDetailEl.innerHTML = `${escapeHtml(point.summary)}
` : `${escapeHtml(point.id || "")}
`}${Number(point.likes || 0)} likes · ${escapeHtml(point.sdk || "unknown sdk")}
`; } function questBadgeHint(match, label, confidence) { const evidence = String(match?.evidence || "").trim(); const source = questEvidenceSourceLabel(match?.source); const parts = [`${label} ${confidence}% confidence`]; if (evidence) parts.push(`${source}: ${evidence}`); return parts.join(". "); } function questEvidenceSourceLabel(source) { const normalized = String(source || "").trim().toLowerCase(); if (normalized === "readme") return "README evidence"; if (normalized === "app_file") return "App file evidence"; return "Evidence"; } function visibleProjectTags(tags) { return (tags || []).filter((tag) => !String(tag || "").toLowerCase().startsWith("region:")); } function renderAtlasReport(data) { if (!atlasReportEl) return; const cluster = selectedClusterId ? (data.clusters || []).find((item) => item.id === selectedClusterId) : (data.clusters || [])[0]; if (!cluster) { atlasReportEl.innerHTML = `No cluster report is available.
`; return; } const projects = (cluster.representative_projects || []) .map( (project) => `` + `${escapeHtml(project.title || project.id)}
`, ) .join(""); atlasReportEl.innerHTML = `${Number(cluster.project_count || 0)} projects · ${escapeHtml( (cluster.keywords || []).join(", ") || "mixed signals", )}
${projects} `; } function svgEl(tagName) { return document.createElementNS("http://www.w3.org/2000/svg", tagName); } function svgTitle(text) { const title = svgEl("title"); title.textContent = text; return title; } function atlasColor(index) { const palette = [ "#9a2b22", "#b07d12", "#2f6b41", "#6f4b1d", "#3f8453", "#74201b", "#8a714c", "#d8a226", "#5d4528", "#7c6849", ]; return palette[index % palette.length]; } function atlasQuestLabel(questId) { const quest = (dashboardData?.quest_report?.quests || []).find((item) => item.id === questId); return quest?.label || questId; } function atlasPointRadius(point) { return atlasPointRadiusNumber(point).toFixed(3); } function atlasPointRadiusNumber(point) { return 0.62 + Math.min(0.72, Math.sqrt(Number(point.likes || 0)) * 0.12); } function atlasShortTitle(title) { const cleaned = String(title || "").trim(); return cleaned.length > 22 ? `${cleaned.slice(0, 20).trim()}...` : cleaned; } async function runTurn(message) { if (sessionControlsLocked) return false; bumpSessionRevision(); setActiveTab("page"); input.value = ""; submit.disabled = true; setCommandDisabled(true); setSessionControlsDisabled(true); ink.classList.remove("bleed", "gold"); corrections.textContent = ""; planEl.innerHTML = ""; delete session.ui_status; resetTurnProgress(); startTurnWatchdog(); let completed = false; try { const response = await fetch("/api/agent-turn", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message, session_json: JSON.stringify(session), }), }); if (!response.ok) throw new Error(`advisor failed with ${response.status}`); if (!response.body) throw new Error("advisor stream was empty"); for await (const raw of readNdjson(response.body)) { handleEvent(JSON.parse(raw)); } completed = true; } catch (error) { clearTurnWatchdog(); ink.textContent = `The advisor could not answer: ${error.message}`; ink.classList.remove("thinking"); ink.classList.add("bleed"); } finally { clearTurnWatchdog(); hideTurnProgress(); submit.disabled = false; setSessionControlsDisabled(false); setCommandDisabled(false); input.focus(); } return completed; } async function* readNdjson(stream) { const reader = stream.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let newlineIndex = buffer.indexOf("\n"); while (newlineIndex >= 0) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); if (line) yield line; newlineIndex = buffer.indexOf("\n"); } } buffer += decoder.decode(); const finalLine = buffer.trim(); if (finalLine) yield finalLine; } async function runCommand(command) { if (!command) return; const draft = input.value.trim(); if (draft) { const savedDraft = await runTurn(draft); if (!savedDraft) return; } await runTurn(command); } async function toggleVoiceRecording() { if (voiceRecordingState === "recording" && voiceRecorder?.state === "recording") { stopVoiceRecording(); return; } if (voiceRecordingState !== "idle") return; await startVoiceRecording(); } async function startVoiceRecording() { if (!bootstrapData || sessionControlsLocked || voiceBusy || voiceRecordingState !== "idle") return; if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) { setSessionStatus("Voice recording is not available in this browser. Upload a voice note instead."); return; } setVoiceRecordingState("starting"); submit.disabled = true; setCommandDisabled(true); try { voiceStream = await navigator.mediaDevices.getUserMedia({ audio: true }); voiceChunks = []; const mimeType = recordingMimeType(); voiceRecorder = new MediaRecorder(voiceStream, mimeType ? { mimeType } : undefined); voiceRecorder.addEventListener("dataavailable", (event) => { if (event.data?.size) voiceChunks.push(event.data); }); voiceRecorder.addEventListener("stop", () => { const recorderMimeType = voiceRecorder?.mimeType || mimeType || "audio/webm"; const recordedChunks = voiceChunks; stopVoiceStream(); const extension = recorderMimeType.includes("mp4") ? "m4a" : recorderMimeType.includes("ogg") ? "ogg" : "webm"; const blob = new Blob(recordedChunks, { type: recorderMimeType }); voiceRecorder = null; voiceChunks = []; if (!blob.size) { setVoiceRecordingState("idle"); submit.disabled = false; setCommandDisabled(false); setSessionStatus("Voice note is empty."); return; } setVoiceRecordingState("transcribing"); transcribeVoiceBlob(blob, `recorded-idea.${extension}`); }); voiceRecorder.start(); setVoiceRecordingState("recording"); setSessionStatus("Listening. Press Stop when your idea is ready."); } catch (error) { stopVoiceStream(); voiceRecorder = null; voiceChunks = []; setVoiceRecordingState("idle"); submit.disabled = false; setCommandDisabled(false); setSessionStatus(`Voice recording could not start: ${error.message}`); } } function stopVoiceRecording() { if (!voiceRecorder || voiceRecorder.state !== "recording") return; setVoiceRecordingState("stopping"); setSessionStatus("Stopping recording."); try { voiceRecorder.stop(); } catch (error) { stopVoiceStream(); voiceRecorder = null; voiceChunks = []; setVoiceRecordingState("idle"); submit.disabled = false; setCommandDisabled(false); setSessionStatus(`Voice recording could not stop: ${error.message}`); } } function recordingMimeType() { const candidates = ["audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4"]; return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || ""; } function stopVoiceStream() { if (!voiceStream) return; voiceStream.getTracks().forEach((track) => track.stop()); voiceStream = null; } async function transcribeVoiceBlob(blob, filename) { if (sessionControlsLocked || voiceBusy) return false; if (voiceRecordingState !== "idle" && voiceRecordingState !== "transcribing") return false; if (!blob?.size) { setSessionStatus("Voice note is empty."); return false; } const revision = bumpSessionRevision(); voiceBusy = true; setVoiceRecordingState("transcribing"); submit.disabled = true; input.disabled = true; setCommandDisabled(true); setSessionControlsDisabled(true); setSessionStatus("Transcribing voice note."); try { const formData = new FormData(); formData.append("audio", blob, filename || "voice-note.audio"); const response = await fetch("/api/transcribe", { method: "POST", body: formData, }); if (!response.ok) throw new Error(`voice note failed with ${response.status}`); const data = await response.json(); const transcript = String(data.transcript || "").trim(); if (!transcript) throw new Error("empty transcript"); if (!isCurrentSessionRevision(revision)) return false; input.value = mergeDraftWithTranscript(input.value, transcript); session.ui_status = "Voice note transcribed. Edit the draft or press Ink."; corrections.textContent = session.ui_status; saveSession(); return true; } catch (error) { if (isCurrentSessionRevision(revision)) setSessionStatus(`Voice note could not be transcribed: ${error.message}`); return false; } finally { voiceBusy = false; setVoiceRecordingState("idle"); if (isCurrentSessionRevision(revision)) { submit.disabled = false; input.disabled = false; setSessionControlsDisabled(false); setCommandDisabled(false); input.focus(); } } } function mergeDraftWithTranscript(draft, transcript) { const current = String(draft || "").trim(); return current ? `${current}\n${transcript}` : transcript; } async function bootstrap() { const response = await fetch("/api/bootstrap"); if (!response.ok) throw new Error(`project index failed with ${response.status}`); const data = await response.json(); bootstrapData = data; const rawProfiles = Array.isArray(data.goal_profiles) ? data.goal_profiles : []; const rawOptions = Array.isArray(data.goal_options) ? data.goal_options : []; goalProfiles = normalizeGoalProfiles(rawProfiles, rawOptions); goalOptions = goalProfiles.map((goal) => goal.id); goalProfileById = new Map(goalProfiles.map((goal) => [goal.id, goal])); profileFields = data.profile_fields || []; session = normalizeSession(readSavedSession(), defaultSession(data)); renderProvenance(data); renderGoals(session.goals); renderProfile(session.profile); renderRestoredSession(data); renderWhitespace(data.whitespace || []); setVoiceRecordingState("idle"); } function handleBootstrapError(error) { bootstrapData = null; currentArtifact = null; session = {}; submit.disabled = true; input.disabled = true; setCommandDisabled(true); setSessionControlsDisabled(true); ink.textContent = `The project index could not be opened: ${error.message}`; ink.classList.remove("thinking", "gold"); ink.classList.add("bleed"); corrections.textContent = "Reload the page to try again."; provenanceEl.textContent = "index unavailable"; renderScore(null); setVerdictDisplay("INDEX CLOSED", 0, null); renderWoodMap(null); renderGoals([]); renderProfile({}); renderIdeas([]); renderProjects([]); renderWhitespace([]); renderPlan([]); } function defaultSession(data = bootstrapData) { return { profile: {}, goals: data?.default_goals || goalOptions.slice(0, 3), }; } function setActiveTab(tab) { if (!spreadEl) return; const next = ["page", "proof", "almanac"].includes(tab) ? tab : "page"; spreadEl.dataset.tab = next; document.querySelectorAll(".mobile-nav [data-tab]").forEach((button) => { button.classList.toggle("active", button.dataset.tab === next); }); } function setVerdictDisplay(verdict = "READY", overall = 0, score = null) { const text = String(verdict || "READY"); const isEcho = text.startsWith("ECHO"); const isUnwritten = text.startsWith("UNWRITTEN"); const numericOverall = Number(overall || score?.overall || 0); verdictEl.textContent = text; overallEl.textContent = numericOverall.toFixed(1); sealEl.classList.toggle("echo", isEcho); sealEl.classList.toggle("unwritten", isUnwritten); sealVerdictEl.textContent = text; sealVerdictEl.classList.toggle("echo", isEcho); sealVerdictEl.classList.toggle("unwritten", isUnwritten); sealVerdictEl.classList.toggle("ready", !isEcho && !isUnwritten); verdictStampEl.classList.toggle("verdict-echo", isEcho); verdictStampEl.classList.toggle("verdict-unwritten", isUnwritten); verdictStampEl.classList.toggle("verdict-ready", !isEcho && !isUnwritten); if (!score) { sealCopyEl.textContent = text === "INDEX CLOSED" ? "The project map did not load." : "No idea has been scored yet."; } else if (isEcho) { sealCopyEl.textContent = "Nearby projects already cover parts of this idea."; } else { sealCopyEl.textContent = "This idea sits in a quieter part of the current map."; } } function bumpSessionRevision() { sessionRevision += 1; return sessionRevision; } function isCurrentSessionRevision(revision) { return revision === sessionRevision; } function restoreExportButtonLabels() { setActionButtonLabel(exportNotesButton, "Notes"); setActionButtonLabel(exportChapterButton, "Chapter"); setActionButtonLabel(exportButton, PNG_EXPORT_LABEL); } function actionButtonLabel(button) { return button?.dataset.actionLabel || button?.textContent.trim() || ""; } function setActionButtonLabel(button, label) { if (!button) return; button.dataset.actionLabel = label; const textNode = Array.from(button.childNodes).find( (node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim(), ); if (textNode) { textNode.textContent = ` ${label}`; } else { button.append(document.createTextNode(` ${label}`)); } } function setSessionControlsDisabled(disabled) { sessionControlsLocked = disabled; goalsEl.querySelectorAll("input[data-goal]").forEach((target) => { target.disabled = disabled; }); profileEl.querySelectorAll("input[data-profile-field]").forEach((field) => { field.disabled = disabled; }); ideasEl.querySelectorAll("button[data-idea-id]").forEach((idea) => { idea.disabled = disabled; }); whitespaceEl.querySelectorAll("button[data-gap-prompt]").forEach((gap) => { gap.disabled = disabled; }); setVoiceControlsDisabled(disabled); } function setVoiceControlsDisabled(disabled) { const recording = voiceRecordingState === "recording" && voiceRecorder?.state === "recording"; const lockedForState = ["starting", "stopping", "transcribing"].includes(voiceRecordingState); recordVoiceButton.disabled = !bootstrapData || voiceBusy || lockedForState || (disabled && !recording); uploadVoiceButton.disabled = !bootstrapData || voiceBusy || disabled || voiceRecordingState !== "idle"; } function setVoiceRecordingState(state) { voiceRecordingState = state; recordVoiceButton.dataset.voiceState = state; recordVoiceButton.classList.toggle("recording", state === "recording"); recordVoiceButton.setAttribute("aria-pressed", state === "recording" ? "true" : "false"); const labels = { idle: "Speak", starting: "Starting...", recording: "Stop", stopping: "Stopping...", transcribing: "Hearing...", }; setActionButtonLabel(recordVoiceButton, labels[state] || "Speak"); setVoiceControlsDisabled(sessionControlsLocked); } function resetSession() { if (!bootstrapData) return; bumpSessionRevision(); clearTurnWatchdog(); clearSavedSession(); session = defaultSession(bootstrapData); currentArtifact = null; submit.disabled = false; input.disabled = false; setSessionControlsDisabled(false); input.value = ""; ink.textContent = "The book is open. Describe an idea to start a new page."; ink.classList.remove("thinking", "bleed", "gold"); corrections.textContent = "Session reset."; renderGoals(session.goals); renderProfile(session.profile); renderScore(null); setVerdictDisplay("READY", 0, null); renderWoodMap(null); renderIdeas([]); renderPlan([]); renderProjects([], "Score an idea to see nearby echoes."); renderWhitespace(bootstrapData.whitespace || []); restoreExportButtonLabels(); setCommandDisabled(false); saveSession(); input.focus(); } async function loadDemoSession() { bumpSessionRevision(); setActiveTab("page"); submit.disabled = true; setCommandDisabled(true); setSessionControlsDisabled(true); ink.classList.remove("bleed", "gold"); ink.classList.add("thinking"); ink.textContent = "Loading an example idea board."; corrections.textContent = ""; try { const response = await fetch("/api/demo-session"); if (!response.ok) throw new Error(`example session failed with ${response.status}`); applyDemoSession(await response.json()); } catch (error) { ink.textContent = `The example session could not be loaded: ${error.message}`; ink.classList.remove("thinking"); ink.classList.add("bleed"); } finally { submit.disabled = false; setSessionControlsDisabled(false); setCommandDisabled(false); input.focus(); } } function applyDemoSession(data) { session = data.session || {}; session.profile = session.profile || {}; session.goals = Array.isArray(session.goals) ? session.goals : []; session.last_response = data.response || session.last_response || ""; session.ui_status = "Example idea board loaded with a plan and share page."; currentArtifact = data.artifact || session.last_artifact || null; ink.textContent = data.response || "Example session loaded."; ink.classList.remove("thinking"); if (data.score) { setVerdictDisplay(data.score.verdict, data.score.overall, data.score); renderScore(data.score); ink.classList.toggle("bleed", data.score.verdict.startsWith("ECHO")); ink.classList.toggle("gold", data.score.verdict.startsWith("UNWRITTEN")); } renderGoals(session.goals); renderProfile(session.profile); renderIdeas(session.ideas || []); renderPlan(data.plan || session.last_plan || []); renderWhitespace(data.whitespace || []); if (currentArtifact?.wood_map) renderWoodMap(currentArtifact.wood_map); if (data.score?.echoes?.length) { renderCitations(data.score.echoes); } else { renderProjects(data.projects || []); } setCommandDisabled(false); corrections.textContent = session.ui_status; saveSession(); } function renderProvenance(data) { const projectCount = Number(data.project_count || data.top_projects?.length || 0); const countLabel = projectCount ? `${projectCount} project page${projectCount === 1 ? "" : "s"} mapped` : "Current project map loaded"; const updated = shortDate(data.snapshot_generated_at || data.index_generated_at); provenanceEl.textContent = `${countLabel} · updated ${updated}`; } function renderRestoredSession(data) { const restoredProjectReference = isProjectReferenceTool(session.last_tool_resolution?.call?.name || ""); if (restoredProjectReference) { currentArtifact = null; renderProjectReferenceState(); renderProjects(session.last_projects || [], "No project page matched this request."); renderIdeas(session.ideas || []); delete session.last_plan; renderPlan([]); setCommandDisabled(false); restoreSessionCopy(); return; } const idea = currentIdea(); const storedArtifact = session.last_artifact || null; currentArtifact = !idea || storedArtifact?.title === idea.title ? storedArtifact : null; const score = currentArtifact?.seal || idea?.score || null; if (score) { renderScore(score); const verdict = currentArtifact?.verdict || score.verdict || "UNWRITTEN"; setVerdictDisplay(verdict, currentArtifact?.overall || score.overall || 0, score); ink.classList.toggle("bleed", verdict.startsWith("ECHO")); ink.classList.toggle("gold", verdict.startsWith("UNWRITTEN")); renderWoodMap(currentArtifact?.wood_map || null); if (score.echoes?.length) { renderCitations(score.echoes); } else { renderProjects([]); } } else { renderScore(null); setVerdictDisplay("READY", 0, null); renderWoodMap(null); renderProjects([], "Score an idea to see nearby echoes."); } renderIdeas(session.ideas || []); renderPlan(session.last_plan || []); setCommandDisabled(false); restoreSessionCopy(); } function readSavedSession() { try { const raw = window.localStorage.getItem(SESSION_STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? parsed : null; } catch { return null; } } function normalizeSession(savedSession, defaultSession) { const normalized = { ...defaultSession }; if (!savedSession) return normalized; normalized.profile = savedSession.profile && typeof savedSession.profile === "object" ? savedSession.profile : {}; const savedGoals = Array.isArray(savedSession.goals) ? savedSession.goals : defaultSession.goals; normalized.goals = goalOptions.filter((option) => savedGoals.includes(option)); if (!normalized.goals.length && defaultSession.goals?.length) normalized.goals = [...defaultSession.goals]; if (Array.isArray(savedSession.ideas)) normalized.ideas = savedSession.ideas; if (Array.isArray(savedSession.trace)) normalized.trace = savedSession.trace; if (Array.isArray(savedSession.last_plan)) normalized.last_plan = savedSession.last_plan; if (savedSession.current_idea_id) normalized.current_idea_id = savedSession.current_idea_id; if (savedSession.current_whitespace) normalized.current_whitespace = savedSession.current_whitespace; if (savedSession.last_tool_resolution) normalized.last_tool_resolution = savedSession.last_tool_resolution; if (savedSession.last_artifact) normalized.last_artifact = savedSession.last_artifact; if (Array.isArray(savedSession.last_projects)) normalized.last_projects = savedSession.last_projects; if (typeof savedSession.last_response === "string") normalized.last_response = savedSession.last_response; if (typeof savedSession.ui_status === "string") normalized.ui_status = savedSession.ui_status; return normalized; } function restoreSessionCopy() { const response = typeof session.last_response === "string" ? session.last_response.trim() : ""; if (response) ink.textContent = response; const status = typeof session.ui_status === "string" ? session.ui_status.trim() : ""; if (status) corrections.textContent = status; } function normalizeGoalProfiles(profiles, options) { const byId = new Map( profiles .filter((profile) => profile && typeof profile.id === "string") .map((profile) => [ profile.id, { id: profile.id, label: String(profile.label || profile.id), description: String(profile.description || ""), }, ]), ); return options.map((id) => byId.get(id) || { id, label: id, description: "" }); } function saveSession() { try { window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session)); } catch { // Storage may be disabled in some embeds; the app still works in-memory. } } function clearSavedSession() { try { window.localStorage.removeItem(SESSION_STORAGE_KEY); } catch { // Nothing else to clear when storage is unavailable. } } function renderGoals(selectedGoals) { const selected = new Set(selectedGoals || []); if (goalCountEl) goalCountEl.textContent = selected.size; goalsEl.innerHTML = ""; if (!goalOptions.length) { goalsEl.innerHTML = `${escapeHtml((idea.pitch || "").slice(0, 120))}
${escapeHtml(verdict)} ${goals ? `${escapeHtml(goals)}` : ""} `; ideasEl.append(item); } } function visibleIdeas(ideas) { const currentId = session.current_idea_id; const current = currentId ? ideas.find((idea) => idea.id === currentId) : null; const remaining = ideas.filter((idea) => idea.id !== currentId).slice(-3).reverse(); return current ? [current, ...remaining] : ideas.slice(-4).reverse(); } function ideaCardAriaLabel(idea, score, verdict) { const title = String(idea?.title || "Untitled idea").trim() || "Untitled idea"; const parts = [`Select idea: ${title}`]; if (score) parts.push(`score ${score}`); if (verdict) parts.push(String(verdict)); return parts.join(", "); } function currentIdea() { const ideas = Array.isArray(session.ideas) ? session.ideas : []; return ideas.find((idea) => idea.id === session.current_idea_id) || ideas[ideas.length - 1] || null; } function selectIdea(ideaId) { if (!ideaId || !Array.isArray(session.ideas)) return; const idea = session.ideas.find((item) => item.id === ideaId); if (!idea) return; const changedIdea = idea.id !== session.current_idea_id; bumpSessionRevision(); session.current_idea_id = idea.id; if (Array.isArray(idea.goals) && idea.goals.length) { session.goals = goalOptions.filter((option) => idea.goals.includes(option)); } renderSelectedIdeaSeal(idea); renderSelectedIdeaArtifact(idea); renderGoals(session.goals || []); renderIdeas(session.ideas); if (changedIdea) delete session.last_plan; renderPlan(changedIdea ? [] : session.last_plan || []); session.ui_status = `selected: ${idea.title}`; corrections.textContent = session.ui_status; saveSession(); } function renderSelectedIdeaSeal(idea) { const score = idea?.score || null; if (!score) { renderScore(null); setVerdictDisplay("READY", 0, null); ink.classList.remove("bleed", "gold"); renderProjects([], "Score this idea to see nearby echoes."); return; } setVerdictDisplay(score.verdict || "DRAFT", score.overall || 0, score); renderScore(score); ink.classList.toggle("bleed", String(score.verdict || "").startsWith("ECHO")); ink.classList.toggle("gold", String(score.verdict || "").startsWith("UNWRITTEN")); if (score.echoes?.length) { renderCitations(score.echoes); } else { renderProjects([]); } } function renderSelectedIdeaArtifact(idea) { const artifact = idea.artifact || (session.last_artifact?.title === idea.title ? session.last_artifact : null); if (artifact) { currentArtifact = artifact; session.last_artifact = artifact; renderWoodMap(currentArtifact.wood_map || null); setCommandDisabled(false); return; } currentArtifact = null; renderWoodMap(null); setCommandDisabled(false); } function goalDisplayName(goal) { return goalProfileById.get(goal)?.label || goal; } function renderScore(score) { const rows = [ ["Original", score?.originality || 0], ["Delight", score?.delight || 0], ["AI Need", score?.ai_necessity || 0], ["Feasible", score?.feasibility || 0], ["Goal Fit", score?.goal_fit || 0], ]; scoreEl.innerHTML = rows .map( ([label, value]) => `${escapeHtml(project.summary)}
` : ""; item.innerHTML = `${escapeHtml(project.title || "Untitled project")}${summary}`; projectsEl.append(item); } } function renderCitations(echoes) { projectsEl.innerHTML = ""; if (!echoes.length) { projectsEl.innerHTML = `${escapeHtml(project.summary)}
` : ""; item.innerHTML = ` Page ${escapeHtml(echo.page_number || "?")} · ${escapeHtml(project.title || "Untitled project")} ${summary} ${Number(echo.score || 0).toFixed(3)} · ${escapeHtml(matched)} `; projectsEl.append(item); } } function renderWhitespace(items) { whitespaceEl.innerHTML = ""; if (!items.length) { whitespaceEl.innerHTML = `${escapeHtml(item.pitch)}
Use this direction `; whitespaceEl.append(gap); } } function renderPlan(steps) { planEl.innerHTML = ""; if (!steps.length) { planEl.innerHTML = `