diff --git a/web/js/dashboard.js b/web/js/dashboard.js index 5b4ba84..86b116a 100644 --- a/web/js/dashboard.js +++ b/web/js/dashboard.js @@ -3,6 +3,7 @@ "use strict"; const $ = (id) => document.getElementById(id); + const t = () => window.HG.t || {}; function show(id) { $(id).classList.remove("hidden"); } function hide(id) { $(id).classList.add("hidden"); } @@ -14,16 +15,46 @@ extreme: { bg: "bg-red-100 dark:bg-red-900", text: "text-red-700 dark:text-red-300", border: "border-red-500" }, }; - const budgetColors = { - comfortable: "bg-green-500", - marginal: "bg-yellow-500", - overloaded: "bg-red-500", + const riskGradients = { + low: { from: "#f0fdf4", to: "#dcfce7", darkFrom: "#052e16", darkTo: "#064e3b" }, + moderate: { from: "#fefce8", to: "#fef9c3", darkFrom: "#422006", darkTo: "#3f3f00" }, + high: { from: "#fff7ed", to: "#ffedd5", darkFrom: "#431407", darkTo: "#7c2d12" }, + extreme: { from: "#fef2f2", to: "#fecaca", darkFrom: "#450a0a", darkTo: "#7f1d1d" }, }; - const budgetHexColors = { - comfortable: "#22c55e", - marginal: "#eab308", - overloaded: "#ef4444", + const warningSeverityColors = { + Minor: { bg: "bg-yellow-50 dark:bg-yellow-900/30", border: "border-yellow-300 dark:border-yellow-700", text: "text-yellow-700 dark:text-yellow-300", pill: "bg-yellow-200 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-200" }, + Moderate: { bg: "bg-orange-50 dark:bg-orange-900/30", border: "border-orange-300 dark:border-orange-700", text: "text-orange-700 dark:text-orange-300", pill: "bg-orange-200 text-orange-800 dark:bg-orange-800 dark:text-orange-200" }, + Severe: { bg: "bg-red-50 dark:bg-red-900/30", border: "border-red-300 dark:border-red-800", text: "text-red-700 dark:text-red-300", pill: "bg-red-200 text-red-800 dark:bg-red-800 dark:text-red-200" }, + Extreme: { bg: "bg-red-100 dark:bg-red-900/50", border: "border-red-500 dark:border-red-600", text: "text-red-800 dark:text-red-200", pill: "bg-red-300 text-red-900 dark:bg-red-700 dark:text-red-100" }, + }; + + const budgetBorderColors = { + comfortable: "border-green-400", + marginal: "border-yellow-400", + overloaded: "border-red-400", + }; + + const effortColors = { + none: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300", + low: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300", + medium: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300", + high: "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300", + }; + + const impactColors = { + low: "bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300", + medium: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300", + high: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300", + }; + + const categoryIcons = { + shading: '', + ventilation: '', + internal_gains: '', + ac_strategy: '', + hydration: '', + care: '', }; function tempColorHex(temp) { @@ -35,6 +66,11 @@ return "#bfdbfe"; } + function isDark() { + return document.documentElement.classList.contains("dark") || + window.matchMedia("(prefers-color-scheme: dark)").matches; + } + window.loadDashboard = async function() { hide("no-data"); hide("no-forecast"); @@ -81,6 +117,12 @@ show("data-display"); renderDashboard(data); + // LLM credentials (shared between summary and actions) + const llmProvider = await getSetting("llmProvider"); + const llmApiKey = await getSetting("llmApiKey"); + const llmModel = await getSetting("llmModel"); + const language = window.HG && window.HG.lang === "de" ? "German" : "English"; + // LLM summary (async) try { const llmBody = { @@ -90,13 +132,8 @@ riskLevel: data.riskLevel, acHeadroomBTUH: data.roomBudgets && data.roomBudgets.length > 0 ? data.roomBudgets[0].headroomBtuh : 0, budgetStatus: data.roomBudgets && data.roomBudgets.length > 0 ? data.roomBudgets[0].status : "comfortable", - language: window.HG && window.HG.lang === "de" ? "German" : "English", + language: language, }; - - // Include client-side LLM settings if available - const llmProvider = await getSetting("llmProvider"); - const llmApiKey = await getSetting("llmApiKey"); - const llmModel = await getSetting("llmModel"); if (llmProvider && llmApiKey) { llmBody.provider = llmProvider; llmBody.apiKey = llmApiKey; @@ -111,8 +148,7 @@ if (llmResp.ok) { const llmData = await llmResp.json(); if (llmData.summary) { - $("llm-summary").innerHTML = `
${esc(llmData.summary)}
-AI-generated summary. Not a substitute for professional advice.
`; + renderLlmSummary(llmData.summary); } else { $("llm-section").classList.add("hidden"); } @@ -121,6 +157,61 @@ $("llm-section").classList.add("hidden"); } + // AI actions (async) + if (llmProvider && llmApiKey) { + show("actions-loading"); + try { + const rooms = (payload.rooms || []).map(r => ({ + name: r.name, + orientation: r.orientation || "", + shadingType: r.shadingType || "none", + hasAC: (payload.acAssignments || []).some(a => a.roomId === r.id), + })); + const actionsBody = { + date: data.date, + indoorTempC: data.indoorTempC, + peakTempC: data.peakTempC, + minNightTempC: data.minNightTempC, + poorNightCool: data.poorNightCool, + riskLevel: data.riskLevel, + riskWindows: data.riskWindows || [], + timeline: (data.timeline || []).map(s => ({ + hour: s.hour, + tempC: s.tempC, + humidityPct: s.humidityPct, + budgetStatus: s.budgetStatus, + coolMode: s.coolMode, + })), + roomBudgets: (data.roomBudgets || []).map(rb => ({ + totalGainW: rb.totalGainW, + })), + rooms: rooms, + provider: llmProvider, + apiKey: llmApiKey, + language: language, + }; + if (llmModel) actionsBody.model = llmModel; + + const actResp = await fetch("/api/llm/actions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(actionsBody), + }); + if (actResp.ok) { + const actData = await actResp.json(); + if (actData.actions && actData.actions.length > 0) { + renderAIActions(actData.actions); + } else { + hide("actions-loading"); + } + } else { + hide("actions-loading"); + } + } catch (e) { + hide("actions-loading"); + } + } + } catch (err) { hide("loading"); show("error-state"); @@ -128,12 +219,49 @@ } }; + // ========== Mini Markdown ========== + function miniMarkdown(text) { + return text + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/^- (.+)$/gm, "${rendered}
`; + }).join(""); + + const el = $("llm-summary"); + el.innerHTML = ` +${esc(t().aiDisclaimer)}
+ `; + // Fade in + requestAnimationFrame(() => el.classList.remove("opacity-0")); + } + + // ========== Main Render ========== function renderDashboard(data) { $("profile-name").textContent = data.profileName + " \u2014 " + data.date; - // Risk card + // Risk card with gradient const rc = riskColors[data.riskLevel] || riskColors.low; - $("risk-card").className = `rounded-xl p-4 text-center shadow-sm ${rc.bg}`; + const rg = riskGradients[data.riskLevel] || riskGradients.low; + const dark = isDark(); + const gradFrom = dark ? rg.darkFrom : rg.from; + const gradTo = dark ? rg.darkTo : rg.to; + const riskCard = $("risk-card"); + riskCard.className = "rounded-xl p-4 text-center shadow-sm transition-all duration-200 hover:shadow-md"; + riskCard.style.background = `linear-gradient(135deg, ${gradFrom}, ${gradTo})`; + + const shieldSvg = ''; + $("risk-icon").innerHTML = `${shieldSvg}`; $("risk-level").className = `text-2xl font-bold capitalize ${rc.text}`; $("risk-level").textContent = data.riskLevel; @@ -146,18 +274,24 @@ $("poor-night-cool").classList.remove("hidden"); } - // Warnings + // Warnings — severity-based coloring if (data.warnings && data.warnings.length > 0) { show("warnings-section"); - $("warnings-section").innerHTML = '