retro / static /app.js
sankalphs's picture
Fix AI insight leaking thinking content + make insights on-demand instead of auto-generated
9cbb438
Raw
History Blame Contribute Delete
28.5 kB
// Retro Alpha - Frontend with improved UX
// NN/g heuristics: system status visibility, recognition > recall,
// error prevention, user control & freedom, minimalist design
(function () {
const E = window.RetroEngine;
const Ev = window.RetroEvents;
const DISPLAY = E.ASSET_DISPLAY_NAMES;
const TRADABLE_DISPLAY = E.TRADABLE_KEYS.map(function (k) { return DISPLAY[k]; });
// DOM refs
function $(id) { return document.getElementById(id); }
var els = {
date: $("date-display"),
llmStatus: $("llm-status"),
llmBadge: $("llm-badge"),
chatLlmBadge: $("chat-llm-badge"),
watchBody: $("watch-body"),
insightText: $("insight-text"),
positionsBody: $("positions-body"),
newsContent: $("news-content"),
agentLog: $("agent-log"),
chartTitle: $("chart-title"),
chart: $("price-chart"),
cashLine: $("cash-line"),
netWorth: $("net-worth"),
investedLine: $("invested-line"),
pnlLine: $("pnl-line"),
tradeForm: $("trade-form"),
tradeBtn: $("trade-btn"),
sideBuy: $("side-buy"),
sideSell: $("side-sell"),
actionInput: $("action"),
amountRange: $("amount-range"),
amountInput: $("amount"),
advanceBtn: $("advance-btn"),
mentorBtn: $("mentor-btn"),
resetBtn: $("reset-btn"),
statusLine: $("status-line"),
modal: $("mentor-modal"),
closeModal: $("close-modal"),
mentorRoast: $("mentor-roast"),
mentorLesson: $("mentor-lesson"),
mentorSuggestion: $("mentor-suggestion"),
chatLog: $("chat-log"),
chatForm: $("chat-form"),
chatInput: $("chat-input"),
progressBar: $("progress-bar"),
progressLabel: $("progress-label"),
goalPct: $("goal-pct"),
onboarding: $("onboarding-overlay"),
onboardStart: $("onboard-start"),
helpBtn: $("help-btn"),
helpModal: $("help-modal"),
closeHelp: $("close-help"),
toastContainer: $("toast-container"),
};
// State
var state = E.newGame();
var chartMode = "networth";
var prevPrices = {};
for (var k in state.prices) prevPrices[k] = state.prices[k];
// Formatters
var fmtMoney = function (n) {
var neg = n < 0 ? "-" : "";
return neg + "\u20b9" + Math.abs(Math.round(n)).toLocaleString("en-IN");
};
var fmtPct = function (n) { return (n >= 0 ? "+" : "") + n.toFixed(2) + "%"; };
var chgClass = function (n) { return n > 0.001 ? "up" : n < -0.001 ? "down" : "flat"; };
// Toast notifications
function toast(msg, type) {
type = type || "info";
var div = document.createElement("div");
div.className = "toast " + type;
div.textContent = msg;
els.toastContainer.appendChild(div);
setTimeout(function () {
if (div.parentNode) div.parentNode.removeChild(div);
}, 3000);
}
// API (LLM only)
async function apiLLM(path, body) {
try {
var r = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error("HTTP " + r.status);
return await r.json();
} catch (e) {
console.error("LLM API failed:", e);
return null;
}
}
async function fetchHealth() {
try {
var r = await fetch("/game-api/health");
return await r.json();
} catch (e) {
return { llm: "error" };
}
}
// --- Rendering ---
function render() {
var s = state;
var total = E.totalValue(s);
var invested = E.investedValue(s);
var pnl = E.totalPnl(s);
var startVal = E.STARTING_CASH;
var goalPct = (total / E.WIN_THRESHOLD) * 100;
// Date & progress
els.date.textContent = s.year + "-" + String(s.month).padStart(2, "0");
var monthsDone = s.months_elapsed;
var pct = (monthsDone / E.GAME_LENGTH_MONTHS) * 100;
els.progressBar.style.width = pct + "%";
els.progressBar.className = "progress-bar" +
(pct > 80 ? " danger" : pct > 60 ? " warning" : "");
els.progressLabel.textContent = "Month " + monthsDone + "/" + E.GAME_LENGTH_MONTHS;
els.goalPct.textContent = Math.round(goalPct) + "%";
els.goalPct.className = goalPct >= 100 ? "goal-pct up" : "goal-pct";
// Summary
els.cashLine.textContent = fmtMoney(s.cash_balance);
els.netWorth.textContent = fmtMoney(total);
els.investedLine.textContent = fmtMoney(invested);
els.pnlLine.textContent = fmtMoney(pnl);
els.pnlLine.className = pnl >= 0 ? "up" : "down";
renderWatch();
renderPositions();
renderChart();
renderTradeBtn();
// Game over
if (s.game_over) {
els.advanceBtn.disabled = true;
els.tradeBtn.disabled = true;
if (s.won) {
setStatus("YOU WIN! \u20b920L reached.");
showGameOverBanner(true);
} else {
setStatus("GAME OVER - 10 years elapsed.");
showGameOverBanner(false);
}
}
}
function renderTradeBtn() {
var action = els.actionInput.value;
els.tradeBtn.textContent = action === "buy" ? "BUY \u25b2" : "SELL \u25bc";
els.tradeBtn.className = action === "buy" ? "btn btn-buy" : "btn btn-sell";
}
function showGameOverBanner(won) {
var existing = document.querySelector(".game-over-banner");
if (existing) existing.remove();
var banner = document.createElement("div");
banner.className = "game-over-banner " + (won ? "win" : "lose");
banner.textContent = won
? "CONGRATULATIONS! You doubled your money. You beat the market."
: "GAME OVER. 10 years passed. Reset to try again.";
var summary = els.tradeForm.parentNode.querySelector(".summary");
if (summary) summary.insertAdjacentElement("afterend", banner);
}
function renderWatch() {
var s = state;
var rows = E.TRADABLE_KEYS.map(function (key) {
var display = DISPLAY[key];
var price = s.prices[key];
var prev = prevPrices[key] !== undefined ? prevPrices[key] : price;
var chg = (price - prev) / Math.max(prev, 1e-9) * 100;
var absChg = price - prev;
var cls = chgClass(chg);
var active = chartMode === key ? " active" : "";
return "<tr class=\"" + active + "\" data-asset=\"" + key + "\">" +
"<td>" + display + "</td>" +
"<td>" + fmtMoney(price) + "</td>" +
"<td class=\"" + cls + "\">" + (absChg >= 0 ? "+" : "") + absChg.toFixed(3) + "</td>" +
"<td class=\"" + cls + "\">" + fmtPct(chg) + "</td>" +
"</tr>";
}).join("");
els.watchBody.innerHTML = rows;
els.watchBody.querySelectorAll("tr").forEach(function (tr) {
tr.addEventListener("click", function () {
var asset = tr.dataset.asset;
chartMode = asset;
els.chartTitle.textContent = DISPLAY[asset];
document.querySelectorAll(".chip").forEach(function (c) { c.classList.remove("active"); });
var chip = document.querySelector('.chip[data-chart="' + asset + '"]');
if (chip) chip.classList.add("active");
else document.querySelector('.chip[data-chart="networth"]').classList.add("active");
renderWatch();
renderChart();
});
});
}
function renderPositions() {
var s = state;
var rows = [];
for (var i = 0; i < E.TRADABLE_KEYS.length; i++) {
var asset = E.TRADABLE_KEYS[i];
var qty = s.portfolio[asset];
if (qty <= 0.0001) continue;
var price = s.prices[asset];
var basis = s.cost_basis[asset];
var current = qty * price;
var pnl = current - basis;
var pnlPct = basis > 0 ? (pnl / basis) * 100 : 0;
var pnlCls = pnl >= 0 ? "pnl-pos" : "pnl-neg";
rows.push("<tr>" +
"<td>" + DISPLAY[asset] + "</td>" +
"<td>" + qty.toFixed(4) + "</td>" +
"<td>" + (basis > 0 ? fmtMoney(basis / qty) : "-") + "</td>" +
"<td>" + fmtMoney(price) + "</td>" +
"<td>" + fmtMoney(current) + "</td>" +
"<td class=\"" + pnlCls + "\">" + fmtMoney(pnl) + " (" + fmtPct(pnlPct) + ")</td>" +
"</tr>");
}
if (rows.length === 0) {
els.positionsBody.innerHTML = '<tr><td colspan="6" class="muted center">Buy assets to see them here</td></tr>';
} else {
els.positionsBody.innerHTML = rows.join("");
}
}
// Chart
function renderChart() {
var canvas = els.chart;
var dpr = window.devicePixelRatio || 1;
var cssW = canvas.clientWidth;
var cssH = canvas.clientHeight;
if (cssW <= 0 || cssH <= 0) return;
canvas.width = cssW * dpr;
canvas.height = cssH * dpr;
var ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, cssW, cssH);
var padL = 48, padR = 10, padT = 8, padB = 20;
var plotW = cssW - padL - padR;
var plotH = cssH - padT - padB;
if (plotW <= 0 || plotH <= 0) return;
var series;
if (chartMode === "networth") {
series = state.value_history;
} else {
series = (state.price_history || []).map(function (snap) { return snap[chartMode] || 0; });
}
if (!series || series.length < 2) {
ctx.fillStyle = "rgba(51,255,51,0.3)";
ctx.font = '12px "Share Tech Mono", monospace';
ctx.textAlign = "center";
ctx.fillText("Advance a month to build chart data", cssW / 2, cssH / 2);
return;
}
var min = Infinity, max = -Infinity;
for (var i = 0; i < series.length; i++) {
if (series[i] < min) min = series[i];
if (series[i] > max) max = series[i];
}
var range = (max - min) || 1;
var yLo = min - range * 0.05;
var yHi = max + range * 0.05;
var yRange = yHi - yLo;
var xStep = plotW / Math.max(series.length - 1, 1);
// Grid
ctx.strokeStyle = "rgba(51,255,51,0.06)";
ctx.lineWidth = 0.5;
ctx.font = '10px "Share Tech Mono", monospace';
ctx.fillStyle = "rgba(51,255,51,0.35)";
for (var j = 0; j <= 4; j++) {
var gy = padT + (plotH / 4) * j;
ctx.beginPath();
ctx.moveTo(padL, gy);
ctx.lineTo(cssW - padR, gy);
ctx.stroke();
var val = yHi - (yRange / 4) * j;
var label = chartMode === "networth"
? fmtMoney(val).replace("\u20b9", "")
: val.toFixed(3);
ctx.textAlign = "right";
ctx.fillText(label, padL - 4, gy + 3);
}
// X labels
var totalMonths = series.length;
var labelEvery = Math.max(Math.floor(totalMonths / 6), 1);
ctx.textAlign = "center";
for (var k = 0; k < totalMonths; k += labelEvery) {
var lx = padL + k * xStep;
var mIdx = state.months_elapsed - (totalMonths - 1 - k);
var yStart = E.STARTING_YEAR;
var absMonth = mIdx + E.STARTING_MONTH - 1;
var yr = yStart + Math.floor(absMonth / 12);
var mo = (absMonth % 12) + 1;
ctx.fillText(String(mo).padStart(2, "0") + "/" + (yr % 100), lx, cssH - 4);
}
// Line
var lineColor = chartMode === "networth" ? "#33ff33" : "#00e5ff";
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
ctx.shadowColor = lineColor;
ctx.shadowBlur = 6;
ctx.beginPath();
for (var m = 0; m < series.length; m++) {
var sx = padL + m * xStep;
var sy = padT + plotH - ((series[m] - yLo) / yRange) * plotH;
if (m === 0) ctx.moveTo(sx, sy);
else ctx.lineTo(sx, sy);
}
ctx.stroke();
ctx.shadowBlur = 0;
// Last dot
var last = series[series.length - 1];
var lastX = padL + (series.length - 1) * xStep;
var lastY = padT + plotH - ((last - yLo) / yRange) * plotH;
ctx.fillStyle = lineColor;
ctx.beginPath();
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
ctx.fill();
if (chartMode === "networth") {
els.chartTitle.textContent = "Net Worth";
}
}
// News & agents
function addNews(headline, year, month) {
if (!headline) return;
var item = document.createElement("div");
item.className = "item";
item.innerHTML = '<span class="ts">[' + year + '-' + String(month).padStart(2, "0") + ']</span>' + headline;
els.newsContent.prepend(item);
var children = els.newsContent.children;
while (children.length > 30) {
children[children.length - 1].remove();
}
}
function addAgentActions(actions) {
els.agentLog.innerHTML = "";
if (!actions || !actions.length) {
els.agentLog.innerHTML = '<div class="muted">No agent activity this month.</div>';
return;
}
actions.forEach(function (a) {
var t = (a.actions && a.actions[0]) || {};
var entry = document.createElement("div");
entry.className = "agent-entry";
entry.innerHTML =
'<span class="name">' + a.agent + '</span> ' +
'<span class="action">' + (t.action || "hold") + ' ' + (DISPLAY[t.asset] || t.asset || "") + '</span>' +
'<span class="sentiment">' + (a.sentiment || "") + '</span>' +
'<div style="color:var(--phosphor-dim);margin-top:2px;">' + (t.reason || "") + '</div>';
els.agentLog.appendChild(entry);
});
}
// --- Trade ---
function handleTrade(e) {
e.preventDefault();
if (state.game_over) { toast("Game is over. Reset to play again.", "warn"); return; }
var assetDisplay = els.tradeForm.querySelector("#asset").value;
var action = els.actionInput.value;
var pct = parseFloat(els.amountInput.value) / 100;
var key = Object.keys(DISPLAY).find(function (k) { return DISPLAY[k] === assetDisplay; });
if (!key) { toast("Invalid asset selection.", "error"); return; }
if (pct <= 0 || pct > 1) { toast("Amount must be 1-100% of portfolio.", "error"); return; }
try {
if (action === "buy") {
var total = E.totalValue(state);
var cost = total * pct;
if (cost > state.cash_balance) {
toast("Insufficient cash. You need \u20b9" + Math.round(cost).toLocaleString("en-IN") + ".", "error");
return;
}
} else {
var qty = state.portfolio[key];
if (qty <= 0.0001) { toast("You don't own any " + assetDisplay + ".", "error"); return; }
}
E.executePlayerTrade(state, key, action, pct);
addNews("You " + action + " " + (pct * 100).toFixed(0) + "% " + assetDisplay, state.year, state.month);
setStatus("Traded: " + action.toUpperCase() + " " + assetDisplay + " " + (pct * 100).toFixed(0) + "%");
toast(action.toUpperCase() + " " + assetDisplay + " (" + (pct * 100).toFixed(0) + "%)", "info");
render();
} catch (err) {
toast("Trade failed: " + err.message, "error");
}
}
// --- Advance Month ---
async function handleAdvance() {
if (state.game_over) { toast("Game over. Reset to play again.", "warn"); return; }
setStatus("Advancing...");
els.advanceBtn.disabled = true;
els.advanceBtn.textContent = "...";
try {
prevPrices = {};
for (var k in state.prices) prevPrices[k] = state.prices[k];
var nextY = state.year + (state.month === 12 ? 1 : 0);
var nextM = state.month === 12 ? 1 : state.month + 1;
var ev = Ev.eventForMonth(nextY, nextM);
var snap = {
month: state.month, year: state.year,
prices: {}, portfolio: {}, cash: state.cash_balance,
total_value: E.totalValue(state), unrealized_pnl: E.totalPnl(state),
};
for (var k2 in state.prices) snap.prices[k2] = state.prices[k2];
for (var k3 in state.portfolio) snap.portfolio[k3] = state.portfolio[k3];
var agentActions = E.allLocalAgentsDecide(snap, ev);
var news = {
headline: ev.headline, regime: ev.regime,
impact: {}, duration_months: ev.duration_months,
year: ev.year, month: ev.month,
};
for (var k4 in ev.impact) news.impact[k4] = ev.impact[k4];
E.advanceMonth(state, news, agentActions, ev);
addNews(ev.headline, state.year, state.month);
addAgentActions(agentActions);
// Show deterministic insight (AI insight only on demand)
var detInsight = fallbackInsight(ev, state);
els.insightText.textContent = detInsight;
els.insightText.className = "insight-text deterministic";
var monthLabel = state.year + "-" + String(state.month).padStart(2, "0");
setStatus("Month " + state.months_elapsed + "/" + E.GAME_LENGTH_MONTHS + " (" + monthLabel + ")");
if (state.game_over) {
if (state.won) {
toast("YOU WIN! \u20b920L reached. Congratulations!", "info");
} else {
toast("GAME OVER. 10 years have passed.", "warn");
}
}
render();
} catch (e) {
setStatus("Error: " + e.message, true);
} finally {
els.advanceBtn.textContent = "Advance Month \u23ce";
if (!state.game_over) els.advanceBtn.disabled = false;
}
}
function fallbackInsight(ev, s) {
var pnl = E.totalPnl(s);
var regime = (ev.regime || "stagnation").replace(/_/g, " ");
if (pnl < -50000) return "Cut losers in " + regime + " regimes. Rotate into defensives.";
if (pnl > 50000) return "Book partial profits. " + regime + " trends rarely last.";
return "Hold steady through this " + regime + " phase.";
}
async function generateInsight() {
if (state.game_over) return;
var ev = Ev.eventForMonth(state.year, state.month);
els.insightText.textContent = "Generating...";
els.insightText.className = "insight-text";
els.llmBadge.textContent = "...";
els.llmBadge.className = "badge";
try {
var r = await apiLLM("/game-api/insight", {
event: { headline: ev.headline, regime: ev.regime },
snapshot: {
unrealized_pnl: E.totalPnl(state),
cash: state.cash_balance,
total_value: E.totalValue(state),
},
});
if (r && r.insight) {
els.insightText.textContent = r.insight;
els.insightText.className = "insight-text";
els.llmBadge.textContent = "LLM";
els.llmBadge.className = "badge live";
} else {
els.insightText.textContent = fallbackInsight(ev, state);
els.insightText.className = "insight-text deterministic";
els.llmBadge.textContent = "FALLBACK";
els.llmBadge.className = "badge fallback";
}
} catch (e2) {
els.insightText.textContent = fallbackInsight(ev, state);
els.insightText.className = "insight-text deterministic";
els.llmBadge.textContent = "FALLBACK";
els.llmBadge.className = "badge fallback";
}
}
// --- Reset ---
function handleReset() {
if (state.game_over || state.months_elapsed > 0) {
if (!confirm("Reset your game? All progress will be lost.")) return;
}
state = E.newGame();
prevPrices = {};
for (var k in state.prices) prevPrices[k] = state.prices[k];
els.chatLog.innerHTML = '<div class="chat-msg bot">Welcome back. Ask me about your portfolio or strategy.</div>';
els.newsContent.innerHTML = '<div class="muted">System ready. Press Advance Month to begin.</div>';
els.agentLog.innerHTML = "";
els.insightText.textContent = "Click Generate to get AI market commentary.";
els.insightText.className = "insight-text deterministic";
els.llmBadge.textContent = "OFF";
els.llmBadge.className = "badge fallback";
els.advanceBtn.disabled = false;
els.advanceBtn.textContent = "Advance Month \u23ce";
els.tradeBtn.disabled = false;
els.progressBar.style.width = "0%";
els.progressBar.className = "progress-bar";
els.progressLabel.textContent = "Month 0/" + E.GAME_LENGTH_MONTHS;
var banner = document.querySelector(".game-over-banner");
if (banner) banner.remove();
setStatus("Game reset");
toast("New game started. Good luck!", "info");
render();
}
// --- Mentor ---
async function handleMentor() {
var total = E.totalValue(state);
var invested = E.investedValue(state);
var pnl = E.totalPnl(state);
// Ensure at least a year has passed
if (state.months_elapsed < 1) {
toast("Advance at least one month before getting a review.", "warn");
return;
}
setStatus("Generating review...");
var summary = {
year: state.year, month: state.month,
starting_value: E.STARTING_CASH,
ending_value: total,
invested_value: invested,
cash: state.cash_balance,
unrealized_pnl: pnl,
max_drawdown: -0.25,
sharpe_ratio: 0.0,
allocations: computeAllocations(state),
ledger: state.ledger.filter(function (t) { return t.year === state.year; }),
};
var r = await apiLLM("/game-api/mentor", { summary: summary });
if (r && r.review) {
els.mentorRoast.textContent = r.review.roast || "-";
els.mentorLesson.textContent = "LESSON: " + (r.review.lesson || "");
els.mentorSuggestion.textContent = "NEXT MOVE: " + (r.review.suggestion || "");
els.modal.classList.remove("hidden");
setStatus("Review ready");
} else {
toast("Mentor unavailable. Try again.", "error");
}
}
function computeAllocations(s) {
var total = E.totalValue(s);
var out = {};
for (var i = 0; i < E.TRADABLE_KEYS.length; i++) {
var a = E.TRADABLE_KEYS[i];
out[a] = total > 0 ? (s.portfolio[a] * s.prices[a]) / total : 0;
}
return out;
}
// --- Chat ---
async function handleChat(e) {
e.preventDefault();
var msg = els.chatInput.value.trim();
if (!msg) return;
appendChat("user", msg);
els.chatInput.value = "";
var snapshot = {
cash: state.cash_balance,
total_value: E.totalValue(state),
unrealized_pnl: E.totalPnl(state),
positions: E.TRADABLE_KEYS
.filter(function (k) { return state.portfolio[k] > 0.0001; })
.map(function (k) { return {
asset: DISPLAY[k], qty: state.portfolio[k],
price: state.prices[k], value: state.portfolio[k] * state.prices[k],
}; }),
};
var warmDiv = appendChat("bot", "Thinking...");
var r = await apiLLM("/game-api/chat", { message: msg, snapshot: snapshot });
if (warmDiv && warmDiv.parentNode) warmDiv.remove();
if (r && r.reply) {
var isFallback = r.reply.indexOf("trouble") >= 0 || r.reply.length < 20;
appendChat("bot", r.reply, isFallback);
} else {
appendChat("bot", "Can't reach the advisor right now. Check your connection.", true);
}
}
function appendChat(role, content, fallback) {
var div = document.createElement("div");
div.className = "chat-msg " + role + (fallback ? " fallback" : "");
div.textContent = content;
els.chatLog.appendChild(div);
els.chatLog.scrollTop = els.chatLog.scrollHeight;
return div;
}
function setStatus(text, isError) {
els.statusLine.textContent = text;
els.statusLine.style.color = isError ? "var(--red)" : "var(--phosphor-dim)";
}
// --- Side toggle ---
function setTradeSide(side) {
els.actionInput.value = side;
els.sideBuy.classList.toggle("active", side === "buy");
els.sideSell.classList.toggle("active", side === "sell");
renderTradeBtn();
}
els.sideBuy.addEventListener("click", function () { setTradeSide("buy"); });
els.sideSell.addEventListener("click", function () { setTradeSide("sell"); });
// Amount slider sync
els.amountRange.addEventListener("input", function () {
els.amountInput.value = els.amountRange.value;
});
els.amountInput.addEventListener("input", function () {
var v = parseInt(els.amountInput.value) || 1;
v = Math.max(1, Math.min(100, v));
els.amountRange.value = v;
});
// --- Chart chips ---
document.querySelectorAll(".chip").forEach(function (chip) {
chip.addEventListener("click", function () {
document.querySelectorAll(".chip").forEach(function (c) { c.classList.remove("active"); });
chip.classList.add("active");
chartMode = chip.dataset.chart;
els.chartTitle.textContent = chartMode === "networth" ? "Net Worth" : DISPLAY[chartMode];
renderWatch();
renderChart();
});
});
// --- Keyboard shortcuts ---
document.addEventListener("keydown", function (e) {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT") return;
if (e.key === "Enter") {
e.preventDefault();
if (els.onboarding && !els.onboarding.classList.contains("hidden")) {
startGame();
} else if (!state.game_over) {
handleAdvance();
}
}
});
// --- Onboarding ---
function startGame() {
els.onboarding.classList.add("hidden");
render();
setStatus("Ready - Trade or Advance");
}
els.onboardStart.addEventListener("click", startGame);
// --- Help modal ---
els.helpBtn.addEventListener("click", function () {
els.helpModal.classList.remove("hidden");
});
els.closeHelp.addEventListener("click", function () {
els.helpModal.classList.add("hidden");
});
els.helpModal.addEventListener("click", function (e) {
if (e.target === els.helpModal) els.helpModal.classList.add("hidden");
});
// --- Mentor modal close ---
els.closeModal.addEventListener("click", function () {
els.modal.classList.add("hidden");
});
els.modal.addEventListener("click", function (e) {
if (e.target === els.modal) els.modal.classList.add("hidden");
});
// --- Event wiring ---
els.tradeForm.addEventListener("submit", handleTrade);
els.advanceBtn.addEventListener("click", handleAdvance);
els.mentorBtn.addEventListener("click", handleMentor);
els.resetBtn.addEventListener("click", handleReset);
els.chatForm.addEventListener("submit", handleChat);
window.addEventListener("resize", function () { requestAnimationFrame(renderChart); });
// Generate insight on demand
document.getElementById("insight-generate-btn").addEventListener("click", generateInsight);
// --- LLM status ---
function applyLlmStatus(h) {
var status = h.llm || "uninitialized";
if (status === "modal") {
els.llmStatus.textContent = "LLM: CLOUD";
els.llmStatus.className = "llm-tag loaded";
els.llmBadge.className = "badge live";
els.llmBadge.textContent = "LLM";
els.chatLlmBadge.className = "badge live";
els.chatLlmBadge.textContent = "LLM";
setStatus("Ready (cloud GPU)");
} else if (status === "hf") {
els.llmStatus.textContent = "LLM: HF API";
els.llmStatus.className = "llm-tag loaded";
els.llmBadge.className = "badge live";
els.llmBadge.textContent = "LLM";
els.chatLlmBadge.className = "badge live";
els.chatLlmBadge.textContent = "LLM";
setStatus("Ready (HF API)");
} else if (status === "mock") {
els.llmStatus.textContent = "LLM: LOCAL";
els.llmStatus.className = "llm-tag mock";
els.llmBadge.className = "badge fallback";
els.llmBadge.textContent = "OFF";
els.chatLlmBadge.className = "badge fallback";
els.chatLlmBadge.textContent = "FALLBACK";
setStatus("Ready (local fallback mode)");
} else if (status === "loading") {
els.llmStatus.textContent = "LLM: LOADING";
els.llmStatus.className = "llm-tag loading";
els.llmBadge.className = "badge fallback";
els.llmBadge.textContent = "OFF";
els.chatLlmBadge.className = "badge fallback";
els.chatLlmBadge.textContent = "FALLBACK";
setStatus("Loading LLM...");
} else {
els.llmStatus.textContent = "LLM: LOCAL";
els.llmStatus.className = "llm-tag mock";
els.llmBadge.className = "badge fallback";
els.llmBadge.textContent = "OFF";
els.chatLlmBadge.className = "badge fallback";
els.chatLlmBadge.textContent = "FALLBACK";
setStatus("Ready (local fallback)");
}
}
var lastStatusKey = null;
async function pollLlm() {
try {
var h = await fetchHealth();
var key = (h.llm || "?") + "|" + (h.llm_error || "");
if (key !== lastStatusKey) {
lastStatusKey = key;
applyLlmStatus(h);
}
} catch (e) {}
}
// Boot
(async function boot() {
await pollLlm();
// Show onboarding
if (els.onboarding) {
els.onboarding.classList.remove("hidden");
}
// Poll LLM status
var tick = setInterval(async function () {
var h = await (await fetch("/game-api/health").catch(function () { return null; }));
if (!h || !h.json) return;
try { h = await h.json(); } catch (e) { return; }
var cur = h.llm || "?";
if (cur !== (lastStatusKey || "").split("|")[0]) {
applyLlmStatus(h);
lastStatusKey = (h.llm || "?") + "|" + (h.llm_error || "");
}
if (cur === "modal" || cur === "mock" || cur === "hf" || cur === "error") {
clearInterval(tick);
}
}, 3000);
})();
})();