LifeStack / templates /index.html
Soham Banerjee
deploy: pure lifestack with partitioned wisdom pool
77da5ce
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LifeStack Portal | Meta OpenEnv 2026</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #0f172a; color: #f8fafc; overflow-x: hidden; }
.glass { background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.08); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); }
.tab-active { border-bottom: 2px solid #818cf8; color: #818cf8; }
.metric-bar { transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes pulse-red { 0%, 100% { background-color: #ef4444; } 50% { background-color: #f87171; } }
.pulse-red { animation: pulse-red 1s infinite; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #475569; }
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); }
</style>
</head>
<body class="p-4 md:p-8">
<div class="max-w-6xl mx-auto">
<!-- Header -->
<header class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-extrabold tracking-tight text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-cyan-400">πŸͺ LifeStack Engine</h1>
<p class="text-slate-400 text-sm">Meta Γ— HuggingFace OpenEnv Hackathon Finale</p>
</div>
<div class="hidden md:block text-right">
<span class="px-3 py-1 rounded-full bg-indigo-500/10 text-indigo-400 text-xs font-bold border border-indigo-500/20">PRODUCTION HARDENED v3.0</span>
</div>
</header>
<!-- Navigation Tabs -->
<div class="flex gap-6 border-b border-slate-800 mb-8 overflow-x-auto no-scrollbar whitespace-nowrap">
<button onclick="showTab('situation')" id="tab-situation" class="pb-4 text-sm font-semibold transition tab-active">Situational Portal</button>
<button onclick="showTab('custom')" id="tab-custom" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">πŸ’­ Try Your Case</button>
<button onclick="showTab('compare')" id="tab-compare" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">⚑ Untrained vs GRPO-Trained</button>
<button onclick="showTab('memeffect')" id="tab-memeffect" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">🧠 Memory Effect</button>
<button onclick="showTab('arjun')" id="tab-arjun" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">πŸ—“οΈ Arjun's Journey</button>
<button onclick="showTab('history')" id="tab-history" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">πŸ“Ό Episode Replay</button>
<button onclick="showTab('lab')" id="tab-lab" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">🎭 Personality Lab</button>
<button onclick="showTab('whatif')" id="tab-whatif" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">🧩 What-If Lab</button>
<button onclick="showTab('tasks')" id="tab-tasks" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">πŸ—ΊοΈ Task Explorer</button>
<button onclick="showTab('performance')" id="tab-performance" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">πŸ“Š Analytics</button>
<button onclick="showTab('verification')" id="tab-verification" class="pb-4 text-sm font-semibold text-slate-400 hover:text-white transition">πŸ“¬ Verification</button>
</div>
<!-- ── TAB 1: SITUATION PORTAL ── -->
<div id="content-situation" class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-1 space-y-6">
<div class="glass p-6 rounded-2xl">
<h3 class="text-lg font-bold mb-4">Simulation Control</h3>
<div class="space-y-4 mb-6">
<div class="text-sm">
<label class="block text-slate-400 mb-2">Subject Persona</label>
<select id="person-select" onchange="resetDemoUI()" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white">
{% for p in persons %} <option value="{{ p }}">{{ p }}</option> {% endfor %}
</select>
</div>
<div class="text-sm">
<label class="block text-slate-400 mb-2">Life Conflict</label>
<select id="conflict-select" onchange="resetDemoUI()" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white">
{% for c in conflicts %} <option value="{{ c }}">{{ c }}</option> {% endfor %}
</select>
</div>
</div>
<div class="flex items-center justify-between p-3 bg-indigo-500/5 rounded-xl border border-indigo-500/20">
<div class="text-[10px] uppercase font-black text-indigo-400">Context Augmentation (RAG)</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="memory-toggle" class="sr-only peer">
<div class="w-9 h-5 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<button onclick="startDemo()" id="simulate-btn" class="w-full py-4 bg-indigo-600 hover:bg-indigo-500 rounded-xl font-bold transition flex items-center justify-center gap-2">
<span id="btn-icon">β–Ά</span> <span id="btn-text">Start Simulation</span>
</button>
</div>
<!-- Trajectroy -->
<div id="trajectory-panel" class="hidden glass p-6 rounded-2xl border-l-4 border-yellow-500">
<h4 class="text-[10px] font-bold text-slate-500 tracking-widest uppercase mb-2">AI Risk Prediction</h4>
<p id="traj-summary" class="text-sm text-slate-200 mb-4 italic leading-relaxed">Analyzing...</p>
<div class="h-1.5 bg-slate-800 rounded-full overflow-hidden"><div id="traj-bar" class="h-full bg-yellow-500 transition-all duration-700" style="width: 0%"></div></div>
</div>
</div>
<div class="lg:col-span-1 glass p-6 rounded-2xl relative overflow-hidden">
<h3 class="text-lg font-bold flex justify-between mb-2"><span>Vital Metrics</span><span id="metric-phase" class="text-[10px] bg-slate-800 px-2 py-1 rounded text-slate-400 uppercase tracking-tighter">STABLE</span></h3>
<!-- Domain Risk Heatmap -->
<div class="mb-4">
<div class="text-[9px] font-black text-slate-600 uppercase tracking-widest mb-2">Domain Risk Heatmap</div>
<div id="domain-heatmap" class="grid grid-cols-6 gap-1.5">
<div class="flex flex-col items-center gap-1">
<div class="w-full h-8 rounded-lg bg-slate-700 transition-all duration-700" id="hm-career" title="Career"></div>
<span class="text-[8px] text-slate-500 font-bold">WORK</span>
</div>
<div class="flex flex-col items-center gap-1">
<div class="w-full h-8 rounded-lg bg-slate-700 transition-all duration-700" id="hm-finances" title="Finances"></div>
<span class="text-[8px] text-slate-500 font-bold">MONEY</span>
</div>
<div class="flex flex-col items-center gap-1">
<div class="w-full h-8 rounded-lg bg-slate-700 transition-all duration-700" id="hm-relationships" title="Relationships"></div>
<span class="text-[8px] text-slate-500 font-bold">SOCIAL</span>
</div>
<div class="flex flex-col items-center gap-1">
<div class="w-full h-8 rounded-lg bg-slate-700 transition-all duration-700" id="hm-physical_health" title="Physical Health"></div>
<span class="text-[8px] text-slate-500 font-bold">BODY</span>
</div>
<div class="flex flex-col items-center gap-1">
<div class="w-full h-8 rounded-lg bg-slate-700 transition-all duration-700" id="hm-mental_wellbeing" title="Mental Wellbeing"></div>
<span class="text-[8px] text-slate-500 font-bold">MIND</span>
</div>
<div class="flex flex-col items-center gap-1">
<div class="w-full h-8 rounded-lg bg-slate-700 transition-all duration-700" id="hm-time" title="Time"></div>
<span class="text-[8px] text-slate-500 font-bold">TIME</span>
</div>
</div>
</div>
<div id="metrics-container" class="space-y-2.5 relative z-10 max-h-[520px] overflow-y-auto pr-2 custom-scrollbar"></div>
<!-- Mini-Graph Overlay -->
<div id="cascade-graph" class="absolute inset-0 opacity-10 pointer-events-none"></div>
</div>
<div class="lg:col-span-1 space-y-6">
<div id="action-card" class="hidden glass p-6 rounded-2xl border-l-4 border-indigo-500">
<div class="flex justify-between items-start mb-4">
<h3 id="action-type" class="text-xl font-extrabold text-indigo-400 tracking-tighter uppercase">--</h3>
<div id="episode-tag" class="text-[10px] font-mono bg-indigo-500/20 px-2 py-1 rounded">ID: --</div>
</div>
<p id="action-desc" class="text-sm text-slate-300 mb-4">--</p>
<div class="p-4 bg-slate-950/50 rounded-xl border border-slate-800 mb-4">
<p id="action-reasoning" class="text-xs text-slate-400 italic">--</p>
</div>
<div class="grid grid-cols-2 gap-3 text-xs font-bold">
<div class="p-3 bg-slate-900 rounded-xl">⏱️ <span id="action-time">--</span></div>
<div class="p-3 bg-slate-900 rounded-xl">πŸ’΅ <span id="action-money">--</span></div>
</div>
</div>
<div id="counterfactual-card" class="hidden glass p-6 rounded-2xl"><h4 class="text-xs font-bold text-slate-500 tracking-widest uppercase mb-4">βš–οΈ What-If Analysis</h4><div id="cf-container" class="space-y-3 text-[11px]"></div></div>
<!-- Memory Retrieval Insight -->
<div id="memory-card" class="hidden glass p-6 rounded-2xl border-l-4 border-green-500">
<h4 class="text-[10px] font-black text-green-500 tracking-widest uppercase mb-3 flex justify-between"><span>Retrieved Precedents (RAG)</span> <span class="bg-green-500/20 px-2 py-0.5 rounded text-white">+116% EFFICIENCY</span></h4>
<div id="memory-container" class="space-y-3"></div>
</div>
</div>
</div>
<!-- ── 7-DAY FORECAST (spans full width below the portal) ── -->
<div id="forecast-panel" class="hidden glass p-6 rounded-2xl mt-6 border-t-2 border-cyan-500/30">
<div class="flex justify-between items-center mb-4">
<div>
<h3 class="text-base font-black tracking-tighter text-cyan-400">7-DAY LIFE TRAJECTORY</h3>
<p class="text-[10px] text-slate-500 mt-0.5">What happens to your six domains over the next week if nothing extraordinary occurs β€” computed from the real LifeStack env rollout, not estimated.</p>
</div>
<div class="text-right">
<div class="text-[9px] font-black text-slate-600 uppercase tracking-widest">Discounted Reward (Ξ³=0.9)</div>
<div id="forecast-total-reward" class="text-xl font-black text-cyan-400">--</div>
</div>
</div>
<div class="relative h-52">
<canvas id="trajectoryChart"></canvas>
</div>
<!-- Per-day step summary -->
<div id="forecast-steps" class="grid grid-cols-7 gap-2 mt-4 text-[9px]"></div>
</div>
<!-- ── TAB 2: TRY YOUR CASE ── -->
<div id="content-custom" class="hidden space-y-6">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- Left: inputs -->
<div class="glass p-6 rounded-2xl space-y-6">
<h3 class="text-xl font-bold">Describe Your Situation</h3>
<textarea id="custom-situation" class="w-full bg-slate-950 border border-slate-800 rounded-xl p-4 text-sm text-slate-200 outline-none focus:border-indigo-500 h-32" placeholder="e.g. My boss gave me a huge deadline today, but it's my partner's birthday and I'm already exhausted..."></textarea>
<div class="grid grid-cols-2 gap-4 text-xs font-bold uppercase text-slate-500">
<div class="space-y-4">
<label>πŸ’Ό Work Stress <input type="range" id="custom-work" class="w-full accent-indigo-500"></label>
<label>πŸ’° Money Stress <input type="range" id="custom-money" class="w-full accent-indigo-500"></label>
</div>
<div class="space-y-4">
<label>❀️ Relationships <input type="range" id="custom-rel" class="w-full accent-indigo-500"></label>
<label>⚑ Energy <input type="range" id="custom-energy" class="w-full accent-indigo-500"></label>
</div>
</div>
<button onclick="runDigitalSync()" id="digital-sync-btn" class="w-full py-3 bg-gradient-to-r from-cyan-700 to-indigo-700 rounded-xl text-sm font-bold shadow-lg hover:opacity-90 transition flex items-center justify-center gap-2">
<span id="sync-icon">πŸ“‘</span> <span id="sync-text">Sync Digital Life (Gmail + Calendar + Fitness)</span>
</button>
<button onclick="runCustom()" class="w-full py-3 bg-indigo-600 rounded-xl text-sm font-bold shadow-lg shadow-indigo-500/20">Analyze & Plan</button>
</div>
<!-- Centre: Digital Sync panel -->
<div id="digital-sync-panel" class="glass p-6 rounded-2xl space-y-4">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-2">Digital Life Signals</div>
<div id="sync-idle" class="text-center py-16 text-slate-600 text-sm italic">Click "Sync Digital Life" to pull signals from your connected services.</div>
<div id="sync-sources" class="hidden space-y-4">
<!-- Gmail source card -->
<div class="p-4 rounded-xl border border-slate-700 bg-slate-900/50">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<span class="text-base">πŸ“§</span>
<span class="text-xs font-black text-slate-300 uppercase tracking-wider">Gmail</span>
</div>
<span id="gmail-badge" class="text-[8px] font-black px-2 py-0.5 rounded-full border uppercase"></span>
</div>
<p id="gmail-sync-summary" class="text-[10px] text-slate-400 italic leading-relaxed"></p>
<div id="gmail-threads" class="mt-2 space-y-1"></div>
</div>
<!-- Calendar source card -->
<div class="p-4 rounded-xl border border-slate-700 bg-slate-900/50">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<span class="text-base">πŸ“…</span>
<span class="text-xs font-black text-slate-300 uppercase tracking-wider">Google Calendar</span>
</div>
<span id="cal-badge" class="text-[8px] font-black px-2 py-0.5 rounded-full border uppercase"></span>
</div>
<p id="cal-sync-summary" class="text-[10px] text-slate-400 italic leading-relaxed"></p>
<div id="cal-deadlines" class="mt-2 space-y-1"></div>
</div>
<!-- Fitness source card -->
<div class="p-4 rounded-xl border border-slate-700 bg-slate-900/50">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<span class="text-base">πŸ’ͺ</span>
<span class="text-xs font-black text-slate-300 uppercase tracking-wider">Fitness</span>
</div>
<span id="fit-badge" class="text-[8px] font-black px-2 py-0.5 rounded-full border uppercase bg-amber-500/10 border-amber-500/30 text-amber-400">Demo</span>
</div>
<p id="fit-sync-summary" class="text-[10px] text-slate-400 italic leading-relaxed"></p>
<div id="fit-stats" class="mt-2 grid grid-cols-3 gap-2"></div>
</div>
</div>
</div>
<!-- Centre-right: Manual upload cards -->
<div class="space-y-4">
<!-- Health Upload -->
<div class="glass p-4 rounded-2xl border border-slate-700">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-3">πŸ’ͺ Health Data Upload</div>
<div class="grid grid-cols-3 gap-2 mb-3">
<div>
<label class="text-[9px] text-slate-500 uppercase font-bold">Sleep (h)</label>
<input type="number" id="health-sleep" value="7" step="0.5" min="0" max="12" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-white mt-1">
</div>
<div>
<label class="text-[9px] text-slate-500 uppercase font-bold">HR (bpm)</label>
<input type="number" id="health-hr" value="70" min="40" max="120" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-white mt-1">
</div>
<div>
<label class="text-[9px] text-slate-500 uppercase font-bold">Steps/day</label>
<input type="number" id="health-steps" value="8000" step="500" min="0" max="30000" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-white mt-1">
</div>
</div>
<button onclick="uploadHealthData()" class="w-full py-2 bg-emerald-700 hover:bg-emerald-600 rounded-lg text-xs font-bold transition">Upload Health Data</button>
<p id="health-upload-status" class="text-[9px] text-slate-500 italic mt-2"></p>
</div>
<!-- Calendar Upload -->
<div class="glass p-4 rounded-2xl border border-slate-700">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-3">πŸ“… Calendar Data Upload</div>
<div class="grid grid-cols-2 gap-2 mb-3">
<div>
<label class="text-[9px] text-slate-500 uppercase font-bold">Occupancy %</label>
<input type="number" id="cal-occupancy" value="60" min="0" max="100" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-white mt-1">
</div>
<div>
<label class="text-[9px] text-slate-500 uppercase font-bold">Back-to-back</label>
<input type="number" id="cal-btb" value="2" min="0" max="20" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-white mt-1">
</div>
</div>
<div class="mb-3">
<label class="text-[9px] text-slate-500 uppercase font-bold">Critical Deadlines (count)</label>
<input type="number" id="cal-critical" value="1" min="0" max="20" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-white mt-1">
</div>
<button onclick="uploadCalendarData()" class="w-full py-2 bg-blue-700 hover:bg-blue-600 rounded-lg text-xs font-bold transition">Upload Calendar Data</button>
<p id="cal-upload-status" class="text-[9px] text-slate-500 italic mt-2"></p>
</div>
</div>
<!-- Right: result -->
<div id="custom-result" class="hidden glass p-6 rounded-2xl overflow-y-auto max-h-[620px]">
<div class="flex justify-between items-center mb-6"><h3 class="text-lg font-bold">Personalised Resolution</h3><span id="custom-id" class="text-[10px] font-mono opacity-40">ID: --</span></div>
<div id="custom-metrics-grid" class="mb-8"></div>
<div id="custom-plan" class="p-4 bg-indigo-500/10 border border-indigo-500/20 rounded-xl space-y-4"></div>
</div>
</div>
</div>
<!-- ── TAB A: UNTRAINED LLM VS GRPO-TRAINED ── -->
<div id="content-compare" class="hidden space-y-6">
<div class="glass p-8 rounded-2xl">
<div class="flex flex-col md:flex-row gap-6 items-end mb-8">
<div class="flex-1 space-y-2">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest">Conflict Scenario</div>
<select id="cmp-conflict" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm">
{% for c in conflicts %} <option value="{{ c }}">{{ c }}</option> {% endfor %}
</select>
</div>
<div class="flex-1 space-y-2">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest">Subject Persona</div>
<select id="cmp-person" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm">
{% for p in persons %} <option value="{{ p }}">{{ p }}</option> {% endfor %}
</select>
</div>
<button onclick="runComparison()" id="cmp-btn" class="px-10 py-3 bg-gradient-to-r from-indigo-600 to-cyan-600 rounded-xl font-bold shadow-lg shadow-indigo-600/20 active:scale-95 transition whitespace-nowrap">⚑ Run Comparison</button>
</div>
<div id="cmp-intro" class="text-center py-12 text-slate-500 italic">Select a conflict and run to see how GRPO training changes the LLM's decisions.</div>
<div id="cmp-content" class="hidden grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Baseline (Untrained LLM) -->
<div class="glass p-6 rounded-2xl border-t-4 border-slate-600 bg-slate-700/5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest">No RL Training</div>
<h4 class="text-xl font-black text-slate-300 mt-0.5">Untrained LLM</h4>
</div>
<div class="text-right">
<div id="cmp-reward-baseline" class="text-2xl font-black text-slate-400">--</div>
<div class="text-[8px] text-slate-600 uppercase font-black">reward</div>
</div>
</div>
<div class="p-3 bg-slate-900/60 rounded-xl border border-slate-700 mb-4">
<div class="text-[9px] font-black text-slate-600 uppercase mb-1">Strategy</div>
<p id="cmp-action-baseline" class="text-sm font-bold text-slate-300">--</p>
<p id="cmp-reason-baseline" class="text-xs text-slate-500 italic mt-1">--</p>
</div>
<div id="cmp-metrics-baseline" class="space-y-1.5 text-[10px]"></div>
</div>
<!-- Trained -->
<div class="glass p-6 rounded-2xl border-t-4 border-indigo-500 bg-indigo-500/5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-[9px] font-black text-indigo-400 uppercase tracking-widest">GRPO-Trained LifeStack</div>
<h4 class="text-xl font-black text-white mt-0.5">LifeStack Agent</h4>
</div>
<div class="text-right">
<div id="cmp-reward-trained" class="text-2xl font-black text-indigo-400">--</div>
<div class="text-[8px] text-indigo-600 uppercase font-black">reward</div>
</div>
</div>
<div class="p-3 bg-indigo-950/50 rounded-xl border border-indigo-500/20 mb-4">
<div class="text-[9px] font-black text-indigo-500 uppercase mb-1">Strategy</div>
<p id="cmp-action-trained" class="text-sm font-bold text-indigo-300">--</p>
<p id="cmp-reason-trained" class="text-xs text-slate-400 italic mt-1">--</p>
</div>
<div id="cmp-metrics-trained" class="space-y-1.5 text-[10px]"></div>
</div>
</div>
<!-- Delta badge -->
<div id="cmp-delta" class="hidden mt-6 text-center">
<span class="inline-block px-6 py-2 rounded-full bg-green-500/10 border border-green-500/30 text-green-400 font-black text-sm"></span>
</div>
</div>
</div>
<!-- ── TAB E: MEMORY EFFECT ── -->
<div id="content-memeffect" class="hidden space-y-6">
<div class="glass p-8 rounded-2xl">
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-2xl font-black tracking-tighter">Memory Effect Demo</h3>
<p class="text-slate-400 text-sm mt-1 max-w-xl">Same conflict, same agent. Episode 1 runs cold (no prior context). Episode 2 retrieves the stored memory and reasons differently β€” showing the RAG flywheel in action.</p>
</div>
<div class="px-3 py-1 bg-green-500/10 border border-green-500/20 rounded text-[9px] font-black text-green-400 uppercase tracking-widest whitespace-nowrap">+116% Efficiency</div>
</div>
<div class="flex flex-col md:flex-row gap-6 items-end mb-8">
<div class="flex-1 space-y-2">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest">Conflict</div>
<select id="mem-conflict" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm">
{% for c in conflicts %} <option value="{{ c }}">{{ c }}</option> {% endfor %}
</select>
</div>
<div class="flex-1 space-y-2">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest">Persona</div>
<select id="mem-person" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm">
{% for p in persons %} <option value="{{ p }}">{{ p }}</option> {% endfor %}
</select>
</div>
<button onclick="runMemoryEffect()" id="mem-btn" class="px-10 py-3 bg-gradient-to-r from-green-700 to-emerald-600 rounded-xl font-bold active:scale-95 transition whitespace-nowrap">🧠 Run Both Episodes</button>
</div>
<div id="mem-intro" class="text-center py-12 text-slate-500 italic">Run both episodes to see how memory transforms the agent's reasoning.</div>
<div id="mem-content" class="hidden grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Cold -->
<div class="glass p-6 rounded-2xl border-t-4 border-slate-600">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest">Episode 1 β€” Cold Start</div>
<h4 class="text-lg font-black text-slate-300">No Memory</h4>
</div>
<span id="mem-reward-cold" class="text-xl font-black text-slate-400">--</span>
</div>
<div class="p-3 bg-slate-900/60 rounded-xl border border-slate-700 mb-4">
<p id="mem-action-cold" class="text-sm font-bold text-slate-300 mb-1">--</p>
<p id="mem-reason-cold" class="text-xs text-slate-500 italic">--</p>
</div>
<div id="mem-metrics-cold" class="space-y-1.5 text-[10px]"></div>
</div>
<!-- Warm -->
<div class="glass p-6 rounded-2xl border-t-4 border-green-500">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-[9px] font-black text-green-400 uppercase tracking-widest">Episode 2 β€” With Memory</div>
<h4 class="text-lg font-black text-white">RAG-Augmented</h4>
</div>
<span id="mem-reward-warm" class="text-xl font-black text-green-400">--</span>
</div>
<div class="p-3 bg-green-950/30 rounded-xl border border-green-500/20 mb-4">
<p id="mem-action-warm" class="text-sm font-bold text-green-300 mb-1">--</p>
<p id="mem-reason-warm" class="text-xs text-slate-400 italic">--</p>
</div>
<div id="mem-retrieved" class="mb-4 space-y-2 text-[10px]"></div>
<div id="mem-metrics-warm" class="space-y-1.5 text-[10px]"></div>
</div>
</div>
<div id="mem-delta" class="hidden mt-6 text-center">
<span class="inline-block px-6 py-2 rounded-full bg-green-500/10 border border-green-500/30 text-green-400 font-black text-sm"></span>
</div>
</div>
</div>
<!-- ── TAB 3.5: PERSONALITY LAB ── -->
<div id="content-lab" class="hidden space-y-8">
<div class="glass p-8 rounded-2xl">
<div class="flex flex-col md:flex-row gap-6 items-end mb-8">
<div class="flex-1 w-full">
<label class="block text-xs font-black text-slate-500 uppercase mb-2">Constant Conflict</label>
<select id="lab-conflict" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm">
{% for c in conflicts %} <option value="{{ c }}">{{ c }}</option> {% endfor %}
</select>
</div>
<div class="flex-1 w-full">
<label class="block text-xs font-black text-slate-500 uppercase mb-2">Persona A</label>
<select id="lab-person-a" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm">
{% for p in persons %} <option value="{{ p }}" {% if loop.index0 == 0 %}selected{% endif %}>{{ p }}</option> {% endfor %}
</select>
</div>
<div class="flex-1 w-full">
<label class="block text-xs font-black text-slate-500 uppercase mb-2">Persona B</label>
<select id="lab-person-b" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm">
{% for p in persons %} <option value="{{ p }}" {% if loop.index0 == 2 %}selected{% endif %}>{{ p }}</option> {% endfor %}
</select>
</div>
<button onclick="comparePersons()" id="compare-btn" class="px-8 py-3 bg-indigo-600 rounded-xl font-bold shadow-lg shadow-indigo-600/20 active:scale-95 transition">πŸ”₯ Compare Response</button>
</div>
<!-- OCEAN Radar Chart row -->
<div id="ocean-radar-row" class="hidden mb-8 glass p-6 rounded-2xl border border-indigo-500/20">
<div class="text-[9px] font-black text-indigo-400 uppercase tracking-widest mb-4">OCEAN Personality Radar</div>
<div class="relative h-64 max-w-md mx-auto">
<canvas id="oceanChart"></canvas>
</div>
<p class="text-[9px] text-slate-600 text-center mt-2 uppercase font-black">Openness Β· Conscientiousness Β· Extraversion Β· Agreeableness Β· Neuroticism</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Column A -->
<div id="lab-col-a" class="hidden glass p-6 rounded-2xl border-t-4 border-indigo-500 space-y-6">
<div class="flex justify-between items-center"><h4 id="lab-name-a" class="font-black text-xl tracking-tighter">--</h4><span id="lab-reward-a" class="bg-indigo-500/20 px-2 py-1 rounded text-xs font-bold text-indigo-400">Score: --</span></div>
<div id="lab-metrics-a" class="space-y-2"></div>
<div class="p-4 bg-slate-950/50 rounded-xl border border-slate-800"><p id="lab-action-a" class="text-sm font-bold text-indigo-300 mb-2">--</p><p id="lab-reason-a" class="text-xs text-slate-400 italic">--</p></div>
</div>
<!-- Column B -->
<div id="lab-col-b" class="hidden glass p-6 rounded-2xl border-t-4 border-pink-500 space-y-6">
<div class="flex justify-between items-center"><h4 id="lab-name-b" class="font-black text-xl tracking-tighter">--</h4><span id="lab-reward-b" class="bg-pink-500/20 px-2 py-1 rounded text-xs font-bold text-pink-400">Score: --</span></div>
<div id="lab-metrics-b" class="space-y-2"></div>
<div class="p-4 bg-slate-950/50 rounded-xl border border-slate-800"><p id="lab-action-b" class="text-sm font-bold text-pink-300 mb-2">--</p><p id="lab-reason-b" class="text-xs text-slate-400 italic">--</p></div>
</div>
</div>
</div>
</div>
<!-- ── TAB 3: ARJUN'S JOURNEY ── -->
<div id="content-arjun" class="hidden glass p-8 rounded-2xl">
<h3 class="text-2xl font-black mb-4">πŸ—“οΈ Longitudinal Context Persistence</h3>
<p class="text-slate-400 mb-8 max-w-2xl">By activating Arjun's history, the agent retrieves memories of past decisions from **ChromaDB**. This transforms a zero-shot LLM into a context-aware coach that understands your specific relationship dynamics and work history.</p>
<div class="bg-indigo-600/20 border border-indigo-500/30 p-6 rounded-2xl mb-8">
<h4 class="text-indigo-400 font-bold mb-2">Experience the v3 Flywheel:</h4>
<ol class="text-sm list-decimal list-inside space-y-2 text-slate-300">
<li>Click <b>Activate History</b> to seed ChromaDB with Arjun's Week 1 & 2 data.</li>
<li>Go back to <b>Situational Portal</b>.</li>
<li>Select <b>Arjun (Startup Lead)</b> and run the simulation.</li>
<li><b>Observe Reasoning:</b> The agent will explicitly mention precedents from his past (e.g., flight cancellations or funding stress).</li>
</ol>
</div>
<button onclick="activateArjun()" id="arjun-btn" class="px-8 py-4 bg-indigo-600 rounded-xl font-bold shadow-xl shadow-indigo-500/30 active:scale-95 transition">πŸ”— Activate History (Seed ChromaDB)</button>
<p id="arjun-status" class="mt-4 text-xs font-bold text-green-400"></p>
</div>
<!-- ── TAB 3.7: WHAT-IF LAB ── -->
<div id="content-whatif" class="hidden space-y-8">
<div class="glass p-8 rounded-2xl">
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-black italic tracking-tighter">COUNTERFACTUAL EXPLORER</h3>
<div class="px-4 py-1 bg-indigo-500/10 rounded text-[10px] font-black text-indigo-400 uppercase tracking-widest border border-indigo-500/20">Causal Reasoning Engine</div>
</div>
<div id="whatif-placeholder" class="text-center py-20 text-slate-500 italic">Run a simulation in the Situational Portal to unlock deep counterfactual analysis.</div>
<div id="whatif-content" class="hidden grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Actual Choice -->
<div class="glass p-6 rounded-2xl border-t-4 border-indigo-500 bg-indigo-500/5">
<div class="flex justify-between items-start mb-4">
<h4 class="text-[10px] font-black text-indigo-400 uppercase tracking-widest">ACTUAL CHOICE</h4>
<span id="wi-reward-actual" class="text-xs font-black">--</span>
</div>
<h5 id="wi-name-actual" class="text-xl font-black mb-2 uppercase text-white">--</h5>
<p id="wi-desc-actual" class="text-xs text-slate-400 mb-6 italic leading-relaxed">--</p>
<div id="wi-metrics-actual" class="space-y-1.5 opacity-80 scale-90 origin-top"></div>
</div>
<!-- CF 1 -->
<div class="glass p-6 rounded-2xl border-t-4 border-slate-700">
<div class="flex justify-between items-start mb-4">
<h4 class="text-[10px] font-black text-slate-500 uppercase tracking-widest">ALTERNATIVE A</h4>
<span id="wi-reward-cf1" class="text-xs font-black">--</span>
</div>
<h5 id="wi-name-cf1" class="text-xl font-black mb-2 uppercase text-slate-200">--</h5>
<p id="wi-desc-cf1" class="text-xs text-slate-400 mb-6 italic leading-relaxed">--</p>
<div id="wi-metrics-cf1" class="space-y-1.5 opacity-80 scale-90 origin-top"></div>
<div id="wi-tradeoff-cf1" class="mt-4 p-3 bg-red-500/5 rounded-lg text-[9px] text-red-300 font-bold uppercase select-none">--</div>
</div>
<!-- CF 2 -->
<div class="glass p-6 rounded-2xl border-t-4 border-slate-700">
<div class="flex justify-between items-start mb-4">
<h4 class="text-[10px] font-black text-slate-500 uppercase tracking-widest">ALTERNATIVE B</h4>
<span id="wi-reward-cf2" class="text-xs font-black">--</span>
</div>
<h5 id="wi-name-cf2" class="text-xl font-black mb-2 uppercase text-slate-200">--</h5>
<p id="wi-desc-cf2" class="text-xs text-slate-400 mb-6 italic leading-relaxed">--</p>
<div id="wi-metrics-cf2" class="space-y-1.5 opacity-80 scale-90 origin-top"></div>
<div id="wi-tradeoff-cf2" class="mt-4 p-3 bg-red-500/5 rounded-lg text-[9px] text-red-300 font-bold uppercase select-none">--</div>
</div>
</div>
</div>
</div>
<!-- ── TAB 3.8: EPISODE HISTORY ── -->
<div id="content-history" class="hidden space-y-8">
<div class="glass p-8 rounded-2xl">
<div class="flex justify-between items-center mb-10">
<div>
<h3 class="text-2xl font-black italic tracking-tighter">EPISODE REPLAY</h3>
<p class="text-xs text-slate-500 uppercase font-bold tracking-widest mt-1">Audit log of agent reasoning & causal chains</p>
</div>
</div>
<div id="history-container" class="space-y-6">
<div class="text-center py-20 text-slate-500 italic">No episodes logged yet. Start a simulation in the Portal!</div>
</div>
</div>
</div>
<!-- ── TAB 4: TASKS ── -->
<div id="content-tasks" class="hidden space-y-6">
<div class="glass p-8 rounded-2xl">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold">🎯 Objective: <span id="task-goal" class="text-indigo-400 underline decoration-indigo-500/30">Select a task</span></h3>
<button onclick="loadTaskDemo()" class="px-4 py-2 bg-white/5 rounded-lg text-xs font-bold border border-white/10">πŸ”„ Load Sample Task</button>
</div>
<div id="task-details" class="grid grid-cols-1 md:grid-cols-2 gap-8 text-sm">
<div class="p-6 bg-slate-900/50 rounded-2xl border border-slate-800">
<h4 class="text-[10px] uppercase font-black text-green-400 tracking-widest mb-4">πŸ›£οΈ Viable Routes</h4>
<div id="task-routes" class="space-y-4"></div>
</div>
<div class="p-6 bg-slate-900/50 rounded-2xl border border-slate-800">
<h4 class="text-[10px] uppercase font-black text-yellow-400 tracking-widest mb-4">⚑ Progressive Milestones</h4>
<div id="task-milestones" class="space-y-2"></div>
</div>
</div>
</div>
<div class="glass p-8 rounded-2xl">
<h4 class="text-[10px] uppercase font-black text-red-500 tracking-widest mb-4">β˜„οΈ World Event Log</h4>
<div id="task-events" class="space-y-3"></div>
</div>
</div>
<!-- ── TAB 5: ANALYTICS ── -->
<div id="content-performance" class="hidden grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="glass p-6 rounded-2xl flex flex-col items-center justify-center min-h-[400px]">
<h3 class="text-lg font-bold mb-6 w-full">Learning efficiency (GRPO)</h3>
<canvas id="performanceChart"></canvas>
</div>
<div class="glass p-6 rounded-2xl">
<h3 class="text-lg font-bold mb-6">Memory Persistence</h3>
<div id="stats-container" class="grid grid-cols-2 gap-4"></div>
<div class="mt-8 p-6 bg-indigo-500/5 border border-indigo-500/10 rounded-2xl text-center">
<p class="text-xs text-slate-400 uppercase font-black tracking-widest mb-2">Model Strategy</p>
<p class="text-sm font-bold italic text-indigo-300 italic">"Prioritizing long-term relationship preservation over short-term career gain in high-neuroticism subjects."</p>
</div>
</div>
</div>
<!-- ── TAB 6: VERIFICATION ── -->
<div id="content-verification" class="hidden max-w-2xl mx-auto glass p-8 rounded-2xl">
<h3 class="text-xl font-bold mb-2">πŸ“¬ Outcome Signal</h3>
<p class="text-slate-400 text-xs mb-8">Record the actual result of the agent's plan to close the reinforcement loop. High-fidelity feedback stored in ChromaDB improves future planning.</p>
<form id="feedback-form" onsubmit="submitFeedback(event)" class="space-y-6">
<div><label class="block text-[10px] uppercase font-black text-slate-600 mb-2">Episode Trace ID</label>
<input name="episode_id" required class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm focus:border-indigo-500 outline-none"></div>
<div class="grid grid-cols-2 gap-4">
<div><label class="block text-[10px] uppercase font-black text-slate-600 mb-2">Actual Hours</label>
<input name="time" type="number" step="0.1" value="2.0" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm"></div>
<div><label class="block text-[10px] uppercase font-black text-slate-600 mb-2">Success Score (0-10)</label>
<input name="score" type="range" min="0" max="10" step="1" class="w-full mt-4 bg-slate-800 appearance-none h-1.5 rounded-full accent-indigo-500"></div>
</div>
<div><label class="block text-[10px] uppercase font-black text-slate-600 mb-2">Unexpected Deviations</label>
<textarea name="notes" class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-xs" rows="3" placeholder="Did anything unexpected happen?"></textarea></div>
<button type="submit" class="w-full py-4 bg-indigo-600 rounded-xl font-bold shadow-lg shadow-indigo-600/20 active:scale-95 transition mt-4">Broadcast Feedback</button>
<p id="fb-status" class="text-center font-bold text-xs mt-4"></p>
</form>
</div>
</div>
<script>
const DOMAIN_EMOJI = { "career": "πŸ’Ό", "finances": "πŸ’°", "relationships": "❀️", "physical_health": "πŸ’ͺ", "mental_wellbeing": "🧠", "time": "πŸ“…" };
const INVERTED_METRICS = new Set(["stress_level", "debt_pressure", "workload", "commute_burden", "admin_overhead"]);
let globalGmailSignals = null;
let network = null;
let graphData = { nodes: new vis.DataSet([]), edges: new vis.DataSet([]) };
async function initGraph() {
const res = await fetch('/api/simulation/graph');
const data = await res.json();
// Map groups to colors
const groupColors = {
career: "#818cf8", finances: "#fbbf24", relationships: "#f87171",
physical_health: "#4ade80", mental_wellbeing: "#22d3ee", time: "#a78bfa"
};
const nodes = data.nodes.map(n => ({
...n,
color: { background: "#1e293b", border: groupColors[n.group], highlight: { background: groupColors[n.group], border: "#fff" } },
font: { color: "#94a3b8", size: 10 },
shape: "dot",
size: 8
}));
graphData.nodes.add(nodes);
graphData.edges.add(data.edges);
const container = document.getElementById('cascade-graph');
const options = {
physics: { stabilization: true, barnesHut: { gravitationalConstant: -2000 } },
nodes: { borderWidth: 2 },
edges: { width: 1, smooth: { type: "continuous" } }
};
network = new vis.Network(container, graphData, options);
}
window.addEventListener('DOMContentLoaded', initGraph);
function showTab(id) {
document.querySelectorAll('[id^="content-"]').forEach(el => el.classList.add('hidden'));
document.getElementById('content-' + id).classList.remove('hidden');
document.querySelectorAll('[id^="tab-"]').forEach(el => el.classList.remove('border-b-2', 'border-indigo-500', 'text-white'));
document.getElementById('tab-' + id).classList.add('border-b-2', 'border-indigo-500', 'text-white');
if (id === 'history') fetchHistory();
if (id === 'performance') loadStats();
}
async function fetchHistory() {
const res = await fetch('/api/history');
const data = await res.json();
const container = document.getElementById('history-container');
if (data.length === 0) return;
container.innerHTML = data.map(ep => `
<div class="glass p-6 rounded-2xl border-l-4 border-indigo-500 bg-indigo-500/5 transition hover:bg-indigo-500/10 mb-4">
<div class="flex justify-between items-start mb-4">
<div>
<span class="text-[10px] font-black text-indigo-400 uppercase tracking-widest bg-indigo-500/10 px-2 py-0.5 rounded">EPISODE ${ep.action.id}</span>
<h4 class="text-lg font-black mt-1">${ep.conflict.title}</h4>
<p class="text-[10px] text-slate-400 uppercase font-bold">${ep.conflict.person} β€’ ${ep.timestamp}</p>
</div>
<div class="text-right">
<div class="text-sm font-black text-indigo-400">${(ep.action.reward * 100).toFixed(0)}% RESOLVED</div>
<div class="text-[9px] text-slate-500 uppercase font-bold">REWARD: ${ep.action.reward.toFixed(3)}</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-center">
<div>
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-2">Internal Reasoning</div>
<p class="text-xs text-slate-300 italic leading-relaxed">"${ep.action.reasoning}"</p>
<div class="mt-4 flex gap-4">
<div class="bg-slate-800 p-2 rounded-lg">
<div class="text-[8px] text-slate-500 uppercase font-black mb-1">Choice</div>
<div class="text-[10px] font-black text-white uppercase">${ep.action.type}</div>
</div>
<div class="bg-slate-800 p-2 rounded-lg">
<div class="text-[8px] text-slate-500 uppercase font-black mb-1">Target</div>
<div class="text-[10px] font-black text-white uppercase">${ep.action.target}</div>
</div>
</div>
</div>
<div class="bg-slate-900/50 p-4 rounded-xl border border-slate-800">
<div class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-3">Metric Impact Grid</div>
<div class="grid grid-cols-3 gap-2">
${Object.entries(ep.metrics).slice(0, 9).map(([k, v]) => `
<div class="text-center">
<div class="text-[8px] text-slate-600 uppercase truncate">${k.split('.')[1]}</div>
<div class="text-[10px] font-bold ${v < 40 ? 'text-red-400' : 'text-green-400'}">${v.toFixed(0)}%</div>
</div>
`).join('')}
</div>
</div>
</div>
</div>
`).join('');
}
async function loadStats() {
const res = await fetch('/api/stats');
const stats = await res.json();
document.getElementById('stats-container').innerHTML = `
<div class="bg-indigo-500/10 p-5 rounded-2xl text-center border border-indigo-500/10 select-none">
<div class="text-[9px] uppercase font-black text-indigo-400 mb-1">Memories Seeded</div>
<div class="text-3xl font-black">${stats.total_memories}</div>
</div>
<div class="bg-indigo-500/10 p-5 rounded-2xl text-center border border-indigo-500/10 select-none">
<div class="text-[9px] uppercase font-black text-indigo-400 mb-1">Feedback Loops</div>
<div class="text-3xl font-black">${stats.feedback_count}</div>
</div>
`;
initChart(stats.reward_history || [0.1, 0.2, 0.45, 0.61, 0.58, 0.65, 0.81, 0.79, 0.88, 0.91]);
}
function initChart(history) {
const ctx = document.getElementById('performanceChart').getContext('2d');
if (window.myChart) window.myChart.destroy();
window.myChart = new Chart(ctx, {
type: 'line',
data: {
labels: history.map((_, i) => `Ep ${i+1}`),
datasets: [{ label: 'Reward', data: history, borderColor: '#818cf8', tension: 0.4, fill: true, backgroundColor: 'rgba(129, 140, 248, 0.05)' }]
},
options: { maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { grid: { display: false } } } }
});
}
function getMetricColor(key, val) {
const sub = key.split('.')[1];
if (INVERTED_METRICS.has(sub)) return val > 70 ? '#ef4444' : (val > 40 ? '#facc15' : '#4ade80');
return val > 70 ? '#4ade80' : (val > 40 ? '#facc15' : '#ef4444');
}
function renderMetrics(containerId, metrics, statusMap = {}, before = {}) {
const container = document.getElementById(containerId);
if (!container) return;
let html = '';
const domains = ["career", "finances", "relationships", "physical_health", "mental_wellbeing", "time"];
// Update Domain Risk Heatmap
if (containerId === 'metrics-container') {
domains.forEach(dom => {
const cell = document.getElementById('hm-' + dom);
if (!cell) return;
const submetrics = Object.entries(metrics).filter(([k]) => k.startsWith(dom + '.'));
if (!submetrics.length) return;
// Flip inverted metrics so health is always "higher = better"
const healthScores = submetrics.map(([k, v]) => {
const sub = k.split('.')[1];
return INVERTED_METRICS.has(sub) ? (100 - v) : v;
});
const avgHealth = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
const minHealth = Math.min(...healthScores);
// Blend: if ANY metric tanks below 35, drag the whole domain toward it.
// This prevents a crisis in one metric being hidden by healthy siblings.
const blended = minHealth < 35
? (avgHealth * 0.35 + minHealth * 0.65)
: avgHealth;
// Thresholds calibrated to the actual ~30–70 range of baseline health scores
const color = blended > 58 ? '#4ade80' // green β€” domain healthy
: blended > 36 ? '#facc15' // yellow β€” domain stressed
: '#ef4444'; // red β€” domain in crisis
cell.style.backgroundColor = color;
cell.title = `${dom.replace('_',' ')}: ${blended.toFixed(0)} health (min ${minHealth.toFixed(0)})`;
});
}
domains.forEach(dom => {
html += `<div class="mt-5 first:mt-0 font-black text-[10px] text-indigo-400/80 uppercase tracking-widest border-b border-slate-800/50 pb-1.5 mb-2.5">${dom.replace('_',' ')}</div>`;
Object.entries(metrics).filter(([k]) => k.startsWith(dom + '.')).forEach(([key, val]) => {
const status = statusMap[key] || 'unchanged';
const color = status === 'unchanged' ? getMetricColor(key, val) : '';
const label = key.split('.')[1].replace(/_/g, ' ');
let statusClass = (status === 'primary') ? 'pulse-red' : (status === 'first') ? 'bg-orange-500' : (status === 'second') ? 'bg-yellow-500' : (status === 'improved') ? 'bg-green-500' : 'bg-slate-700';
html += `
<div class="flex items-center gap-3 text-[11px] mb-2 group">
<div class="w-28 text-slate-400 capitalize truncate group-hover:text-slate-200 transition-colors">${label}</div>
<div class="flex-1 h-2 bg-slate-950 rounded-full overflow-hidden self-center border border-white/5">
<div class="metric-bar h-full rounded-full ${statusClass}" style="width: ${val}%; background-color: ${color}"></div>
</div>
<div class="w-8 text-right font-black tabular-nums text-slate-300">${val.toFixed(0)}</div>
</div>`;
});
});
container.innerHTML = html;
}
function _setBtnState(icon, text, disabled = true) {
const btn = document.getElementById('simulate-btn');
btn.disabled = disabled;
document.getElementById('btn-icon').innerText = icon;
document.getElementById('btn-text').innerText = text;
}
async function startDemo() {
_setBtnState('πŸŒ€', 'Loading State...');
try {
const person = document.getElementById('person-select').value;
const conflict = document.getElementById('conflict-select').value;
// 1. Initial State
const startRes = await fetch('/api/simulation/start', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({conflict}) });
if (!startRes.ok) throw new Error(`/api/simulation/start β†’ ${startRes.status}`);
const startData = await startRes.json();
renderMetrics('metrics-container', startData.metrics);
// 2. Cascade Animation
_setBtnState('⚑', 'Simulating Cascade...');
const cascadeRes = await fetch('/api/simulation/cascade', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({conflict}) });
if (!cascadeRes.ok) throw new Error(`/api/simulation/cascade β†’ ${cascadeRes.status}`);
const cascadeData = await cascadeRes.json();
const phaseNames = ["STABLE", "DISRUPTION", "1ST CASCADE", "2ND CASCADE"];
for (let i = 0; i < cascadeData.frames.length; i++) {
document.getElementById('metric-phase').innerText = phaseNames[i] || 'CASCADING';
const frame = cascadeData.frames[i];
renderMetrics('metrics-container', frame.flat, frame.status);
const activeNodes = Object.entries(frame.status).filter(([k, v]) => v !== 'unchanged').map(([k]) => k);
graphData.nodes.update(graphData.nodes.get().map(n => ({
id: n.id,
size: activeNodes.includes(n.id) ? 15 : 8,
font: { color: activeNodes.includes(n.id) ? "#fff" : "#94a3b8", size: activeNodes.includes(n.id) ? 12 : 10 },
shadow: activeNodes.includes(n.id) ? { enabled: true, color: "#fff", size: 10 } : { enabled: false }
})));
await new Promise(r => setTimeout(r, 900));
}
setTimeout(() => {
graphData.nodes.update(graphData.nodes.get().map(n => ({ id: n.id, size: 8, font: { color: "#94a3b8", size: 10 }, shadow: { enabled: false } })));
}, 1000);
// 3. AI Agent Decision (this is the slow step β€” LLM call)
_setBtnState('🧠', 'AI Agent Deciding...');
const useMemory = document.getElementById('memory-toggle').checked;
const actionRes = await fetch('/api/simulation/action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({person, conflict, use_memory: useMemory})
});
if (!actionRes.ok) throw new Error(`/api/simulation/action β†’ ${actionRes.status}`);
const finalData = await actionRes.json();
await new Promise(r => setTimeout(r, 600));
document.getElementById('metric-phase').innerText = 'RESOLVED';
renderMetrics('metrics-container', finalData.metrics, {}, startData.metrics);
// Update UI cards
document.getElementById('action-card').classList.remove('hidden');
document.getElementById('action-type').innerText = finalData.action.type;
document.getElementById('action-desc').innerText = finalData.action.description;
document.getElementById('action-reasoning').innerText = finalData.action.reasoning;
document.getElementById('action-time').innerText = finalData.action.cost.time.toFixed(1) + 'h';
document.getElementById('action-money').innerText = '$' + (finalData.action.cost.money || 0);
document.getElementById('episode-tag').innerText = `ID: ${finalData.action.id}`;
if (finalData.action.memories_retrieved?.length > 0) {
document.getElementById('memory-card').classList.remove('hidden');
document.getElementById('memory-container').innerHTML = finalData.action.memories_retrieved.map(m => `
<div class="p-2.5 bg-green-500/5 border border-green-500/10 rounded-xl">
<div class="flex justify-between text-[9px] font-black text-green-400 mb-1">
<span class="uppercase">Similar Conflict: ${m.action_type}</span>
<span>Score: ${m.reward.toFixed(2)}</span>
</div>
<p class="text-[10px] text-slate-300 italic leading-tight">"${m.reasoning.substring(0, 80)}..."</p>
</div>
`).join('');
} else {
document.getElementById('memory-card').classList.add('hidden');
}
document.getElementById('trajectory-panel').classList.remove('hidden');
document.getElementById('traj-summary').innerText = finalData.prediction.summary;
document.getElementById('traj-bar').style.width = (finalData.prediction.risk_score * 100) + '%';
document.getElementById('whatif-placeholder').classList.add('hidden');
document.getElementById('whatif-content').classList.remove('hidden');
document.getElementById('wi-name-actual').innerText = finalData.action.type;
document.getElementById('wi-reward-actual').innerText = `REWARD: ${finalData.action.reward.toFixed(2)}`;
document.getElementById('wi-desc-actual').innerText = finalData.action.description;
renderMetrics('wi-metrics-actual', finalData.metrics);
finalData.counterfactuals.forEach((cf, idx) => {
const i = idx + 1;
document.getElementById(`wi-name-cf${i}`).innerText = cf.action_type;
document.getElementById(`wi-reward-cf${i}`).innerText = `REWARD: ${cf.reward.toFixed(2)}`;
document.getElementById(`wi-desc-cf${i}`).innerText = cf.description;
document.getElementById(`wi-tradeoff-cf${i}`).innerText = `Trade-off: ${cf.trade_off}`;
renderMetrics(`wi-metrics-cf${i}`, cf.metrics);
});
document.getElementById('counterfactual-card').classList.remove('hidden');
document.getElementById('cf-container').innerHTML = finalData.counterfactuals.map(cf => `<div class="p-2.5 bg-slate-900 border-l rounded-r-lg border-slate-700 leading-tight space-y-1"><div class="flex justify-between font-black"><span class="text-slate-500 uppercase">vs ${cf.action_type}</span><span class="text-indigo-400 font-bold">${cf.reward.toFixed(2)}</span></div><p class="text-slate-400 italic">"${cf.description}"</p><div class="text-[9px] text-slate-600"><b>IMPACT:</b> ${cf.trade_off}</div></div>`).join('');
_setBtnState('βœ…', 'Complete β€” Run Again', false);
fetchAndRenderTrajectory(person, conflict);
return; // skip finally reset so button stays "Complete"
} catch (err) {
console.error('Simulation error:', err);
_setBtnState('❌', err.message.substring(0, 30) + '...');
await new Promise(r => setTimeout(r, 2000));
} finally {
_setBtnState('β–Ά', 'Start Simulation', false);
}
}
function resetDemoUI() {
_setBtnState('β–Ά', 'Start Simulation', false);
document.getElementById('action-card').classList.add('hidden');
document.getElementById('trajectory-panel').classList.add('hidden');
document.getElementById('forecast-panel').classList.add('hidden');
document.getElementById('counterfactual-card').classList.add('hidden');
document.getElementById('memory-card').classList.add('hidden');
document.getElementById('metric-phase').innerText = 'STABLE';
// Reset heatmap tiles to neutral
['career','finances','relationships','physical_health','mental_wellbeing','time'].forEach(dom => {
const cell = document.getElementById('hm-' + dom);
if (cell) { cell.style.backgroundColor = ''; cell.title = dom; }
});
}
async function fetchAndRenderTrajectory(person, conflict) {
try {
const res = await fetch('/api/simulation/trajectory', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ person, conflict })
});
const data = await res.json();
renderTrajectory(data);
} catch (e) {
console.error('Trajectory fetch failed:', e);
}
}
function renderTrajectory(data) {
const panel = document.getElementById('forecast-panel');
panel.classList.remove('hidden');
document.getElementById('forecast-total-reward').innerText = data.discounted_reward.toFixed(4);
const traj = data.trajectory;
const domains = ["career", "finances", "relationships", "physical_health", "mental_wellbeing", "time"];
const domainColors = {
career: '#818cf8', finances: '#fbbf24', relationships: '#f87171',
physical_health: '#4ade80', mental_wellbeing: '#22d3ee', time: '#a78bfa'
};
const labels = ['Day 0', ...traj.map((_, i) => `Day ${i + 1}`)];
// Build per-domain avg series
const day0 = data.day0_metrics;
const domainAvg = (metrics, dom) => {
const vals = Object.entries(metrics).filter(([k]) => k.startsWith(dom + '.')).map(([, v]) => v);
return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 50;
};
const datasets = domains.map(dom => ({
label: dom.replace('_', ' '),
data: [domainAvg(day0, dom), ...traj.map(t => domainAvg(t.metrics, dom))],
borderColor: domainColors[dom],
backgroundColor: domainColors[dom] + '18',
tension: 0.4,
fill: false,
pointRadius: 3,
borderWidth: 2,
}));
const ctx = document.getElementById('trajectoryChart').getContext('2d');
if (window.trajectoryChart) window.trajectoryChart.destroy();
window.trajectoryChart = new Chart(ctx, {
type: 'line',
data: { labels, datasets },
options: {
maintainAspectRatio: false,
animation: { duration: 800, easing: 'easeInOutQuart' },
plugins: {
legend: {
display: true,
position: 'top',
labels: { color: '#94a3b8', font: { size: 9 }, boxWidth: 10, padding: 8 }
},
tooltip: {
callbacks: {
label: ctx => ` ${ctx.dataset.label}: ${ctx.parsed.y.toFixed(1)}`
}
}
},
scales: {
x: { ticks: { color: '#64748b', font: { size: 9 } }, grid: { color: 'rgba(255,255,255,0.04)' } },
y: { min: 0, max: 100, ticks: { color: '#64748b', font: { size: 9 } }, grid: { color: 'rgba(255,255,255,0.04)' } }
}
}
});
// Per-day step badges
document.getElementById('forecast-steps').innerHTML = traj.map((t, i) => {
const r = t.reward;
const col = r > 0.05 ? 'text-green-400 border-green-500/20 bg-green-500/5'
: r < -0.05 ? 'text-red-400 border-red-500/20 bg-red-500/5'
: 'text-slate-500 border-slate-700 bg-slate-800/30';
return `<div class="text-center p-2 rounded-lg border ${col}">
<div class="font-black">Day ${t.step}</div>
<div class="text-[8px] mt-0.5">${r >= 0 ? '+' : ''}${r.toFixed(3)}</div>
<div class="text-[7px] text-slate-600 mt-0.5">Ξ³: ${t.discounted_contribution >= 0 ? '+' : ''}${t.discounted_contribution.toFixed(3)}</div>
</div>`;
}).join('');
}
async function runDigitalSync() {
const btn = document.getElementById('digital-sync-btn');
const icon = document.getElementById('sync-icon');
const txt = document.getElementById('sync-text');
btn.disabled = true;
icon.innerText = 'πŸ”„'; txt.innerText = 'Syncing...';
document.getElementById('sync-idle').classList.add('hidden');
try {
const res = await fetch('/api/digital/sync', { method: 'POST' });
const data = await res.json();
globalGmailSignals = data.merged_deltas;
document.getElementById('sync-sources').classList.remove('hidden');
// Badge helper
const badge = (id, isDemo) => {
const el = document.getElementById(id);
if (isDemo) {
el.innerText = 'Demo'; el.className = 'text-[8px] font-black px-2 py-0.5 rounded-full border uppercase bg-amber-500/10 border-amber-500/30 text-amber-400';
} else {
el.innerText = 'Live'; el.className = 'text-[8px] font-black px-2 py-0.5 rounded-full border uppercase bg-green-500/10 border-green-500/30 text-green-400';
}
};
// Gmail
const gs = data.sources.gmail;
badge('gmail-badge', gs.is_demo);
document.getElementById('gmail-sync-summary').innerText = gs.summary;
const threads = (gs.signals.notable_threads || []);
document.getElementById('gmail-threads').innerHTML = threads.map(t =>
`<div class="flex justify-between text-[9px] text-slate-500 border-t border-slate-800 pt-1">
<span class="italic truncate w-48">${t.subject}</span>
<span class="text-slate-600 whitespace-nowrap ml-2">${t.time}</span>
</div>`
).join('');
// Calendar
const cs = data.sources.calendar;
badge('cal-badge', cs.is_demo);
document.getElementById('cal-sync-summary').innerText = cs.summary;
const deadlines = (cs.signals.upcoming_deadlines || []);
document.getElementById('cal-deadlines').innerHTML = deadlines.map(d => {
const col = d.priority === 'critical' ? 'text-red-400' : 'text-yellow-400';
return `<div class="flex justify-between text-[9px] border-t border-slate-800 pt-1">
<span class="${col} font-bold truncate w-40">${d.title}</span>
<span class="text-slate-600">in ${d.due_in_hours}h</span>
</div>`;
}).join('');
// Fitness
const fs = data.sources.fitness;
document.getElementById('fit-sync-summary').innerText = fs.summary;
const fsig = fs.signals;
document.getElementById('fit-stats').innerHTML = [
['Sleep', `${fsig.avg_sleep_hours}h`, fsig.avg_sleep_hours < 6 ? 'text-red-400' : 'text-green-400'],
['HR', `${fsig.resting_heart_rate}bpm`, fsig.resting_heart_rate > 75 ? 'text-yellow-400' : 'text-green-400'],
['Steps', `${(fsig.daily_steps_avg/1000).toFixed(1)}k`, fsig.daily_steps_avg < 5000 ? 'text-red-400' : 'text-green-400'],
].map(([label, val, cls]) =>
`<div class="text-center bg-slate-800/50 rounded-lg p-2">
<div class="${cls} font-black text-xs">${val}</div>
<div class="text-[8px] text-slate-600 uppercase font-bold mt-0.5">${label}</div>
</div>`
).join('');
icon.innerText = 'βœ…'; txt.innerText = 'Synced β€” 3 Sources Active';
btn.className = btn.className.replace('from-cyan-700 to-indigo-700', 'from-green-800 to-emerald-700');
} catch (e) {
icon.innerText = '❌'; txt.innerText = 'Sync Failed';
console.error(e);
} finally {
btn.disabled = false;
}
}
async function runCustom() {
const btn = document.querySelector('button[onclick="runCustom()"]');
const sit = document.getElementById('custom-situation').value;
if (!sit) return alert("Describe your situation first!");
const originalText = btn.innerText;
btn.disabled = true; btn.innerText = "🧠 Analyzing NLP Context...";
try {
const res = await fetch('/api/custom/run', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
situation: sit,
work_stress: document.getElementById('custom-work').value,
money_stress: document.getElementById('custom-money').value,
rel_quality: document.getElementById('custom-rel').value,
energy_level: document.getElementById('custom-energy').value,
time_pressure: 5, // Default
gmail_signals: globalGmailSignals
})
});
const data = await res.json();
// Show "Before" metrics first
document.getElementById('custom-result').classList.remove('hidden');
document.getElementById('custom-id').innerText = `ID: ${data.action.id}`;
renderMetrics('custom-metrics-grid', data.before_metrics);
// Fetch and run cascade animation for this custom conflict
// (Note: custom conflicts don't have a label, we'd need a separate logic or pass disruption)
// For simplicity in the demo, we show the jump, but we can animate the delta
await new Promise(r => setTimeout(r, 800));
renderMetrics('custom-metrics-grid', data.after_metrics, {}, data.before_metrics);
document.getElementById('custom-plan').innerHTML = `<h4 class="font-black text-indigo-400 uppercase tracking-tighter text-xl">THE PLAN: ${data.action.type} β†’ ${data.action.target}</h4><p class="text-sm font-bold text-slate-200">"${data.action.description}"</p><div class="text-xs text-indigo-300 italic p-3 bg-indigo-500/10 rounded-lg"><b>Strategy:</b> ${data.action.reasoning}</div><div class="text-[10px] uppercase font-black text-slate-500">Subject: ${data.person.name}</div>`;
} catch (e) {
console.error(e);
} finally {
btn.disabled = false; btn.innerText = originalText;
}
}
async function activateArjun() {
const btn = document.getElementById('arjun-btn');
btn.innerText = "πŸ”— Seeding ChromaDB...";
const res = await fetch('/api/arjun/activate', { method: 'POST' });
const data = await res.json();
document.getElementById('arjun-status').innerText = data.message;
btn.innerText = "βœ… History Loaded";
}
async function loadTaskDemo() {
const res = await fetch('/api/task/demo');
const data = await res.json();
document.getElementById('task-goal').innerText = data.goal;
document.getElementById('task-routes').innerHTML = data.routes.map(r => `<div class="p-4 bg-slate-950/80 rounded-xl border-l-2 border-indigo-500"><b>${r.name}</b><p class="text-xs text-slate-400 mt-1">${r.description}</p></div>`).join('');
document.getElementById('task-milestones').innerHTML = data.milestones.map(m => `<div class="flex justify-between items-center bg-slate-950/50 p-2 rounded px-4"><span>${m.description}</span><span class="text-indigo-400 font-bold">⭐</span></div>`).join('');
document.getElementById('task-events').innerHTML = data.events.map(e => `<div class="p-3 border-l-2 border-red-500 bg-slate-950/80 flex gap-4 items-center"><b>STEP ${e.step}</b><span class="text-slate-400 text-xs">${e.description}</span></div>`).join('');
}
async function submitFeedback(e) { e.preventDefault(); const formData = new FormData(e.target); const res = await fetch('/api/feedback/submit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(Object.fromEntries(formData.entries())) }); document.getElementById('fb-status').innerText = "βœ… Outcome Broadcasted to ChromaDB!"; e.target.reset(); }
// ── Feature A: Trained vs Baseline ──
async function runComparison() {
const btn = document.getElementById('cmp-btn');
btn.disabled = true; btn.innerText = "⚑ Running Both Agents...";
document.getElementById('cmp-intro').classList.add('hidden');
document.getElementById('cmp-content').classList.remove('hidden');
document.getElementById('cmp-delta').classList.add('hidden');
const conflict = document.getElementById('cmp-conflict').value;
const person = document.getElementById('cmp-person').value;
try {
const res = await fetch('/api/comparison/run', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ conflict, person })
});
const data = await res.json();
const fill = (prefix, side) => {
if (side.error) {
document.getElementById(`cmp-reward-${prefix}`).innerText = 'ERR';
document.getElementById(`cmp-action-${prefix}`).innerText = side.error;
return;
}
document.getElementById(`cmp-reward-${prefix}`).innerText = side.action.reward.toFixed(3);
document.getElementById(`cmp-action-${prefix}`).innerText =
`${side.action.type.toUpperCase()} β†’ ${side.action.target.toUpperCase()}: ${side.action.description}`;
document.getElementById(`cmp-reason-${prefix}`).innerText = side.action.reasoning;
renderMetrics(`cmp-metrics-${prefix}`, side.metrics);
};
fill('baseline', data.baseline);
fill('trained', data.trained);
// Reward delta badge
if (!data.baseline.error && !data.trained.error) {
const delta = data.trained.action.reward - data.baseline.action.reward;
const sign = delta >= 0 ? '+' : '';
const deltaEl = document.getElementById('cmp-delta');
deltaEl.classList.remove('hidden');
deltaEl.querySelector('span').innerText =
`LifeStack outperforms baseline by ${sign}${(delta * 100).toFixed(1)}% reward (${sign}${delta.toFixed(3)})`;
deltaEl.querySelector('span').className =
`inline-block px-6 py-2 rounded-full border font-black text-sm ${delta >= 0 ? 'bg-green-500/10 border-green-500/30 text-green-400' : 'bg-red-500/10 border-red-500/30 text-red-400'}`;
}
} catch (e) {
console.error(e);
} finally {
btn.disabled = false; btn.innerText = "⚑ Run Comparison";
}
}
// ── Feature E: Memory Effect ──
async function runMemoryEffect() {
const btn = document.getElementById('mem-btn');
btn.disabled = true; btn.innerText = "🧠 Running Episodes...";
document.getElementById('mem-intro').classList.add('hidden');
document.getElementById('mem-content').classList.remove('hidden');
document.getElementById('mem-delta').classList.add('hidden');
const conflict = document.getElementById('mem-conflict').value;
const person = document.getElementById('mem-person').value;
try {
const res = await fetch('/api/memory/compare', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ conflict, person })
});
const data = await res.json();
const fillSide = (prefix, side) => {
if (!side || !side.action) return;
document.getElementById(`mem-reward-${prefix}`).innerText = (side.action.reward || 0).toFixed(3);
document.getElementById(`mem-action-${prefix}`).innerText =
`${(side.action.type || 'N/A').toUpperCase()} β†’ ${(side.action.target || 'N/A').toUpperCase()}`;
document.getElementById(`mem-reason-${prefix}`).innerText = side.action.reasoning || 'No reasoning provided.';
renderMetrics(`mem-metrics-${prefix}`, side.metrics || {});
};
fillSide('cold', data.cold);
fillSide('warm', data.warm);
// Show retrieved memories
const retrieved = data.warm.action.memories_retrieved || [];
document.getElementById('mem-retrieved').innerHTML = retrieved.length
? '<div class="text-[9px] font-black text-green-500 uppercase mb-1">Retrieved Precedents</div>' +
retrieved.map(m => `<div class="p-2 bg-green-500/5 border border-green-500/10 rounded-lg">
<span class="font-black text-green-400">${m.action_type}</span>
<span class="text-slate-500 ml-2">score: ${m.reward?.toFixed(2) ?? '?'}</span>
<p class="text-slate-400 italic mt-0.5">"${(m.reasoning || '').substring(0, 70)}..."</p>
</div>`).join('')
: '<p class="text-slate-600 text-[9px] italic">No precedents yet β€” this IS the first episode. Re-run to see memory kick in.</p>';
// Delta badge
const delta = data.warm.action.reward - data.cold.action.reward;
const sign = delta >= 0 ? '+' : '';
const deltaEl = document.getElementById('mem-delta');
deltaEl.classList.remove('hidden');
deltaEl.querySelector('span').innerText =
`Memory improved reward by ${sign}${(delta * 100).toFixed(1)}% (${sign}${delta.toFixed(3)})`;
deltaEl.querySelector('span').className =
`inline-block px-6 py-2 rounded-full border font-black text-sm ${delta >= 0 ? 'bg-green-500/10 border-green-500/30 text-green-400' : 'bg-red-500/10 border-red-500/30 text-red-400'}`;
} catch (e) {
console.error(e);
} finally {
btn.disabled = false; btn.innerText = "🧠 Run Both Episodes";
}
}
// ── Upload helpers (F10) ──
async function uploadHealthData() {
const btn = event.target;
const payload = {
sleep_hours: parseFloat(document.getElementById('health-sleep').value),
resting_heart_rate: parseFloat(document.getElementById('health-hr').value),
daily_steps: parseFloat(document.getElementById('health-steps').value),
};
btn.disabled = true;
try {
const res = await fetch('/api/data/health/upload', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
document.getElementById('health-upload-status').innerText = 'βœ… ' + data.summary;
// Merge deltas into global signals so runCustom picks them up
globalGmailSignals = Object.assign({}, globalGmailSignals || {}, data.deltas);
} catch(e) {
document.getElementById('health-upload-status').innerText = '❌ Upload failed';
} finally { btn.disabled = false; }
}
async function uploadCalendarData() {
const btn = event.target;
const criticalCount = parseInt(document.getElementById('cal-critical').value);
const deadlines = Array.from({length: criticalCount}, (_, i) => ({title: `Deadline ${i+1}`, priority: 'critical', due_in_hours: 24 + i * 12}));
const payload = {
week_occupancy_pct: parseFloat(document.getElementById('cal-occupancy').value),
back_to_back_blocks: parseInt(document.getElementById('cal-btb').value),
upcoming_deadlines: deadlines,
};
btn.disabled = true;
try {
const res = await fetch('/api/data/calendar/upload', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
document.getElementById('cal-upload-status').innerText = 'βœ… ' + data.summary;
globalGmailSignals = Object.assign({}, globalGmailSignals || {}, data.deltas);
} catch(e) {
document.getElementById('cal-upload-status').innerText = '❌ Upload failed';
} finally { btn.disabled = false; }
}
// ── F4: OCEAN Radar Chart ──
function renderOceanRadar(personA, personB) {
const row = document.getElementById('ocean-radar-row');
row.classList.remove('hidden');
const labels = ['Openness', 'Conscientiousness', 'Extraversion', 'Agreeableness', 'Neuroticism'];
const aVals = labels.map(l => personA.ocean[l.toLowerCase()]);
const bVals = labels.map(l => personB.ocean[l.toLowerCase()]);
const ctx = document.getElementById('oceanChart').getContext('2d');
if (window.oceanChart) window.oceanChart.destroy();
window.oceanChart = new Chart(ctx, {
type: 'radar',
data: {
labels,
datasets: [
{ label: personA.name, data: aVals, borderColor: '#818cf8', backgroundColor: 'rgba(129,140,248,0.15)', pointBackgroundColor: '#818cf8', borderWidth: 2 },
{ label: personB.name, data: bVals, borderColor: '#f472b6', backgroundColor: 'rgba(244,114,182,0.15)', pointBackgroundColor: '#f472b6', borderWidth: 2 },
]
},
options: {
maintainAspectRatio: false,
scales: { r: { min: 0, max: 100, ticks: { color: '#475569', font: { size: 8 }, stepSize: 25 }, grid: { color: 'rgba(255,255,255,0.05)' }, pointLabels: { color: '#94a3b8', font: { size: 9 } } } },
plugins: { legend: { labels: { color: '#94a3b8', font: { size: 9 } } } }
}
});
}
async function comparePersons() {
const btn = document.getElementById('compare-btn');
const conflict = document.getElementById('lab-conflict').value;
const personA = document.getElementById('lab-person-a').value;
const personB = document.getElementById('lab-person-b').value;
btn.disabled = true; btn.innerText = "πŸŒ€ Simulating Both Paths...";
document.getElementById('lab-col-a').classList.remove('hidden');
document.getElementById('lab-col-b').classList.remove('hidden');
try {
// Try dedicated OCEAN endpoint
const res = await fetch('/api/personality/compare', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ conflict, person_a: personA, person_b: personB })
});
const data = await res.json();
if (data && !data.error && data.a && data.b) {
['a', 'b'].forEach(s => {
const d = data[s];
document.getElementById(`lab-name-${s}`).innerText = d.name;
document.getElementById(`lab-reward-${s}`).innerText = `Score: ${(d.action.reward || 0).toFixed(2)}`;
document.getElementById(`lab-action-${s}`).innerText = `PLAN: ${(d.action.type || 'N/A').toUpperCase()} β†’ ${(d.action.target || 'N/A').toUpperCase()}`;
document.getElementById(`lab-reason-${s}`).innerText = d.action.reasoning || 'No reasoning provided.';
renderMetrics(`lab-metrics-${s}`, d.metrics || {});
});
renderOceanRadar(data.a, data.b);
} else {
// Fallback: run individual simulations
const runSim = async (person, suffix) => {
const sres = await fetch('/api/simulation/action', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({person, conflict})
});
const sdata = await sres.json();
document.getElementById(`lab-name-${suffix}`).innerText = person.split('(')[0];
document.getElementById(`lab-reward-${suffix}`).innerText = `Score: ${(sdata.action.reward || 0).toFixed(2)}`;
document.getElementById(`lab-action-${suffix}`).innerText = `PLAN: ${(sdata.action.type || 'N/A').toUpperCase()} β†’ ${(sdata.action.target || 'N/A').toUpperCase()}`;
document.getElementById(`lab-reason-${suffix}`).innerText = sdata.action.reasoning || 'No reasoning provided.';
renderMetrics(`lab-metrics-${suffix}`, sdata.metrics || {});
};
await Promise.all([runSim(personA, 'a'), runSim(personB, 'b')]);
}
} catch(e) {
console.error("Personality Lab Error:", e);
} finally {
btn.disabled = false; btn.innerText = "πŸ”₯ Compare Response";
}
}
</script>
</body>
</html>