/* ═══════════════════════════════════════════════ BREATHE — Front-end Application ═══════════════════════════════════════════════ */ const API = ""; // same-origin; change to http://localhost:5000 if running separately const ADVICE = { Minimal: "Great job! Your stress levels are minimal. Keep up your healthy routines and sleep schedule.", Mild: "Your stress is mild. Consider a short walk or 5-minute breathing exercise today.", Moderate: "Moderate stress detected. Try to schedule a proper break, reduce screen time and stay hydrated.", Severe: "High stress detected. Please prioritise rest, reach out to someone you trust, and consider reducing workload.", Critical: "Critical stress indicators found. We strongly recommend speaking with a mental health professional. You are not alone.", }; const LEVEL_COLOURS = { Minimal: "#4ade80", Mild: "#86efac", Moderate: "#fbbf24", Severe: "#fb923c", Critical: "#f87171", }; /* ─── Helpers ─────────────────────────────────────────────── */ const $ = (s, el = document) => el.querySelector(s); const $$ = (s, el = document) => el.querySelectorAll(s); function setClass(el, cls, on) { el && (on ? el.classList.add(cls) : el.classList.remove(cls)); } function formatDate(iso) { const d = new Date(iso); return d.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } async function apiFetch(path, options = {}) { const res = await fetch(API + path, { credentials: "include", headers: { "Content-Type": "application/json", ...(options.headers || {}) }, ...options, }); const json = await res.json().catch(() => ({})); if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`); return json; } /* ─── App State ───────────────────────────────────────────── */ const state = { user: null, timelineChart: null, distChart: null, historyPage: 1, }; /* ═══════════════════════════════════════════════ AUTH ═══════════════════════════════════════════════ */ function showAuth() { setClass($("#auth-screen"), "active", true); setClass($("#app-screen"), "active", false); } function showApp() { setClass($("#auth-screen"), "active", false); setClass($("#app-screen"), "active", true); } // Tab switching $$(".tab").forEach(btn => { btn.addEventListener("click", () => { $$(".tab").forEach(t => t.classList.remove("active")); btn.classList.add("active"); $$(".auth-form").forEach(f => f.classList.remove("active")); $(`#${btn.dataset.tab}-form`).classList.add("active"); }); }); // Login $("#login-form").addEventListener("submit", async e => { e.preventDefault(); const errEl = $("#login-error"); errEl.classList.add("hidden"); try { const data = await apiFetch("/api/auth/login", { method: "POST", body: JSON.stringify({ email: $("#login-identity").value, username: $("#login-identity").value, password: $("#login-password").value, }), }); state.user = data.user; onLogin(); } catch (err) { errEl.textContent = err.message; errEl.classList.remove("hidden"); } }); // Signup $("#signup-form").addEventListener("submit", async e => { e.preventDefault(); const errEl = $("#signup-error"); errEl.classList.add("hidden"); try { const data = await apiFetch("/api/auth/signup", { method: "POST", body: JSON.stringify({ username: $("#signup-username").value, email: $("#signup-email").value, password: $("#signup-password").value, }), }); state.user = data.user; onLogin(); } catch (err) { errEl.textContent = err.message; errEl.classList.remove("hidden"); } }); // Logout $("#logout-btn").addEventListener("click", async () => { await apiFetch("/api/auth/logout", { method: "POST" }).catch(() => {}); state.user = null; showAuth(); }); async function onLogin() { showApp(); $("#sidebar-username").textContent = state.user.username; $("#dash-greeting").textContent = `Welcome back, ${state.user.username}!`; await loadDashboard(); goToView("dashboard"); } /* ─── Check existing session on load ─────────────── */ (async () => { try { const data = await apiFetch("/api/auth/me"); state.user = data.user; onLogin(); } catch { showAuth(); } })(); /* ═══════════════════════════════════════════════ NAVIGATION ═══════════════════════════════════════════════ */ function goToView(name) { $$(".view").forEach(v => v.classList.remove("active")); $(`#view-${name}`).classList.add("active"); $$(".nav-item").forEach(b => { b.classList.toggle("active", b.dataset.view === name); }); if (name === "history") loadHistory(1); if (name === "dashboard") loadDashboard(); } $$(".nav-item").forEach(btn => { btn.addEventListener("click", () => goToView(btn.dataset.view)); }); /* ═══════════════════════════════════════════════ DASHBOARD ═══════════════════════════════════════════════ */ async function loadDashboard() { try { const data = await apiFetch("/api/assessments/summary"); const s = data.summary; const tl = data.timeline; if (!s || !Object.keys(s).length) { // No data yet return; } // Stats $("#stat-total .stat-value").textContent = s.total_assessments; $("#stat-latest .stat-value").textContent = stressIcon(s.latest_label) + " " + (s.latest_label || "—"); $("#stat-latest .stat-value").style.fontSize = "1.2rem"; $("#stat-avg .stat-value").textContent = s.avg_score ? (s.avg_score * 100).toFixed(1) + "%" : "—"; // Trend if (tl.length >= 2) { const diff = tl[tl.length-1].fused_score - tl[tl.length-2].fused_score; const el = $("#stat-trend .stat-value"); el.textContent = diff > 0.01 ? "▲ Up" : diff < -0.01 ? "▼ Down" : "→ Stable"; el.style.color = diff > 0.01 ? "var(--danger)" : diff < -0.01 ? "var(--success)" : "var(--text-muted)"; el.style.fontSize = "1rem"; } // Timeline chart buildTimelineChart(tl); // Distribution chart buildDistChart(s.label_distribution); // Recent list (last 5) const recent = [...tl].reverse().slice(0, 5); const listEl = $("#recent-list"); listEl.innerHTML = recent.length ? recent.map(r => assessmentItemHTML(r)).join("") : `

