// Main App: composes all panels, owns shared state, wires shortcuts const { useState, useEffect, useCallback, useMemo } = React; // Default hypothesis shape — every web has one. Binary, event-based, with a // resolveBy date and clear yes/no scenarios. Webs without one get this stub // when the user opens them. const DEFAULT_HYPOTHESIS = () => ({ question: '', resolveBy: '', yesScenario: '', noScenario: '', status: 'open', // open | yes | no | undecided }); // Migrate older saved webs (which had no `hypothesis`) into the new shape so // hooks reading `web.hypothesis` don't crash on legacy state. function migrateWeb(w) { if (!w) return w; return { ...w, hypothesis: { ...DEFAULT_HYPOTHESIS(), ...(w.hypothesis || {}) } }; } // Editable banner sitting above the canvas. Shows the active web's hypothesis // (the binary, event-based question the entire web exists to answer) plus // the yes/no scenarios and resolveBy date. All fields click-to-edit. function HypothesisBanner({ hypothesis, setHypothesis, onInvent, inventing }) { // Auto-open the seed textarea when there's no hypothesis yet, so a new // user lands on the "type your idea" prompt instead of an empty banner // they have to figure out how to interact with. const _initialEmpty = !((hypothesis || {}).question || '').trim(); const [seedOpen, setSeedOpen] = useState(_initialEmpty); const [seedText, setSeedText] = useState(() => { // Restore in-flight seed across page reloads (e.g. user clicks Invent, // it fails, they refresh — without this, their typed text is gone). try { return localStorage.getItem('tibeb.seedDraft') || ''; } catch { return ''; } }); const seedRef = useRef(null); useEffect(() => { if (seedOpen) setTimeout(() => seedRef.current && seedRef.current.focus(), 50); }, [seedOpen]); // Persist the seed draft on every keystroke so refreshes don't blow it away. useEffect(() => { try { localStorage.setItem('tibeb.seedDraft', seedText); } catch {} }, [seedText]); // Watch for hypothesis transitions: when invent succeeds, the parent's // hypothesis prop gets a real question. THAT's the safe moment to clear // the seed draft. If invent fails, hypothesis stays empty and the user's // text remains so they can edit + retry. const prevHadHypothesisRef = useRef(!_initialEmpty); useEffect(() => { const hasNow = !!((hypothesis || {}).question || '').trim(); if (hasNow && !prevHadHypothesisRef.current && seedText) { // Hypothesis just became non-empty — the prior submit must have landed. setSeedText(''); try { localStorage.removeItem('tibeb.seedDraft'); } catch {} } prevHadHypothesisRef.current = hasNow; }, [hypothesis && hypothesis.question]); // If inventing flips false WITHOUT a hypothesis appearing, the request // failed. Re-open the seed pane so the user sees their text + a path to // retry instead of staring at an empty banner. const prevInventingRef = useRef(inventing); useEffect(() => { const wasInventing = prevInventingRef.current; const stillEmpty = !((hypothesis || {}).question || '').trim(); if (wasInventing && !inventing && stillEmpty && seedText) { setSeedOpen(true); } prevInventingRef.current = inventing; }, [inventing]); const submitSeed = () => { const t = seedText.trim(); if (!t) return; onInvent({ seed: t }); // CRITICAL: keep seedText populated until the hypothesis prop confirms // the invent landed. If invent fails, the user's text stays in the box // so they can edit it and retry instead of losing it forever. setSeedOpen(false); }; // Minimize toggle — same pattern as the LeftRail / chat collapses. // Persists across reloads so the user's preference sticks. const [collapsed, setCollapsed] = useState(() => { try { return localStorage.getItem('jarvis.hypoCollapsed') === '1'; } catch { return false; } }); useEffect(() => { try { localStorage.setItem('jarvis.hypoCollapsed', collapsed ? '1' : '0'); } catch {} }, [collapsed]); const h = hypothesis || DEFAULT_HYPOTHESIS(); const empty = !(h.question || '').trim(); const statusColor = { open: 'var(--text-2)', yes: '#00d4aa', no: '#ff4466', leaning_yes: '#00d4aa', leaning_no: '#ff4466', undecided: '#d4b800', }[h.status || 'open']; const statusLabel = { open: 'open', yes: 'resolved · YES', no: 'resolved · NO', leaning_yes: 'leaning YES', leaning_no: 'leaning NO', undecided: 'undecided', }[h.status || 'open'] || (h.status || 'open'); // Probability + source split (set after a Mad Max sweep finishes) const hasProb = typeof h.probability === 'number'; const sup = h.supportingCount || 0; const con = h.contradictingCount || 0; return (
Hypothesis · {statusLabel} {hasProb && ( {h.probability}% YES )} {(sup + con) > 0 && ( {sup} supporting · {con} contradicting )}
{!collapsed && seedOpen && (
{empty && (
START HERE · STEP 1 OF 4
What's your investing idea?
Type a rough thought. Tibeb sharpens it into a research-grade hypothesis, runs Mad Max for sources, and offers a sized paper trade — one click, end-to-end.
{[ 'AI compute is overrated — find one mispriced semi short', 'Hedge my tech book through earnings season', 'What\'s the next leg of the data-center power story?', 'Is there a misread on inventory cyclicality in DRAM?', ].map((s, i) => ( ))}
)}