CoolFace
Apppublic

build-small-hackathon/hackathon-advisor

sourceHugging Facemitupdated 3mo agoView on Hugging Face
16likes
app.js2757 linesDownload Raw Back to static
1const form = document.querySelector("#turn-form");2const atlasView = document.querySelector("#atlas-view");3const advisorView = document.querySelector("#advisor-view");4const openAdvisorButton = document.querySelector("#open-advisor");5const openAtlasButton = document.querySelector("#open-atlas");6const refreshDashboardButton = document.querySelector("#refresh-dashboard");7const atlasStatusEl = document.querySelector("#atlas-status");8const atlasSearchForm = document.querySelector("#atlas-search-form");9const atlasSearchInput = document.querySelector("#atlas-search");10const atlasSearchClearButton = document.querySelector("#atlas-search-clear");11const atlasSearchSectionEl = document.querySelector("#atlas-search-section");12const atlasSearchSummaryEl = document.querySelector("#atlas-search-summary");13const atlasSearchResultsEl = document.querySelector("#atlas-search-results");14const atlasStatsEl = document.querySelector("#atlas-stats");15const atlasClustersEl = document.querySelector("#atlas-clusters");16const atlasQuestsEl = document.querySelector("#atlas-quests");17const atlasSvgEl = document.querySelector("#atlas-svg");18const atlasDetailEl = document.querySelector("#atlas-detail");19const atlasReportEl = document.querySelector("#atlas-report");20const atlasRefreshProgressEl = document.querySelector("#atlas-refresh-progress");21const input = document.querySelector("#message");22const submit = document.querySelector("#submit");23const ink = document.querySelector("#ink");24const corrections = document.querySelector("#corrections");25const projectsEl = document.querySelector("#projects");26const whitespaceEl = document.querySelector("#whitespace");27const ideasEl = document.querySelector("#ideas");28const goalsEl = document.querySelector("#goals");29const profileEl = document.querySelector("#profile");30const woodMapEl = document.querySelector("#wood-map");31const scoreEl = document.querySelector("#score");32const planEl = document.querySelector("#plan");33const provenanceEl = document.querySelector("#provenance");34const verdictEl = document.querySelector("#verdict");35const overallEl = document.querySelector("#overall");36const sealEl = document.querySelector("#seal");37const sealVerdictEl = document.querySelector("#seal-verdict");38const sealCopyEl = document.querySelector("#seal-copy");39const verdictStampEl = document.querySelector("#verdict-stamp");40const spreadEl = document.querySelector("#spread");41const ideaCountEl = document.querySelector("#idea-count");42const goalCountEl = document.querySelector("#goal-count");43const demoButton = document.querySelector("#load-demo");44const exportButton = document.querySelector("#export-artifact");45const exportNotesButton = document.querySelector("#export-notes");46const exportChapterButton = document.querySelector("#export-chapter");47const resetButton = document.querySelector("#reset-session");48const recordVoiceButton = document.querySelector("#record-voice");49const uploadVoiceButton = document.querySelector("#upload-voice");50const voiceFileInput = document.querySelector("#voice-file");51const turnProgressEl = document.querySelector("#turn-progress");52const turnStageIconEl = document.querySelector("#turn-stage-icon");53const turnStageTextEl = document.querySelector("#turn-stage-text");54const turnTokensEl = document.querySelector("#turn-tokens");55const turnEtaEl = document.querySelector("#turn-eta");56const turnBarFillEl = document.querySelector("#turn-bar-fill");57const toolChipsEl = document.querySelector("#tool-chips");58 59const SESSION_STORAGE_KEY = "hackathon-advisor-session-v2";60const STAGE_ICONS = { planning: "đŸĒļ", running_tool: "🔧", writing: "âœī¸" };61const FIELD_NOTES_FILENAME = "hackathon-advisor-field-notes.md";62const CHAPTER_FILENAME = "hackathon-advisor-chapter.md";63const PNG_EXPORT_LABEL = "PNG";64 65let session = {};66let currentArtifact = null;67let goalOptions = [];68let goalProfiles = [];69let goalProfileById = new Map();70let profileFields = [];71let turnWatchdog = null;72let sawTurnToken = false;73let bootstrapData = null;74let sessionRevision = 0;75let sessionControlsLocked = false;76let voiceBusy = false;77let voiceRecorder = null;78let voiceStream = null;79let voiceChunks = [];80let voiceRecordingState = "idle";81let decodeStartedAt = 0;82let turnProgressTimer = null;83let dashboardData = null;84const SELF_PROJECT_ID = "build-small-hackathon/hackathon-advisor";85let selectedClusterId = "";86let selectedQuestId = "";87let selectedProjectId = "";88let dashboardRefreshTimer = null;89let atlasSearchQuery = "";90let atlasSearchResults = [];91let atlasSearchResultIds = new Set();92let atlasSearchTimer = null;93let atlasSearchController = null;94let atlasSearchUnavailable = false;95let atlasSearchBusy = false;96 97setVoiceRecordingState("idle");98setupViewRouting();99loadDashboard().catch(handleDashboardError);100bootstrap().catch(handleBootstrapError);101 102form.addEventListener("submit", async (event) => {103  event.preventDefault();104  if (sessionControlsLocked || submit.disabled || input.disabled) return;105  const message = input.value.trim();106  if (!message) return;107  await runTurn(message);108});109 110input.addEventListener("keydown", (event) => {111  if (event.key !== "Enter" || event.shiftKey) return;112  event.preventDefault();113  form.requestSubmit();114});115 116document.querySelectorAll(".mobile-nav [data-tab]").forEach((button) => {117  button.addEventListener("click", () => setActiveTab(button.dataset.tab || "page"));118});119 120document.querySelectorAll("[data-command]").forEach((button) => {121  button.addEventListener("click", async () => {122    await runCommand(button.dataset.command || "");123  });124});125 126setupMenus();127 128demoButton.addEventListener("click", async () => {129  await loadDemoSession();130});131 132exportButton.addEventListener("click", () => {133  if (!currentArtifact) return;134  exportArtifact(currentArtifact);135});136 137exportNotesButton.addEventListener("click", () => exportNotes());138 139exportChapterButton.addEventListener("click", () => exportChapter());140 141resetButton.addEventListener("click", () => {142  resetSession();143});144 145openAdvisorButton?.addEventListener("click", () => {146  window.location.hash = "advisor";147});148 149openAtlasButton?.addEventListener("click", () => {150  window.location.hash = "atlas";151});152 153refreshDashboardButton?.addEventListener("click", async () => {154  await startDashboardRefresh();155});156 157atlasSearchForm?.addEventListener("submit", (event) => {158  event.preventDefault();159  runAtlasSearch(atlasSearchInput?.value || "");160});161 162atlasSearchInput?.addEventListener("input", () => {163  scheduleAtlasSearch(atlasSearchInput.value || "");164});165 166atlasSearchClearButton?.addEventListener("click", () => {167  clearAtlasSearch();168});169 170recordVoiceButton.addEventListener("click", async () => {171  await toggleVoiceRecording();172});173 174uploadVoiceButton.addEventListener("click", () => {175  if (uploadVoiceButton.disabled || voiceBusy || sessionControlsLocked || voiceRecordingState !== "idle") return;176  voiceFileInput.click();177});178 179voiceFileInput.addEventListener("change", async () => {180  const file = voiceFileInput.files?.[0] || null;181  voiceFileInput.value = "";182  if (!file) return;183  await transcribeVoiceBlob(file, file.name || "voice-note.audio");184});185 186goalsEl.addEventListener("change", (event) => {187  const target = event.target;188  if (!(target instanceof HTMLInputElement) || !target.dataset.goal) return;189  bumpSessionRevision();190  const checked = new Set(191    Array.from(goalsEl.querySelectorAll("input[data-goal]:checked")).map((input) => input.dataset.goal),192  );193  session.goals = goalOptions.filter((option) => checked.has(option));194  syncCurrentIdeaGoals();195  invalidateCurrentSeal("Goals updated. Press Ink or Plan to refresh the score.");196  saveSession();197  renderGoals(session.goals);198  renderIdeas(session.ideas || []);199});200 201profileEl.addEventListener("input", (event) => {202  const target = event.target;203  if (!(target instanceof HTMLInputElement) || !target.dataset.profileField) return;204  bumpSessionRevision();205  const profile = { ...(session.profile || {}) };206  const value = target.value.trim();207  if (value) {208    profile[target.dataset.profileField] = value;209  } else {210    delete profile[target.dataset.profileField];211  }212  session.profile = profile;213  invalidateCurrentPlan("Profile updated. Press Plan to refresh the build path.");214  saveSession();215});216 217ideasEl.addEventListener("click", (event) => {218  const card = event.target.closest("[data-idea-id]");219  if (!(card instanceof HTMLElement) || !ideasEl.contains(card)) return;220  selectIdea(card.dataset.ideaId || "");221});222 223whitespaceEl.addEventListener("click", async (event) => {224  const card = event.target.closest("[data-gap-prompt]");225  if (!(card instanceof HTMLButtonElement) || !whitespaceEl.contains(card)) return;226  if (card.disabled) return;227  await runTurn(card.dataset.gapPrompt || "");228});229 230function setupViewRouting() {231  window.addEventListener("hashchange", applyCurrentView);232  applyCurrentView();233}234 235function applyCurrentView() {236  const view = window.location.hash.replace(/^#/, "") === "advisor" ? "advisor" : "atlas";237  document.body.dataset.view = view;238  if (atlasView) atlasView.hidden = view !== "atlas";239  if (advisorView) advisorView.hidden = view !== "advisor";240  if (view === "advisor" && input && !sessionControlsLocked) {241    window.setTimeout(() => input.focus(), 30);242  }243}244 245async function loadDashboard() {246  const response = await fetch("/api/dashboard");247  if (!response.ok) throw new Error(`dashboard failed with ${response.status}`);248  const data = await response.json();249  dashboardData = data;250  renderDashboard(data);251  renderRefreshState(data.refresh || {});252  if (data.refresh?.status === "running") scheduleRefreshPoll();253}254 255function handleDashboardError(error) {256  console.error("Atlas could not load.", error);257  dashboardData = null;258  if (atlasStatusEl) atlasStatusEl.textContent = "Atlas could not load.";259  if (atlasSvgEl) atlasSvgEl.innerHTML = "";260  if (atlasStatsEl) atlasStatsEl.innerHTML = "";261  if (atlasDetailEl) atlasDetailEl.innerHTML = `<p>Reload the page to try again.</p>`;262}263 264async function startDashboardRefresh() {265  if (!refreshDashboardButton || refreshDashboardButton.disabled) return;266  refreshDashboardButton.disabled = true;267  if (atlasStatusEl) atlasStatusEl.textContent = "Starting refresh.";268  try {269    const response = await fetch("/api/dashboard/refresh", { method: "POST" });270    const data = await response.json();271    if (!response.ok) throw new Error(data.detail || `refresh failed with ${response.status}`);272    renderRefreshState(data);273    scheduleRefreshPoll();274  } catch (error) {275    console.error("Dashboard refresh could not start.", error);276    if (atlasStatusEl) atlasStatusEl.textContent = "Refresh could not start.";277    if (atlasRefreshProgressEl) atlasRefreshProgressEl.hidden = true;278    refreshDashboardButton.disabled = false;279  }280}281 282function scheduleRefreshPoll() {283  if (dashboardRefreshTimer) window.clearTimeout(dashboardRefreshTimer);284  dashboardRefreshTimer = window.setTimeout(pollDashboardRefresh, 1400);285}286 287async function pollDashboardRefresh() {288  try {289    const response = await fetch("/api/dashboard/refresh");290    if (!response.ok) throw new Error(`refresh status failed with ${response.status}`);291    const state = await response.json();292    renderRefreshState(state);293    if (state.status === "running") {294      scheduleRefreshPoll();295      return;296    }297    if (state.status === "succeeded") {298      await loadDashboard();299    }300  } catch (error) {301    console.error("Dashboard refresh status unavailable.", error);302    if (atlasStatusEl) atlasStatusEl.textContent = "Refresh status unavailable.";303  } finally {304    if (_refreshIsSettled()) refreshDashboardButton.disabled = false;305  }306}307 308function _refreshIsSettled() {309  const status = String(dashboardData?.refresh?.status || "");310  return status !== "running";311}312 313function renderRefreshState(state) {314  if (dashboardData) dashboardData.refresh = state || {};315  const status = String(state?.status || "idle");316  const stage = state?.stage_label || state?.stage || "";317  if (atlasStatusEl) {318    if (status === "running") {319      atlasStatusEl.textContent = stage ? `Refresh running: ${stage}.` : "Refresh running.";320    } else if (status === "succeeded") {321      atlasStatusEl.textContent = `Atlas refreshed: ${state.result?.project_count || "current"} projects mapped.`;322    } else if (status === "failed") {323      if (state.error) console.error("Dashboard refresh failed.", state.error);324      atlasStatusEl.textContent = "Refresh did not complete; current map is unchanged.";325    } else if (dashboardData) {326      atlasStatusEl.textContent = atlasSearchQuery ? atlasSearchStatusCopy() : atlasProvenanceCopy(dashboardData);327    }328  }329  if (atlasRefreshProgressEl) {330    const show = status === "running";331    const cacheCopy = refreshQuestCacheCopy(state?.quest_cache || {});332    atlasRefreshProgressEl.hidden = !show;333    atlasRefreshProgressEl.textContent =334      status === "running"335        ? `${stage || "Working"}${cacheCopy ? ` ¡ ${cacheCopy}` : ""} ¡ run ${state.run_id || ""}`336        : "";337  }338  if (refreshDashboardButton) refreshDashboardButton.disabled = status === "running";339}340 341function refreshQuestCacheCopy(cache) {342  const total = Number(cache.project_count || 0);343  if (!total) return "";344  const hits = Number(cache.hit_count || 0);345  const misses = Number(cache.miss_count || 0);346  const analyzed = Number(cache.analyzed_count || 0);347  const remaining = Number(cache.remaining_count || 0);348  if (!hits && !misses && !analyzed) return "";349  if (remaining > 0) return `${hits} cached, ${analyzed}/${misses} analyzed`;350  return `${hits} cached, ${analyzed} analyzed`;351}352 353function scheduleAtlasSearch(rawQuery) {354  const query = String(rawQuery || "").trim();355  if (atlasSearchTimer) window.clearTimeout(atlasSearchTimer);356  if (!query) {357    clearAtlasSearch();358    return;359  }360  atlasSearchTimer = window.setTimeout(() => runAtlasSearch(query), 260);361}362 363async function runAtlasSearch(rawQuery) {364  const query = String(rawQuery || "").trim();365  if (!query) {366    clearAtlasSearch();367    return;368  }369  atlasSearchQuery = query;370  atlasSearchUnavailable = false;371  atlasSearchBusy = true;372  renderAtlasSearch();373  if (atlasSearchController) atlasSearchController.abort();374  atlasSearchController = new AbortController();375  try {376    const response = await fetch(`/api/dashboard/search?q=${encodeURIComponent(query)}&limit=12`, {377      signal: atlasSearchController.signal,378    });379    if (!response.ok) throw new Error(`search failed with ${response.status}`);380    const payload = await response.json();381    if (query !== String(atlasSearchInput?.value || "").trim()) return;382    atlasSearchResults = payload.results || [];383    atlasSearchResultIds = new Set(atlasSearchResults.map((result) => result.project_id).filter(Boolean));384    atlasSearchUnavailable = false;385    atlasSearchBusy = false;386    if (atlasSearchResults.length) selectedProjectId = atlasSearchResults[0].project_id || selectedProjectId;387    if (dashboardData) renderDashboard(dashboardData);388  } catch (error) {389    if (error.name === "AbortError") return;390    console.error("Atlas search failed.", error);391    atlasSearchResults = [];392    atlasSearchResultIds = new Set();393    atlasSearchUnavailable = true;394    atlasSearchBusy = false;395    if (dashboardData) renderDashboard(dashboardData);396  }397}398 399function clearAtlasSearch() {400  if (atlasSearchTimer) window.clearTimeout(atlasSearchTimer);401  atlasSearchTimer = null;402  if (atlasSearchController) atlasSearchController.abort();403  atlasSearchController = null;404  atlasSearchQuery = "";405  atlasSearchResults = [];406  atlasSearchResultIds = new Set();407  atlasSearchUnavailable = false;408  atlasSearchBusy = false;409  if (atlasSearchInput) atlasSearchInput.value = "";410  if (dashboardData) renderDashboard(dashboardData);411}412 413function atlasSearchStatusCopy() {414  if (!atlasSearchQuery) return dashboardData ? atlasProvenanceCopy(dashboardData) : "";415  if (atlasSearchBusy) return "Searching.";416  if (atlasSearchUnavailable) return "Search unavailable.";417  if (!atlasSearchResults.length) return `No matches for "${atlasSearchQuery}".`;418  return `${atlasSearchResults.length} matches for "${atlasSearchQuery}".`;419}420 421function renderAtlasSearch() {422  if (!atlasSearchSectionEl || !atlasSearchResultsEl || !atlasSearchSummaryEl) return;423  const active = Boolean(atlasSearchQuery);424  atlasSearchSectionEl.hidden = !active;425  if (atlasSearchClearButton) atlasSearchClearButton.hidden = !active;426  if (!active) {427    atlasSearchResultsEl.innerHTML = "";428    atlasSearchSummaryEl.textContent = "";429    return;430  }431  atlasSearchSummaryEl.textContent = atlasSearchStatusCopy();432  atlasSearchResultsEl.innerHTML = "";433  if (atlasSearchUnavailable || !atlasSearchResults.length) return;434  for (const result of atlasSearchResults.slice(0, 8)) {435    atlasSearchResultsEl.append(atlasSearchResultButton(result));436  }437}438 439function atlasSearchResultButton(result) {440  const button = document.createElement("button");441  button.type = "button";442  button.className = `atlas-search-result ${result.project_id === selectedProjectId ? "active" : ""}`;443  const title = result.title || result.project?.title || result.project_id || "Untitled project";444  const terms = (result.matched_terms || []).slice(0, 4).join(", ");445  const snippet = (result.snippets || [])[0];446  const width = Math.max(8, Math.min(100, Number(result.score || 0) * 100)).toFixed(0);447  button.innerHTML = `448    <strong>${escapeHtml(title)}</strong>449    <span class="atlas-search-meta">${escapeHtml(terms || "Related project")}</span>450    <span class="atlas-search-score" aria-hidden="true"><i style="width: ${width}%"></i></span>451    ${452      snippet453        ? `<span class="atlas-search-snippet">${escapeHtml(snippet.source)}: ${escapeHtml(snippet.text)}</span>`454        : ""455    }456  `;457  button.addEventListener("click", () => {458    selectedProjectId = result.project_id || selectedProjectId;459    if (dashboardData) renderDashboard(dashboardData);460  });461  return button;462}463 464function renderDashboard(data) {465  if (!data?.points?.length) {466    handleDashboardError(new Error("empty dashboard payload"));467    return;468  }469  if (!selectedProjectId) {470    const selfPoint = data.points.find((point) => point.id === SELF_PROJECT_ID);471    selectedProjectId = selfPoint?.id || mostLikedPoint(data.points)?.id || data.points[0].id;472  }473  renderAtlasStats(data);474  renderAtlasClusters(data);475  renderAtlasQuests(data);476  renderAtlasSvg(data);477  renderAtlasDetail(currentAtlasPoint(data));478  renderAtlasReport(data);479  renderAtlasSearch();480  renderChatActionChip(); // keep the chat's applied-filter chip in sync with manual clicks481  if (atlasStatusEl) atlasStatusEl.textContent = atlasSearchQuery ? atlasSearchStatusCopy() : atlasProvenanceCopy(data);482}483 484function atlasProvenanceCopy(data) {485  const count = Number(data.project_count || data.points?.length || 0);486  const updated = shortDate(data.provenance?.snapshot_generated_at || data.generated_at);487  return `${count} projects mapped ¡ ${data.layout?.algorithm || "layout"} ¡ updated ${updated}`;488}489 490function renderAtlasStats(data) {491  if (!atlasStatsEl) return;492  const analyzed = data.quest_report?.status === "analyzed";493  const questCount = (data.quest_report?.quests || []).filter((quest) => Number(quest.project_count || 0) > 0).length;494  atlasStatsEl.innerHTML = `495    <div class="atlas-stat"><strong>${Number(data.project_count || 0)}</strong><span>Projects</span></div>496    <div class="atlas-stat"><strong>${Number(data.clusters?.length || 0)}</strong><span>Clusters</span></div>497    <div class="atlas-stat"><strong>${Number(data.links?.length || 0)}</strong><span>Near links</span></div>498    <div class="atlas-stat"><strong>${analyzed ? questCount : "..."}</strong><span>Quest groups</span></div>499  `;500}501 502function renderAtlasClusters(data) {503  if (!atlasClustersEl) return;504  const allActive = !selectedClusterId;505  atlasClustersEl.innerHTML = "";506  atlasClustersEl.append(507    atlasFilterButton({508      label: "All clusters",509      meta: `${data.project_count || data.points.length} projects`,510      active: allActive,511      onClick: () => {512        selectedClusterId = "";513        renderDashboard(data);514      },515    }),516  );517  for (const cluster of data.clusters || []) {518    atlasClustersEl.append(519      atlasFilterButton({520        label: cluster.label || cluster.id,521        meta: `${cluster.project_count || 0} projects`,522        active: selectedClusterId === cluster.id,523        onClick: () => {524          selectedClusterId = selectedClusterId === cluster.id ? "" : cluster.id;525          renderDashboard(data);526        },527      }),528    );529  }530}531 532function renderAtlasQuests(data) {533  if (!atlasQuestsEl) return;534  const quests = data.quest_report?.quests || [];535  const analyzed = data.quest_report?.status === "analyzed";536  atlasQuestsEl.innerHTML = "";537  atlasQuestsEl.append(538    atlasFilterButton({539      label: "All quests",540      meta: analyzed ? "No quest filter" : "Refresh to analyze",541      active: !selectedQuestId,542      onClick: () => {543        selectedQuestId = "";544        renderDashboard(data);545      },546    }),547  );548  for (const quest of quests) {549    atlasQuestsEl.append(550      atlasFilterButton({551        label: quest.label || quest.id,552        meta: analyzed ? `${quest.project_count || 0} projects` : "Not analyzed",553        active: selectedQuestId === quest.id,554        onClick: () => {555          selectedQuestId = selectedQuestId === quest.id ? "" : quest.id;556          renderDashboard(data);557        },558      }),559    );560  }561}562 563function atlasFilterButton({ label, meta, active, onClick }) {564  const button = document.createElement("button");565  button.type = "button";566  button.className = `atlas-filter ${active ? "active" : ""}`;567  button.innerHTML = `<strong>${escapeHtml(label)}</strong><span>${escapeHtml(meta || "")}</span>`;568  button.addEventListener("click", onClick);569  return button;570}571 572function renderAtlasSvg(data) {573  if (!atlasSvgEl) return;574  atlasSvgEl.innerHTML = "";575  const pointsById = new Map((data.points || []).map((point) => [point.id, point]));576  const visible = new Set(visibleAtlasPoints(data).map((point) => point.id));577  const clusterIndex = new Map((data.clusters || []).map((cluster, index) => [cluster.id, index]));578 579  for (const link of data.links || []) {580    const source = pointsById.get(link.source);581    const target = pointsById.get(link.target);582    if (!source || !target) continue;583    const line = svgEl("line");584    line.setAttribute("x1", source.x);585    line.setAttribute("y1", source.y);586    line.setAttribute("x2", target.x);587    line.setAttribute("y2", target.y);588    line.setAttribute("class", `atlas-link ${visible.has(source.id) && visible.has(target.id) ? "" : "dim"}`);589    atlasSvgEl.append(line);590  }591 592  for (const point of data.points || []) {593    if (!atlasSearchResultIds.has(point.id)) continue;594    const ring = svgEl("circle");595    ring.setAttribute("cx", point.x);596    ring.setAttribute("cy", point.y);597    ring.setAttribute("r", (atlasPointRadiusNumber(point) + 0.62).toFixed(3));598    ring.setAttribute("class", `atlas-search-ring ${visible.has(point.id) ? "" : "dim"}`);599    atlasSvgEl.append(ring);600  }601 602  for (const point of data.points || []) {603    const circle = svgEl("circle");604    circle.setAttribute("cx", point.x);605    circle.setAttribute("cy", point.y);606    circle.setAttribute("r", atlasPointRadius(point));607    circle.setAttribute("fill", atlasColor(clusterIndex.get(point.cluster_id) || 0));608    circle.setAttribute(609      "class",610      `atlas-dot ${visible.has(point.id) ? "" : "dim"} ${point.id === selectedProjectId ? "selected" : ""} ${611        atlasSearchResultIds.has(point.id) ? "search-match" : ""612      }`,613    );614    circle.setAttribute("tabindex", "0");615    circle.setAttribute("role", "button");616    circle.setAttribute("aria-label", point.title || point.id);617    circle.addEventListener("mouseenter", () => renderAtlasDetail(point));618    circle.addEventListener("focus", () => renderAtlasDetail(point));619    circle.addEventListener("click", () => {620      selectedProjectId = point.id;621      renderDashboard(data);622    });623    circle.append(svgTitle(point.title || point.id));624    atlasSvgEl.append(circle);625  }626 627  for (const point of labelAtlasPoints(data)) {628    const text = svgEl("text");629    text.setAttribute("x", boundedPercent(point.x + 1.4));630    text.setAttribute("y", boundedPercent(point.y - 1.1));631    text.setAttribute("class", "atlas-label");632    text.textContent = atlasShortTitle(point.title || point.id);633    atlasSvgEl.append(text);634  }635}636 637function visibleAtlasPoints(data) {638  return (data.points || []).filter((point) => {639    const clusterMatch = !selectedClusterId || point.cluster_id === selectedClusterId;640    const questMatch = !selectedQuestId || (point.quest_ids || []).includes(selectedQuestId);641    const searchMatch = !atlasSearchQuery || atlasSearchResultIds.has(point.id);642    return clusterMatch && questMatch && searchMatch;643  });644}645 646function labelAtlasPoints(data) {647  if (atlasSearchQuery && atlasSearchResults.length) {648    const pointsById = new Map((data.points || []).map((point) => [point.id, point]));649    const visibleIds = new Set(visibleAtlasPoints(data).map((point) => point.id));650    return atlasSearchResults651      .map((result) => pointsById.get(result.project_id))652      .filter(Boolean)653      .filter((point) => visibleIds.has(point.id))654      .slice(0, 16);655  }656  const visible = visibleAtlasPoints(data);657  return [...visible].sort((left, right) => Number(right.likes || 0) - Number(left.likes || 0)).slice(0, 12);658}659 660function currentAtlasPoint(data) {661  return (data.points || []).find((point) => point.id === selectedProjectId) || mostLikedPoint(data.points || []);662}663 664function mostLikedPoint(points) {665  return [...(points || [])].sort((left, right) => Number(right.likes || 0) - Number(left.likes || 0))[0] || null;666}667 668function renderAtlasDetail(point) {669  if (!atlasDetailEl) return;670  if (!point) {671    atlasDetailEl.innerHTML = `<p>Select a project dot to inspect its cluster and quest matches.</p>`;672    return;673  }674  const quests = (point.quest_matches || [])675    .map((match) => {676      const confidence = (Number(match.confidence) * 100).toFixed(0);677      const label = atlasQuestLabel(match.quest);678      const hint = questBadgeHint(match, label, confidence);679      return (680        `<span title="${escapeAttribute(hint)}" aria-label="${escapeAttribute(hint)}">` +681        `${escapeHtml(label)} ${confidence}%</span>`682      );683    })684    .join("");685  const tags = [...(point.models || []).slice(0, 3), ...visibleProjectTags(point.tags || []).slice(0, 3)]686    .map((tag) => `<span>${escapeHtml(tag)}</span>`)687    .join("");688  atlasDetailEl.innerHTML = `689    <h2>${escapeHtml(point.title || "Untitled project")}</h2>690    ${point.summary ? `<p>${escapeHtml(point.summary)}</p>` : `<p>${escapeHtml(point.id || "")}</p>`}691    <p>${Number(point.likes || 0)} likes ¡ ${escapeHtml(point.sdk || "unknown sdk")}</p>692    <p><a href="${escapeAttribute(point.url || "#")}" target="_blank" rel="noreferrer">Open Space</a></p>693    <div class="atlas-tags">${quests || `<span>Quest analysis pending</span>`}</div>694    <div class="atlas-tags">${tags}</div>695  `;696}697 698function questBadgeHint(match, label, confidence) {699  const evidence = String(match?.evidence || "").trim();700  const source = questEvidenceSourceLabel(match?.source);701  const parts = [`${label} ${confidence}% confidence`];702  if (evidence) parts.push(`${source}: ${evidence}`);703  return parts.join(". ");704}705 706function questEvidenceSourceLabel(source) {707  const normalized = String(source || "").trim().toLowerCase();708  if (normalized === "readme") return "README evidence";709  if (normalized === "app_file") return "App file evidence";710  return "Evidence";711}712 713function visibleProjectTags(tags) {714  return (tags || []).filter((tag) => !String(tag || "").toLowerCase().startsWith("region:"));715}716 717function renderAtlasReport(data) {718  if (!atlasReportEl) return;719  const cluster = selectedClusterId720    ? (data.clusters || []).find((item) => item.id === selectedClusterId)721    : (data.clusters || [])[0];722  if (!cluster) {723    atlasReportEl.innerHTML = `<p>No cluster report is available.</p>`;724    return;725  }726  const projects = (cluster.representative_projects || [])727    .map(728      (project) =>729        `<p><a href="${escapeAttribute(project.url || "#")}" target="_blank" rel="noreferrer">` +730        `${escapeHtml(project.title || project.id)}</a></p>`,731    )732    .join("");733  atlasReportEl.innerHTML = `734    <h2>${escapeHtml(cluster.label || cluster.id)}</h2>735    <p>${Number(cluster.project_count || 0)} projects ¡ ${escapeHtml(736      (cluster.keywords || []).join(", ") || "mixed signals",737    )}</p>738    ${projects}739  `;740}741 742function svgEl(tagName) {743  return document.createElementNS("http://www.w3.org/2000/svg", tagName);744}745 746function svgTitle(text) {747  const title = svgEl("title");748  title.textContent = text;749  return title;750}751 752function atlasColor(index) {753  const palette = [754    "#9a2b22",755    "#b07d12",756    "#2f6b41",757    "#6f4b1d",758    "#3f8453",759    "#74201b",760    "#8a714c",761    "#d8a226",762    "#5d4528",763    "#7c6849",764  ];765  return palette[index % palette.length];766}767 768function atlasQuestLabel(questId) {769  const quest = (dashboardData?.quest_report?.quests || []).find((item) => item.id === questId);770  return quest?.label || questId;771}772 773function atlasPointRadius(point) {774  return atlasPointRadiusNumber(point).toFixed(3);775}776 777function atlasPointRadiusNumber(point) {778  return 0.62 + Math.min(0.72, Math.sqrt(Number(point.likes || 0)) * 0.12);779}780 781function atlasShortTitle(title) {782  const cleaned = String(title || "").trim();783  return cleaned.length > 22 ? `${cleaned.slice(0, 20).trim()}...` : cleaned;784}785 786async function runTurn(message) {787  if (sessionControlsLocked) return false;788  bumpSessionRevision();789  setActiveTab("page");790  input.value = "";791  submit.disabled = true;792  setCommandDisabled(true);793  setSessionControlsDisabled(true);794  ink.classList.remove("bleed", "gold");795  corrections.textContent = "";796  planEl.innerHTML = "";797  delete session.ui_status;798  resetTurnProgress();799  startTurnWatchdog();800 801  let completed = false;802  try {803    const response = await fetch("/api/agent-turn", {804      method: "POST",805      headers: { "Content-Type": "application/json" },806      body: JSON.stringify({807        message,808        session_json: JSON.stringify(session),809      }),810    });811    if (!response.ok) throw new Error(`advisor failed with ${response.status}`);812    if (!response.body) throw new Error("advisor stream was empty");813 814    for await (const raw of readNdjson(response.body)) {815      handleEvent(JSON.parse(raw));816    }817    completed = true;818  } catch (error) {819    clearTurnWatchdog();820    ink.textContent = `The advisor could not answer: ${error.message}`;821    ink.classList.remove("thinking");822    ink.classList.add("bleed");823  } finally {824    clearTurnWatchdog();825    hideTurnProgress();826    submit.disabled = false;827    setSessionControlsDisabled(false);828    setCommandDisabled(false);829    input.focus();830  }831  return completed;832}833 834async function* readNdjson(stream) {835  const reader = stream.getReader();836  const decoder = new TextDecoder();837  let buffer = "";838 839  while (true) {840    const { value, done } = await reader.read();841    if (done) break;842    buffer += decoder.decode(value, { stream: true });843    let newlineIndex = buffer.indexOf("\n");844    while (newlineIndex >= 0) {845      const line = buffer.slice(0, newlineIndex).trim();846      buffer = buffer.slice(newlineIndex + 1);847      if (line) yield line;848      newlineIndex = buffer.indexOf("\n");849    }850  }851 852  buffer += decoder.decode();853  const finalLine = buffer.trim();854  if (finalLine) yield finalLine;855}856 857async function runCommand(command) {858  if (!command) return;859  const draft = input.value.trim();860  if (draft) {861    const savedDraft = await runTurn(draft);862    if (!savedDraft) return;863  }864  await runTurn(command);865}866 867async function toggleVoiceRecording() {868  if (voiceRecordingState === "recording" && voiceRecorder?.state === "recording") {869    stopVoiceRecording();870    return;871  }872  if (voiceRecordingState !== "idle") return;873  await startVoiceRecording();874}875 876async function startVoiceRecording() {877  if (!bootstrapData || sessionControlsLocked || voiceBusy || voiceRecordingState !== "idle") return;878  if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) {879    setSessionStatus("Voice recording is not available in this browser. Upload a voice note instead.");880    return;881  }882  setVoiceRecordingState("starting");883  submit.disabled = true;884  setCommandDisabled(true);885  try {886    voiceStream = await navigator.mediaDevices.getUserMedia({ audio: true });887    voiceChunks = [];888    const mimeType = recordingMimeType();889    voiceRecorder = new MediaRecorder(voiceStream, mimeType ? { mimeType } : undefined);890    voiceRecorder.addEventListener("dataavailable", (event) => {891      if (event.data?.size) voiceChunks.push(event.data);892    });893    voiceRecorder.addEventListener("stop", () => {894      const recorderMimeType = voiceRecorder?.mimeType || mimeType || "audio/webm";895      const recordedChunks = voiceChunks;896      stopVoiceStream();897      const extension = recorderMimeType.includes("mp4")898        ? "m4a"899        : recorderMimeType.includes("ogg")900          ? "ogg"901          : "webm";902      const blob = new Blob(recordedChunks, { type: recorderMimeType });903      voiceRecorder = null;904      voiceChunks = [];905      if (!blob.size) {906        setVoiceRecordingState("idle");907        submit.disabled = false;908        setCommandDisabled(false);909        setSessionStatus("Voice note is empty.");910        return;911      }912      setVoiceRecordingState("transcribing");913      transcribeVoiceBlob(blob, `recorded-idea.${extension}`);914    });915    voiceRecorder.start();916    setVoiceRecordingState("recording");917    setSessionStatus("Listening. Press Stop when your idea is ready.");918  } catch (error) {919    stopVoiceStream();920    voiceRecorder = null;921    voiceChunks = [];922    setVoiceRecordingState("idle");923    submit.disabled = false;924    setCommandDisabled(false);925    setSessionStatus(`Voice recording could not start: ${error.message}`);926  }927}928 929function stopVoiceRecording() {930  if (!voiceRecorder || voiceRecorder.state !== "recording") return;931  setVoiceRecordingState("stopping");932  setSessionStatus("Stopping recording.");933  try {934    voiceRecorder.stop();935  } catch (error) {936    stopVoiceStream();937    voiceRecorder = null;938    voiceChunks = [];939    setVoiceRecordingState("idle");940    submit.disabled = false;941    setCommandDisabled(false);942    setSessionStatus(`Voice recording could not stop: ${error.message}`);943  }944}945 946function recordingMimeType() {947  const candidates = ["audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4"];948  return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || "";949}950 951function stopVoiceStream() {952  if (!voiceStream) return;953  voiceStream.getTracks().forEach((track) => track.stop());954  voiceStream = null;955}956 957async function transcribeVoiceBlob(blob, filename) {958  if (sessionControlsLocked || voiceBusy) return false;959  if (voiceRecordingState !== "idle" && voiceRecordingState !== "transcribing") return false;960  if (!blob?.size) {961    setSessionStatus("Voice note is empty.");962    return false;963  }964  const revision = bumpSessionRevision();965  voiceBusy = true;966  setVoiceRecordingState("transcribing");967  submit.disabled = true;968  input.disabled = true;969  setCommandDisabled(true);970  setSessionControlsDisabled(true);971  setSessionStatus("Transcribing voice note.");972  try {973    const formData = new FormData();974    formData.append("audio", blob, filename || "voice-note.audio");975    const response = await fetch("/api/transcribe", {976      method: "POST",977      body: formData,978    });979    if (!response.ok) throw new Error(`voice note failed with ${response.status}`);980    const data = await response.json();981    const transcript = String(data.transcript || "").trim();982    if (!transcript) throw new Error("empty transcript");983    if (!isCurrentSessionRevision(revision)) return false;984    input.value = mergeDraftWithTranscript(input.value, transcript);985    session.ui_status = "Voice note transcribed. Edit the draft or press Ink.";986    corrections.textContent = session.ui_status;987    saveSession();988    return true;989  } catch (error) {990    if (isCurrentSessionRevision(revision)) setSessionStatus(`Voice note could not be transcribed: ${error.message}`);991    return false;992  } finally {993    voiceBusy = false;994    setVoiceRecordingState("idle");995    if (isCurrentSessionRevision(revision)) {996      submit.disabled = false;997      input.disabled = false;998      setSessionControlsDisabled(false);999      setCommandDisabled(false);1000      input.focus();1001    }1002  }1003}1004 1005function mergeDraftWithTranscript(draft, transcript) {1006  const current = String(draft || "").trim();1007  return current ? `${current}\n${transcript}` : transcript;1008}1009 1010async function bootstrap() {1011  const response = await fetch("/api/bootstrap");1012  if (!response.ok) throw new Error(`project index failed with ${response.status}`);1013  const data = await response.json();1014  bootstrapData = data;1015  const rawProfiles = Array.isArray(data.goal_profiles) ? data.goal_profiles : [];1016  const rawOptions = Array.isArray(data.goal_options) ? data.goal_options : [];1017  goalProfiles = normalizeGoalProfiles(rawProfiles, rawOptions);1018  goalOptions = goalProfiles.map((goal) => goal.id);1019  goalProfileById = new Map(goalProfiles.map((goal) => [goal.id, goal]));1020  profileFields = data.profile_fields || [];1021  session = normalizeSession(readSavedSession(), defaultSession(data));1022  renderProvenance(data);1023  renderGoals(session.goals);1024  renderProfile(session.profile);1025  renderRestoredSession(data);1026  renderWhitespace(data.whitespace || []);1027  setVoiceRecordingState("idle");1028}1029 1030function handleBootstrapError(error) {1031  bootstrapData = null;1032  currentArtifact = null;1033  session = {};1034  submit.disabled = true;1035  input.disabled = true;1036  setCommandDisabled(true);1037  setSessionControlsDisabled(true);1038  ink.textContent = `The project index could not be opened: ${error.message}`;1039  ink.classList.remove("thinking", "gold");1040  ink.classList.add("bleed");1041  corrections.textContent = "Reload the page to try again.";1042  provenanceEl.textContent = "index unavailable";1043  renderScore(null);1044  setVerdictDisplay("INDEX CLOSED", 0, null);1045  renderWoodMap(null);1046  renderGoals([]);1047  renderProfile({});1048  renderIdeas([]);1049  renderProjects([]);1050  renderWhitespace([]);1051  renderPlan([]);1052}1053 1054function defaultSession(data = bootstrapData) {1055  return {1056    profile: {},1057    goals: data?.default_goals || goalOptions.slice(0, 3),1058  };1059}1060 1061function setActiveTab(tab) {1062  if (!spreadEl) return;1063  const next = ["page", "proof", "almanac"].includes(tab) ? tab : "page";1064  spreadEl.dataset.tab = next;1065  document.querySelectorAll(".mobile-nav [data-tab]").forEach((button) => {1066    button.classList.toggle("active", button.dataset.tab === next);1067  });1068}1069 1070function setVerdictDisplay(verdict = "READY", overall = 0, score = null) {1071  const text = String(verdict || "READY");1072  const isEcho = text.startsWith("ECHO");1073  const isUnwritten = text.startsWith("UNWRITTEN");1074  const numericOverall = Number(overall || score?.overall || 0);1075 1076  verdictEl.textContent = text;1077  overallEl.textContent = numericOverall.toFixed(1);1078  sealEl.classList.toggle("echo", isEcho);1079  sealEl.classList.toggle("unwritten", isUnwritten);1080 1081  sealVerdictEl.textContent = text;1082  sealVerdictEl.classList.toggle("echo", isEcho);1083  sealVerdictEl.classList.toggle("unwritten", isUnwritten);1084  sealVerdictEl.classList.toggle("ready", !isEcho && !isUnwritten);1085 1086  verdictStampEl.classList.toggle("verdict-echo", isEcho);1087  verdictStampEl.classList.toggle("verdict-unwritten", isUnwritten);1088  verdictStampEl.classList.toggle("verdict-ready", !isEcho && !isUnwritten);1089 1090  if (!score) {1091    sealCopyEl.textContent = text === "INDEX CLOSED" ? "The project map did not load." : "No idea has been scored yet.";1092  } else if (isEcho) {1093    sealCopyEl.textContent = "Nearby projects already cover parts of this idea.";1094  } else {1095    sealCopyEl.textContent = "This idea sits in a quieter part of the current map.";1096  }1097}1098 1099function bumpSessionRevision() {1100  sessionRevision += 1;1101  return sessionRevision;1102}1103 1104function isCurrentSessionRevision(revision) {1105  return revision === sessionRevision;1106}1107 1108function restoreExportButtonLabels() {1109  setActionButtonLabel(exportNotesButton, "Notes");1110  setActionButtonLabel(exportChapterButton, "Chapter");1111  setActionButtonLabel(exportButton, PNG_EXPORT_LABEL);1112}1113 1114function actionButtonLabel(button) {1115  return button?.dataset.actionLabel || button?.textContent.trim() || "";1116}1117 1118function setActionButtonLabel(button, label) {1119  if (!button) return;1120  button.dataset.actionLabel = label;1121  const textNode = Array.from(button.childNodes).find(1122    (node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim(),1123  );1124  if (textNode) {1125    textNode.textContent = ` ${label}`;1126  } else {1127    button.append(document.createTextNode(` ${label}`));1128  }1129}1130 1131function setSessionControlsDisabled(disabled) {1132  sessionControlsLocked = disabled;1133  goalsEl.querySelectorAll("input[data-goal]").forEach((target) => {1134    target.disabled = disabled;1135  });1136  profileEl.querySelectorAll("input[data-profile-field]").forEach((field) => {1137    field.disabled = disabled;1138  });1139  ideasEl.querySelectorAll("button[data-idea-id]").forEach((idea) => {1140    idea.disabled = disabled;1141  });1142  whitespaceEl.querySelectorAll("button[data-gap-prompt]").forEach((gap) => {1143    gap.disabled = disabled;1144  });1145  setVoiceControlsDisabled(disabled);1146}1147 1148function setVoiceControlsDisabled(disabled) {1149  const recording = voiceRecordingState === "recording" && voiceRecorder?.state === "recording";1150  const lockedForState = ["starting", "stopping", "transcribing"].includes(voiceRecordingState);1151  recordVoiceButton.disabled = !bootstrapData || voiceBusy || lockedForState || (disabled && !recording);1152  uploadVoiceButton.disabled = !bootstrapData || voiceBusy || disabled || voiceRecordingState !== "idle";1153}1154 1155function setVoiceRecordingState(state) {1156  voiceRecordingState = state;1157  recordVoiceButton.dataset.voiceState = state;1158  recordVoiceButton.classList.toggle("recording", state === "recording");1159  recordVoiceButton.setAttribute("aria-pressed", state === "recording" ? "true" : "false");1160  const labels = {1161    idle: "Speak",1162    starting: "Starting...",1163    recording: "Stop",1164    stopping: "Stopping...",1165    transcribing: "Hearing...",1166  };1167  setActionButtonLabel(recordVoiceButton, labels[state] || "Speak");1168  setVoiceControlsDisabled(sessionControlsLocked);1169}1170 1171function resetSession() {1172  if (!bootstrapData) return;1173  bumpSessionRevision();1174  clearTurnWatchdog();1175  clearSavedSession();1176  session = defaultSession(bootstrapData);1177  currentArtifact = null;1178  submit.disabled = false;1179  input.disabled = false;1180  setSessionControlsDisabled(false);1181  input.value = "";1182  ink.textContent = "The book is open. Describe an idea to start a new page.";1183  ink.classList.remove("thinking", "bleed", "gold");1184  corrections.textContent = "Session reset.";1185  renderGoals(session.goals);1186  renderProfile(session.profile);1187  renderScore(null);1188  setVerdictDisplay("READY", 0, null);1189  renderWoodMap(null);1190  renderIdeas([]);1191  renderPlan([]);1192  renderProjects([], "Score an idea to see nearby echoes.");1193  renderWhitespace(bootstrapData.whitespace || []);1194  restoreExportButtonLabels();1195  setCommandDisabled(false);1196  saveSession();1197  input.focus();1198}1199 1200async function loadDemoSession() {

Showing the first 1,200 of 2757 lines. Download the file for the rest.