| <!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 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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| <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> |
| |
| |
| <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> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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> |
| |
| <div id="forecast-steps" class="grid grid-cols-7 gap-2 mt-4 text-[9px]"></div> |
| </div> |
|
|
| |
| <div id="content-custom" class="hidden space-y-6"> |
| <div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <div class="space-y-4"> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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(); |
| |
| |
| 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"]; |
| |
| |
| 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; |
| |
| |
| 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); |
| |
| |
| |
| const blended = minHealth < 35 |
| ? (avgHealth * 0.35 + minHealth * 0.65) |
| : avgHealth; |
| |
| |
| const color = blended > 58 ? '#4ade80' |
| : blended > 36 ? '#facc15' |
| : '#ef4444'; |
| |
| 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; |
| |
| |
| 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); |
| |
| |
| _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); |
| |
| |
| _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); |
| |
| |
| 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; |
| |
| } 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'; |
| |
| ['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}`)]; |
| |
| |
| 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)' } } |
| } |
| } |
| }); |
| |
| |
| 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'); |
| |
| |
| 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'; |
| } |
| }; |
| |
| |
| 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(''); |
| |
| |
| 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(''); |
| |
| |
| 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, |
| gmail_signals: globalGmailSignals |
| }) |
| }); |
| const data = await res.json(); |
| |
| |
| document.getElementById('custom-result').classList.remove('hidden'); |
| document.getElementById('custom-id').innerText = `ID: ${data.action.id}`; |
| renderMetrics('custom-metrics-grid', data.before_metrics); |
| |
| |
| |
| |
| 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(); } |
| |
| |
| 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); |
| |
| |
| 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"; |
| } |
| } |
| |
| |
| 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); |
| |
| |
| 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>'; |
| |
| |
| 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"; |
| } |
| } |
| |
| |
| 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; |
| |
| 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; } |
| } |
| |
| |
| 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 { |
| |
| 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 { |
| |
| 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> |
|
|