No assessments yet.

`; listEl.querySelectorAll(".assessment-item").forEach(el => { el.addEventListener("click", () => openDetailModal(el.dataset.id)); }); } catch (err) { console.error("Dashboard load error:", err); } } function buildTimelineChart(tl) { const ctx = $("#timeline-chart").getContext("2d"); if (state.timelineChart) state.timelineChart.destroy(); state.timelineChart = new Chart(ctx, { type: "line", data: { labels: tl.map(r => r.date), datasets: [{ label: "Stress Score", data: tl.map(r => +(r.fused_score * 100).toFixed(1)), borderColor: "#4f8ef7", backgroundColor: "rgba(79,142,247,.08)", fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: tl.map(r => LEVEL_COLOURS[r.fused_label] || "#4f8ef7"), }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: "#7a8aa0", maxTicksLimit: 8 }, grid: { color: "#2a3348" }, }, y: { min: 0, max: 100, ticks: { color: "#7a8aa0", callback: v => v + "%" }, grid: { color: "#2a3348" }, }, }, }, }); } function buildDistChart(dist) { const ctx = $("#distribution-chart").getContext("2d"); if (state.distChart) state.distChart.destroy(); const labels = Object.keys(dist); const values = Object.values(dist); const colours = labels.map(l => LEVEL_COLOURS[l] || "#4f8ef7"); state.distChart = new Chart(ctx, { type: "doughnut", data: { labels, datasets: [{ data: values, backgroundColor: colours, borderWidth: 2, borderColor: "#161b27" }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: "#e2e8f0", font: { size: 12 } } }, }, cutout: "60%", }, }); } /* ═══════════════════════════════════════════════ NEW ASSESSMENT ═══════════════════════════════════════════════ */ $("#assess-form").addEventListener("submit", async e => { e.preventDefault(); const btn = $("#assess-submit"); const errEl = $("#assess-error"); errEl.classList.add("hidden"); btn.disabled = true; btn.textContent = "Analysing…"; try { // Build psychometric payload const psycho = {}; const numFields = [ "Sleep_Duration","Sleep_Quality","Work_Hours","Physical_Activity", "Screen_Time","Travel_Time","Social_Interactions","Caffeine_Intake", "Alcohol_Intake","Blood_Pressure","Cholesterol_Level","Blood_Sugar_Level", ]; const catFields = ["Gender","Occupation","Smoking_Status","Diet_Quality"]; let hasPsycho = false; numFields.forEach(f => { const el = $(`[name="${f}"]`, $("#assess-form")); if (el && el.value !== "") { psycho[f] = parseFloat(el.value); hasPsycho = true; } }); catFields.forEach(f => { const el = $(`[name="${f}"]`, $("#assess-form")); if (el && el.value !== "") { psycho[f] = el.value; hasPsycho = true; } }); const textNote = $("#text-note").value.trim(); if (!hasPsycho && !textNote) { throw new Error("Please fill in at least one section."); } const body = { psychometric: hasPsycho ? psycho : null, text_note: textNote || null, }; const data = await apiFetch("/api/assessments", { method: "POST", body: JSON.stringify(body), }); showResult(data.prediction, data.assessment); } catch (err) { errEl.textContent = err.message; errEl.classList.remove("hidden"); } finally { btn.disabled = false; btn.textContent = "Analyse My Stress →"; } }); function showResult(pred, assessment) { const panel = $("#result-panel"); panel.classList.remove("hidden"); panel.scrollIntoView({ behavior: "smooth", block: "start" }); const score = pred.fused_score; const label = pred.fused_label; const colour = LEVEL_COLOURS[label] || "#4f8ef7"; const perc = (score * 100).toFixed(1); // Gauge const circumference = 2 * Math.PI * 50; // r=50 const filled = (score * circumference).toFixed(1); const arc = $("#gauge-arc"); arc.style.strokeDasharray = `${filled} ${circumference}`; arc.style.stroke = colour; $("#gauge-score").textContent = perc + "%"; $("#gauge-label").textContent = label; $("#gauge-label").style.color = colour; // Details $("#res-psycho-val").textContent = pred.psycho_label ? `${pred.psycho_label} (${(pred.psycho_score * 100).toFixed(1)}%)` : "—"; $("#res-text-val").textContent = pred.text_label ? `${pred.text_label} (${(pred.text_score * 100).toFixed(1)}%)` : "—"; $("#res-modality").textContent = pred.modality_used || "—"; $("#res-time").textContent = assessment ? formatDate(assessment.created_at) : new Date().toLocaleString(); // Advice $("#result-advice").textContent = ADVICE[label] || ""; } /* ═══════════════════════════════════════════════ HISTORY ═══════════════════════════════════════════════ */ async function loadHistory(page = 1) { state.historyPage = page; const listEl = $("#history-list"); const pagEl = $("#history-pagination"); listEl.innerHTML = `

