/* @component-map * App — Main container, handles screen routing and state * IntroScreen — Landing page with ripple animation and start button * FeedScreen — Scenario browser with category filters * PredictScreen — Prediction input chain for a selected scenario * LoadingScreen — Animated loading state during AI evaluation * RevealScreen — Score reveal with hits, misses, blind spots * Header — Top bar with logo, streak, and accuracy * @end-component-map */ import { useCollection, useAI } from '@deplixo/sdk'; import { IntroScreen } from './components/IntroScreen.jsx'; import { FeedScreen } from './components/FeedScreen.jsx'; import { PredictScreen } from './components/PredictScreen.jsx'; import { LoadingScreen } from './components/LoadingScreen.jsx'; import { RevealScreen } from './components/RevealScreen.jsx'; import { Header } from './components/Header.jsx'; import { SCENARIOS } from './components/scenarios.jsx'; function App() { const [screen, setScreen] = useState("intro"); const [scenario, setScenario] = useState(null); const [result, setResult] = useState(null); const [apiError, setApiError] = useState(null); const { items: statsItems, add: addStat, update: updateStat, loading: statsLoading } = useCollection("ripple_stats", { personal: true }); const { items: scoreItems, add: addScore } = useCollection("ripple_scores", { personal: true }); const { generate, loading: aiLoading } = useAI(); const statEntry = statsItems[0]; const streak = statEntry ? (statEntry.value.streak || 0) : 0; const allScores = scoreItems.map(s => s.value.score); const accuracy = allScores.length > 0 ? Math.round(allScores.reduce((a, b) => a + b, 0) / allScores.length) : 0; const ensureStats = async () => { if (!statEntry && !statsLoading) { await addStat({ streak: 0 }); } }; useEffect(() => { ensureStats(); }, [statsItems, statsLoading]); const handleSelect = (s) => { setScenario(s); setApiError(null); setScreen("predict"); }; const handleSubmit = async (preds) => { setScreen("loading"); setApiError(null); const chainText = preds.filter(p => p.trim()).map((p, i) => `${i + 1}. ${p}`).join("\n"); const prompt = `You are an expert systems thinker evaluating a player's cause-and-effect predictions.\n\nSCENARIO: "${scenario.title}"\nCONTEXT: ${scenario.hook}\n\nPLAYER'S CHAIN:\n${chainText}\n\nReturn ONLY a JSON object with no markdown:\n{"score":<0-100>,"hits":[{"prediction":"","annotation":""}],"misses":[{"prediction":"","annotation":""}],"blind_spots":[{"effect":"","order":"2nd order","why":""}],"insight":"<2 provocative sentences about the system dynamics at play>"}\n\nScore: 90+=exceptional, 70+=good 2nd-order thinking, 50+=basic 1st-order, <50=linear only.`; try { const r = await generate({ prompt, json: true }); if (!r || typeof r.score !== 'number') throw new Error('Unexpected AI response format'); setResult(r); const newStreak = streak + 1; if (statEntry) { await updateStat(statEntry.id, { streak: newStreak }); } await addScore({ score: r.score, scenarioId: scenario.id, timestamp: Date.now() }); setScreen("reveal"); } catch (err) { setApiError(`Could not score predictions: ${err.message}`); setScreen("predict"); } }; const handleNext = () => { const idx = SCENARIOS.findIndex(s => s.id === scenario?.id); setScenario(SCENARIOS[(idx + 1) % SCENARIOS.length]); setResult(null); setApiError(null); setScreen("predict"); }; if (screen === "intro") return setScreen("feed")} />; if (screen === "feed") return ; if (screen === "loading") return ; if (screen === "predict") return setScreen("feed")} streak={streak} accuracy={accuracy} initialError={apiError} />; if (screen === "reveal") return setScreen("feed")} streak={streak} accuracy={accuracy} />; return null; } ReactDOM.createRoot(document.getElementById("root")).render();