Loading…

`; pagEl.innerHTML = ""; try { const data = await apiFetch(`/api/assessments?page=${page}&per_page=15`); if (!data.assessments.length) { listEl.innerHTML = `

No assessments yet. Take your first one!

`; return; } listEl.innerHTML = data.assessments.map(a => assessmentItemHTML({ id: a.id, date: formatDate(a.created_at), fused_score: a.fused_score, fused_label: a.fused_label, modality: a.modality_used, })).join(""); listEl.querySelectorAll(".assessment-item").forEach(el => { el.addEventListener("click", () => openDetailModal(el.dataset.id)); }); // Pagination if (data.pages > 1) { for (let p = 1; p <= data.pages; p++) { const btn = document.createElement("button"); btn.className = `page-btn${p === page ? " active" : ""}`; btn.textContent = p; btn.addEventListener("click", () => loadHistory(p)); pagEl.appendChild(btn); } } } catch (err) { listEl.innerHTML = `

${err.message}

`; } } function assessmentItemHTML({ id, date, fused_score, fused_label, modality }) { const levelKey = (fused_label || "").toLowerCase(); const scoreStr = fused_score != null ? (fused_score * 100).toFixed(1) + "%" : "—"; return `
${stressIcon(fused_label)} ${fused_label || "—"} ${date || ""} ${scoreStr} ${modality ? `${modality}` : ""}
`; } function stressIcon(label) { const icons = { Minimal: "🟢", Mild: "🟡", Moderate: "🟠", Severe: "🔴", Critical: "🚨" }; return icons[label] || "⚪"; } /* ═══════════════════════════════════════════════ DETAIL MODAL ═══════════════════════════════════════════════ */ async function openDetailModal(id) { const modal = $("#detail-modal"); const bodyEl = $("#modal-body"); bodyEl.innerHTML = "

Loading…

"; modal.classList.remove("hidden"); try { const data = await apiFetch(`/api/assessments/${id}`); const a = data.assessment; const psycho = a.psychometric_data ? Object.entries(a.psychometric_data) .map(([k,v]) => ``) .join("") : "

Not provided

"; bodyEl.innerHTML = `
Fused Stress Level ${stressIcon(a.fused_label)} ${a.fused_label} (${a.fused_score != null ? (a.fused_score*100).toFixed(1)+"%" : "—"})
Psychometric ${a.psycho_label || "—"}${a.psycho_score != null ? " (" + (a.psycho_score*100).toFixed(1)+"%)" : ""}
Text Sentiment ${a.text_label || "—"}${a.text_score != null ? " (" + (a.text_score*100).toFixed(1)+"%)" : ""}
Date & Time ${formatDate(a.created_at)}
${a.text_note ? `

Text Note

${escapeHtml(a.text_note)}
` : ""}

Psychometric Data

${ADVICE[a.fused_label] || ""}
`; } catch (err) { bodyEl.innerHTML = `

${err.message}

`; } } function closeModal() { $("#detail-modal").classList.add("hidden"); } function escapeHtml(str) { return str.replace(/&/g,"&").replace(//g,">"); } /* ── Expose globals used by inline onclick handlers ── */ const app = { goToView, closeModal };