Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> | |
| <title>Hillside Medical Group - Social Media Monitor</title> | |
| <style> | |
| :root{ | |
| --bg:#f6f7fb; --ink:#0f172a; --muted:#475569; | |
| --card:#ffffff; --border:#e5e7eb; --shadow:0 6px 24px rgba(15,23,42,.06); | |
| --grad1:#2c5aa0; --grad2:#1e3c72; /* Blue theme */ | |
| --ok:#22c55e; --warn:#f59e0b; --err:#ef4444; | |
| --term-bg:#0b1221; --term-ink:#22c55e; --term-hdr:#07101d; --term-border:#1f2937; --term-meta:#86efac; | |
| --badge:#e2e8f0; --badge-ink:#0f172a; | |
| --kw:#f59e0b; --kw-ink:#1f2937; | |
| --pill:#dcfce7; --pill-ink:#166534; | |
| } | |
| *{box-sizing:border-box} | |
| body{margin:0;font-family:Inter,system-ui,Arial,sans-serif;background:var(--bg);color:var(--ink)} | |
| .wrap{max-width:1400px;margin:24px auto;padding:0 16px} | |
| .header{background:linear-gradient(135deg,var(--grad1),var(--grad2));color:#fff;border-radius:14px;padding:18px;box-shadow:0 8px 28px rgba(15,23,42,.18); text-align: center;} /* Centered header text */ | |
| h1{margin:0 0 6px 0;font-size:22px;font-weight:600} | |
| .grid{display:grid;gap:16px;grid-template-columns:1fr 420px} | |
| .card{background:var(--card);border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow);padding:18px} | |
| .btn{background:var(--ok);color:#fff;border:none;border-radius:10px;padding:10px 14px;font-weight:600;cursor:pointer} | |
| .btn.red{background:var(--err)} .btn.gray{background:#64748b} | |
| .btn[disabled]{opacity:.5;cursor:not-allowed} | |
| input, select{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:10px} /* Added select style */ | |
| label{font-weight:600; display: block; margin-bottom: 6px;} /* Improved label spacing */ | |
| .muted{color:var(--muted);font-size:13px} | |
| .bar{height:12px;background:var(--border);border-radius:999px;overflow:hidden} | |
| .fill{height:100%;background:linear-gradient(90deg,var(--grad1),#667eea);width:0%} | |
| .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:#334155;word-break:break-all} | |
| /* Terminal */ | |
| .terminal{background:var(--term-bg);border-radius:14px;border:1px solid var(--term-border);height:520px;display:flex;flex-direction:column} | |
| .term-header{color:#a7f3d0;background:var(--term-hdr);border-radius:14px 14px 0 0;padding:10px 12px;font-weight:700;font-size:13px;border-bottom:1px solid var(--term-border)} | |
| .term-body{flex:1;overflow:auto;padding:10px 12px} | |
| .term-line{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--term-ink);white-space:pre-wrap;word-break:break-word;margin:0} | |
| .term-meta{color:var(--term-meta)} | |
| .term-warn{color:#facc15} | |
| .term-err{color:#f87171} | |
| .term-footer{padding:8px 12px;border-top:1px solid var(--term-border);display:flex;gap:8px} | |
| /* Analysis */ | |
| .tabs{display:flex;gap:10px;margin-bottom:10px} | |
| .tab{background:#e2e8f0;color:#0f172a;border:none;border-radius:10px;padding:8px 12px;font-weight:600;cursor:pointer} | |
| .tab.active{background:#1e293b;color:#fff} | |
| .flex{display:flex;gap:10px;flex-wrap:wrap;align-items:center} | |
| .pill{display:inline-block;background:var(--pill);color:var(--pill-ink);padding:2px 10px;border-radius:999px;font-size:12px;font-weight:700} | |
| .stat{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:10px;margin-top:10px} | |
| .stat .box{background:#f8fafc;border:1px solid var(--border);border-radius:12px;padding:10px;text-align:center} | |
| .stat .num{font-weight:800;font-size:22px} | |
| .filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px} | |
| .filter{background:#e5e7eb;border:none;border-radius:999px;padding:6px 10px;font-weight:600;cursor:pointer} | |
| .filter.active{background:#1e293b;color:#fff} | |
| .search{flex:1;min-width:240px} | |
| .posts{margin-top:12px;display:grid;gap:10px} | |
| .post{background:#ffffff;border:1px solid var(--border);border-radius:12px;padding:12px} | |
| .post-hdr{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap} | |
| .badge{display:inline-block;background:var(--badge);color:var(--badge-ink);padding:2px 8px;border-radius:999px;font-size:12px;font-weight:700} | |
| .kw{display:inline-block;background:#fff3cd;color:var(--kw-ink);border:1px solid #fde68a;padding:2px 8px;border-radius:999px;font-size:12px;margin:2px 4px 0 0} | |
| .ai{margin-top:8px;background:#f1f5f9;border:1px solid var(--border);border-radius:10px;padding:10px} | |
| .ai-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px} | |
| .ai-pill{display:inline-block;border-radius:999px;padding:2px 10px;font-size:12px;font-weight:800} | |
| .ai-ok{background:#dcfce7;color:#166534} | |
| .ai-mid{background:#fef9c3;color:#92400e} | |
| .ai-low{background:#fee2e2;color:#991b1b} | |
| .email{font-size:12px;font-weight:700} | |
| .email.ok{color:#166534} .email.no{color:#991b1b} | |
| .reason{margin-top:6px;background:#fff;border:1px dashed var(--border);border-radius:10px;padding:8px;display:none} | |
| .reason.show{display:block} | |
| /* Recipient Dropdown */ | |
| .recipient-container { margin-top: 10px; position: relative; } /* Container for dropdown and custom input */ | |
| .recipient-select { margin-bottom: 10px; } /* Space below select if custom input appears */ | |
| #custom-recipient { margin-top: 6px; display: none; } /* Initially hidden custom input */ | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <div class="header"> | |
| <h1>🩺 Hillside Medical Group - Social Media Monitor</h1> | |
| <div class="muted">Automated monitoring and analysis of medical help requests in Facebook groups.</div> | |
| </div> | |
| <div class="grid" style="margin-top:16px"> | |
| <div class="left"> | |
| <div class="card"> | |
| <h3>System Status</h3> | |
| <div id="sys"></div> | |
| <button class="btn" style="margin-top:8px" onclick="refreshSystem()">Refresh Status</button> | |
| </div> | |
| <div class="card" style="margin-top:16px"> | |
| <h3>Start Monitoring Process</h3> | |
| <label for="recipient-select">Report Recipients</label> | |
| <div class="recipient-container"> | |
| <select id="recipient-select" class="recipient-select" onchange="handleRecipientChange()"> | |
| <option value="">-- Loading Recipients --</option> | |
| </select> | |
| <input type="email" id="custom-recipient" placeholder="Enter custom email address..." /> | |
| </div> | |
| <div class="muted" style="margin:6px 0 10px"> | |
| Select a recipient from the list or choose 'Custom' to enter an email address. | |
| The report summary will be sent to the selected address(es) after processing. | |
| </div> | |
| <div class="flex"> | |
| <button id="start" class="btn" onclick="startProcess()">Start Monitoring</button> | |
| <button class="btn gray" onclick="refreshLive()">Refresh Live View</button> | |
| </div> | |
| <div style="margin-top:12px"> | |
| <div style="display:flex;justify-content:space-between"><b>Overall Progress</b><span id="pct" class="muted">0%</span></div> | |
| <div class="bar"><div id="fill" class="fill"></div></div> | |
| <div id="msg" class="muted" style="margin-top:6px">idle</div> | |
| </div> | |
| </div> | |
| <div class="card" style="margin-top:16px"> | |
| <div class="tabs"> | |
| <button id="tab-groups" class="tab active" onclick="switchSection('groups')">Groups</button> | |
| <button id="tab-analysis" class="tab" onclick="switchSection('analysis')">Analysis (Live)</button> | |
| <button id="tab-summary" class="tab" onclick="switchSection('summary')">Summary</button> | |
| </div> | |
| <!-- Groups --> | |
| <div id="section-groups"> | |
| <h3>Configured Groups (from groups.txt)</h3> | |
| <div id="groups"></div> | |
| </div> | |
| <!-- Analysis Live --> | |
| <div id="section-analysis" style="display:none"> | |
| <div class="flex"> | |
| <div class="pill" id="live-group">Group: –</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="box"><div class="num" id="cnt-total">0</div><div class="muted">Total Posts</div></div> | |
| <div class="box"><div class="num" id="cnt-kw">0</div><div class="muted">Keyword Hits</div></div> | |
| <div class="box"><div class="num" id="cnt-ai">0</div><div class="muted">AI Analyzed</div></div> | |
| <div class="box"><div class="num" id="cnt-confirmed">0</div><div class="muted">Confirmed</div></div> | |
| </div> | |
| <div class="filters"> | |
| <button class="filter active" id="flt-all" onclick="setFilter('all')">All</button> | |
| <button class="filter" id="flt-kw" onclick="setFilter('kw')">Keyword</button> | |
| <button class="filter" id="flt-confirmed" onclick="setFilter('confirmed')">Confirmed</button> | |
| <input id="search" class="search" placeholder="Search post text..."/> | |
| </div> | |
| <div class="posts" id="posts"></div> | |
| </div> | |
| <!-- Summary --> | |
| <div id="section-summary" style="display:none"> | |
| <h3>Last Run Summary</h3> | |
| <div id="summary" class="muted">No summary available yet.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="right"> | |
| <div class="terminal"> | |
| <div class="term-header">Process Logs (Live)</div> | |
| <div id="term" class="term-body"></div> | |
| <div class="term-footer"> | |
| <button class="btn" onclick="scrollBottom()">Scroll to Bottom</button> | |
| <button class="btn red" onclick="clearLogs()">Clear Logs</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API = "/api"; //Server re babu | |
| async function refreshSystem(){ | |
| try { | |
| const r = await fetch(`${API}/system/status`); | |
| const j = await r.json(); | |
| const chip = (b)=>`<span style="padding:2px 8px;border-radius:999px;font-size:12px;font-weight:700;background:${b?'#dcfce7':'#fee2e2'};color:${b?'#166534':'#991b1b'}">${b?'OK':'Missing'}</span>`; | |
| document.getElementById('sys').innerHTML = ` | |
| <table> | |
| <tr><td>Gmail Service</td><td>${chip(j.gmail)}</td></tr> | |
| <tr><td>Groups File (groups.txt)</td><td>${chip(j.groups_file_exists)} • Count: ${j.groups_count}</td></tr> | |
| <tr><td>Processing Script (final5.py)</td><td>${chip(j.final5_exists)}</td></tr> | |
| <tr><td>Default Sender</td><td class="mono">${j.sender_email}</td></tr> | |
| <tr><td>Data Folders</td><td class="mono">${j.scrape_outdir} • ${j.analysis_outdir}</td></tr> | |
| </table>`; | |
| } catch (error) { | |
| console.error("Error refreshing system status:", error); | |
| document.getElementById('sys').innerHTML = `<div class="muted">Error loading system status.</div>`; | |
| } | |
| } | |
| async function loadGroups(){ | |
| try { | |
| const r = await fetch(`${API}/groups`); | |
| const j = await r.json(); | |
| const list = j.groups || []; | |
| if(!list.length){ | |
| document.getElementById('groups').innerHTML = `<div class="muted">Please add Facebook group links to 'groups.txt' (one per line).</div>`; | |
| return; | |
| } | |
| document.getElementById('groups').innerHTML = list.map((g,i)=>` | |
| <div style="border:1px solid var(--border);border-radius:12px;padding:10px;margin:6px 0;background:#fff"> | |
| <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><b>#${i+1}</b><span class="mono">${g}</span></div> | |
| <div id="g-${i}" class="muted">Status: Pending</div> | |
| </div> | |
| `).join(""); | |
| } catch (error) { | |
| console.error("Error loading groups:", error); | |
| document.getElementById('groups').innerHTML = `<div class="muted">Error loading groups list.</div>`; | |
| } | |
| } | |
| // ---------------- Recipient Management ---------------- | |
| let recipientList = []; // Store fetched recipients | |
| async function loadRecipients() { | |
| const selectElement = document.getElementById('recipient-select'); | |
| selectElement.innerHTML = '<option value="">-- Loading Recipients --</option>'; | |
| try { | |
| const response = await fetch(`${API}/recipients`); | |
| const result = await response.json(); | |
| if (result.success && Array.isArray(result.data)) { | |
| recipientList = result.data; | |
| populateRecipientDropdown(); | |
| } else { | |
| console.warn("API did not return a successful recipient list:", result); | |
| selectElement.innerHTML = '<option value="">-- Error Loading --</option>'; | |
| } | |
| } catch (error) { | |
| console.error("Error fetching recipients:", error); | |
| selectElement.innerHTML = '<option value="">-- Network Error --</option>'; | |
| } | |
| } | |
| function populateRecipientDropdown() { | |
| const selectElement = document.getElementById('recipient-select'); | |
| selectElement.innerHTML = ''; // Clear loading option | |
| // Add default recipient first | |
| const defaultRecipient = "smahato@hillsidemedicalgroup.com"; | |
| let defaultOptionFound = false; | |
| recipientList.forEach(recipient => { | |
| const option = document.createElement('option'); | |
| option.value = recipient.email; | |
| option.textContent = `${recipient.name} (${recipient.email})`; | |
| if (recipient.email === defaultRecipient) { | |
| option.selected = true; | |
| defaultOptionFound = true; | |
| } | |
| selectElement.appendChild(option); | |
| }); | |
| // Add Custom option | |
| const customOption = document.createElement('option'); | |
| customOption.value = "custom"; | |
| customOption.textContent = "-- Custom Email --"; | |
| selectElement.appendChild(customOption); | |
| // If default wasn't in the list, add it and select it | |
| if (!defaultOptionFound) { | |
| const defaultOption = document.createElement('option'); | |
| defaultOption.value = defaultRecipient; | |
| defaultOption.textContent = `Subash Mahato (Default) (${defaultRecipient})`; | |
| defaultOption.selected = true; | |
| selectElement.insertBefore(defaultOption, selectElement.firstChild); // Add to top | |
| } | |
| } | |
| function handleRecipientChange() { | |
| const selectElement = document.getElementById('recipient-select'); | |
| const customInput = document.getElementById('custom-recipient'); | |
| if (selectElement.value === 'custom') { | |
| customInput.style.display = 'block'; | |
| customInput.focus(); | |
| } else { | |
| customInput.style.display = 'none'; | |
| customInput.value = ''; // Clear if deselected | |
| } | |
| } | |
| function getSelectedRecipients() { | |
| const selectElement = document.getElementById('recipient-select'); | |
| const customInput = document.getElementById('custom-recipient'); | |
| let emails = []; | |
| if (selectElement.value === 'custom' && customInput.value.trim() !== '') { | |
| // Validate custom email (basic check) | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (emailRegex.test(customInput.value.trim())) { | |
| emails.push(customInput.value.trim()); | |
| } else { | |
| alert("Please enter a valid custom email address."); | |
| return null; // Signal error | |
| } | |
| } else if (selectElement.value) { | |
| emails.push(selectElement.value); | |
| } else { | |
| // No recipient selected (shouldn't happen with default) | |
| alert("Please select a recipient."); | |
| return null; | |
| } | |
| return emails; | |
| } | |
| // ---------------- Process control ---------------- | |
| async function startProcess(){ | |
| document.getElementById('start').disabled = true; | |
| const recipients = getSelectedRecipients(); | |
| if (!recipients) { | |
| // Error message already shown in getSelectedRecipients | |
| document.getElementById('start').disabled = false; | |
| return; | |
| } | |
| try { | |
| await fetch(`${API}/process/clear-logs`, {method:'POST'}); // Clear previous logs | |
| const r = await fetch(`${API}/process/start`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({recipients: recipients}) // Send selected recipients | |
| }); | |
| const j = await r.json(); | |
| if(!j.success){ | |
| alert(j.message||'Failed to start the process.'); | |
| document.getElementById('start').disabled = false; | |
| return; | |
| } | |
| pollStatus(); // Start polling for status updates | |
| } catch (error) { | |
| console.error("Error starting process:", error); | |
| alert("An error occurred while trying to start the process."); | |
| document.getElementById('start').disabled = false; | |
| } | |
| } | |
| async function pollStatus(){ | |
| try { | |
| const r = await fetch(`${API}/process/status`); | |
| const j = await r.json(); | |
| document.getElementById('msg').innerText = j.message || 'Idle'; | |
| document.getElementById('pct').innerText = `${j.progress||0}%`; | |
| document.getElementById('fill').style.width = `${j.progress||0}%`; | |
| (j.groups||[]).forEach((g,idx)=>{ | |
| const el = document.getElementById(`g-${idx}`); | |
| if(!el) return; | |
| el.innerHTML = `Status: ${g.stage} • Scraped: ${g.scraped_posts} • Confirmed: ${g.detected_posts}${g.error?(' • Error: '+g.error):''}`; | |
| }); | |
| if(j.running){ | |
| setTimeout(pollStatus, 1200); // Poll every 1.2 seconds while running | |
| } else { | |
| document.getElementById('start').disabled = false; | |
| loadSummary(); // Load the final summary when done | |
| } | |
| } catch (error) { | |
| console.error("Error polling status:", error); | |
| // Optionally, retry or show error state | |
| setTimeout(pollStatus, 3000); // Retry after a longer delay on error | |
| } | |
| } | |
| // ---------------- Logs (terminal) ---------------- | |
| let lastLogId = 0; | |
| function appendLogs(entries){ | |
| const term = document.getElementById('term'); | |
| const nearBottom = term.scrollTop + term.clientHeight >= term.scrollHeight - 40; | |
| entries.forEach(e=>{ | |
| const div = document.createElement('div'); | |
| div.className = 'term-line'; | |
| const color = e.level === 'error' ? 'term-err' : e.level === 'warn' ? 'term-warn' : ''; | |
| div.innerHTML = `<span class="term-meta">[${e.ts}] [${e.source}]</span> <span class="${color}">${escapeHtml(e.msg)}</span>`; | |
| term.appendChild(div); | |
| }); | |
| if(nearBottom || entries.length){ term.scrollTop = term.scrollHeight; } | |
| } | |
| async function pollLogs(){ | |
| try{ | |
| const r = await fetch(`${API}/process/logs?after=${lastLogId}&limit=500`); | |
| const j = await r.json(); | |
| if(j.entries && j.entries.length){ | |
| appendLogs(j.entries); | |
| lastLogId = j.last || lastLogId; | |
| } | |
| }catch(e){ | |
| console.error("Error polling logs:", e); | |
| // Continue polling even on error | |
| } | |
| setTimeout(pollLogs, 900); // Poll logs every 0.9 seconds | |
| } | |
| function scrollBottom(){ | |
| const term = document.getElementById('term'); | |
| term.scrollTop = term.scrollHeight; | |
| } | |
| async function clearLogs(){ | |
| try { | |
| await fetch(`${API}/process/clear-logs`, {method:'POST'}); | |
| document.getElementById('term').innerHTML = ''; | |
| lastLogId = 0; | |
| } catch (error) { | |
| console.error("Error clearing logs:", error); | |
| // Optionally inform user | |
| } | |
| } | |
| function escapeHtml(s){ | |
| return (s||'').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); | |
| } | |
| // ---------------- Live Analysis ---------------- | |
| let liveFilter = 'all'; // 'all' | 'kw' | 'confirmed' | |
| let liveSearch = ''; | |
| function setFilter(f){ | |
| liveFilter = f; | |
| document.getElementById('flt-all').classList.toggle('active', f==='all'); | |
| document.getElementById('flt-kw').classList.toggle('active', f==='kw'); | |
| document.getElementById('flt-confirmed').classList.toggle('active', f==='confirmed'); | |
| refreshLive(); | |
| } | |
| document.getElementById('search').addEventListener('input', e=>{ | |
| liveSearch = e.target.value.toLowerCase(); | |
| refreshLive(); | |
| }); | |
| async function refreshLive(){ | |
| try { | |
| const r = await fetch(`${API}/live/state`); | |
| const j = await r.json(); | |
| if(!j.success) { | |
| console.warn("Live state API returned not success:", j); | |
| return; | |
| } | |
| const data = j.data || {}; | |
| renderLive(data); | |
| } catch (error) { | |
| console.error("Error refreshing live state:", error); | |
| // Optionally update UI to show error | |
| } | |
| } | |
| function renderLive(data){ | |
| // header + counts | |
| document.getElementById('live-group').innerText = `Current Group: ${data.group || '–'}`; | |
| const c = data.counts || {}; | |
| document.getElementById('cnt-total').innerText = c.total_posts || 0; | |
| document.getElementById('cnt-kw').innerText = c.kw_hits || 0; | |
| document.getElementById('cnt-ai').innerText = c.ai_done || 0; | |
| document.getElementById('cnt-confirmed').innerText = c.confirmed || 0; | |
| const posts = Array.isArray(data.posts) ? data.posts : []; | |
| // filter/search | |
| const filtered = posts.filter(p=>{ | |
| const hasKW = Array.isArray(p.found_keywords) && p.found_keywords.length>0; | |
| const isConfirmed = p.ai && p.ai.is_medical_seeking; | |
| if(liveFilter==='kw' && !hasKW) return false; | |
| if(liveFilter==='confirmed' && !isConfirmed) return false; | |
| if(liveSearch && !(p.text||'').toLowerCase().includes(liveSearch)) return false; | |
| return true; | |
| }); | |
| const html = filtered.map(p=>{ | |
| const hasKW = Array.isArray(p.found_keywords) && p.found_keywords.length>0; | |
| const ai = p.ai || null; | |
| const confirm = !!(ai && ai.is_medical_seeking); | |
| const conf = ai ? (ai.confidence||'').toLowerCase() : ''; | |
| const urg = ai ? (ai.urgency_level||'').toLowerCase() : ''; | |
| const confClass = conf==='high' ? 'ai-ok' : conf==='medium' ? 'ai-mid' : 'ai-low'; | |
| const urgClass = urg==='high' ? 'ai-low' : urg==='medium' ? 'ai-mid' : 'ai-ok'; | |
| const emailTxt = p.email_sent ? '<span class="email ok">Email Sent</span>' : '<span class="email no">No Email</span>'; | |
| return ` | |
| <div class="post"> | |
| <div class="post-hdr"> | |
| <div class="flex"> | |
| <span class="badge">Post #${p.id || '-'}</span> | |
| ${hasKW ? '<span class="badge">Keyword Hit</span>' : ''} | |
| ${confirm ? '<span class="badge" style="background-color: #bfdbfe; color: #1e40af;">CONFIRMED</span>' : ''} <!-- Blue badge for confirmed --> | |
| </div> | |
| <div class="muted mono">Group: ${escapeHtml(p.group_link || 'N/A')}</div> | |
| </div> | |
| <div style="margin-top:6px; white-space: pre-wrap;">${escapeHtml(p.text || '')}</div> <!-- pre-wrap for text formatting --> | |
| ${hasKW ? ` | |
| <div style="margin-top:6px">${(p.found_keywords||[]).map(k=>`<span class="kw">${escapeHtml(k)}</span>`).join('')}</div> | |
| ` : ''} | |
| ${ai ? ` | |
| <div class="ai"> | |
| <div class="ai-row"> | |
| <span class="ai-pill ${confClass}">Confidence: ${(ai.confidence||'').toUpperCase()}</span> | |
| <span class="ai-pill ${urgClass}">Urgency: ${(ai.urgency_level||'').toUpperCase()}</span> | |
| ${emailTxt} | |
| </div> | |
| <div><b>Summary:</b> ${escapeHtml(ai.medical_summary || '')}</div> | |
| <div style="margin-top:4px"><b>Analysis:</b> ${escapeHtml(ai.analysis || '')}</div> | |
| ${Array.isArray(ai.suggested_services) && ai.suggested_services.length ? ` | |
| <div style="margin-top:4px"><b>Suggested Services:</b> ${(ai.suggested_services||[]).map(s=>`<span class="badge" style="margin-right:6px">${escapeHtml(s)}</span>`).join('')}</div> | |
| `:''} | |
| <div style="margin-top:6px"> | |
| <button class="btn gray" onclick="toggleReason(${p.id})">Show Reasoning</button> | |
| <div id="reason-${p.id}" class="reason">${escapeHtml(ai.reasoning || 'No reasoning provided.')}</div> | |
| </div> | |
| </div> | |
| ` : '<div class="muted">Awaiting AI analysis...</div>'} | |
| </div> | |
| `; | |
| }).join(''); | |
| document.getElementById('posts').innerHTML = html || `<div class="muted">No posts match the current filters.</div>`; | |
| } | |
| function toggleReason(id){ | |
| const el = document.getElementById(`reason-${id}`); | |
| if(!el) return; | |
| el.classList.toggle('show'); | |
| } | |
| // auto poll live state | |
| async function pollLive(){ | |
| await refreshLive(); | |
| setTimeout(pollLive, 1200); // Poll live view every 1.2 seconds | |
| } | |
| // ---------------- Summary ---------------- | |
| async function loadSummary(){ | |
| try { | |
| const r = await fetch(`${API}/results/summary`); | |
| const el = document.getElementById('summary'); | |
| if(r.status!==200){ | |
| el.innerHTML = `<div class="muted">Processing complete. Summary will appear here shortly.</div>`; | |
| // Retry once after a short delay if not found immediately | |
| setTimeout(async () => { | |
| const retry = await fetch(`${API}/results/summary`); | |
| if(retry.status === 200) { | |
| const j = await retry.json(); | |
| el.innerHTML = `<pre class="mono">${JSON.stringify(j.data,null,2)}</pre>`; | |
| } | |
| }, 2000); | |
| return; | |
| } | |
| const j = await r.json(); | |
| // Check if data exists and format nicely | |
| if (j.success && j.data) { | |
| // You can create a more user-friendly summary view here instead of raw JSON | |
| // For now, we'll keep the JSON view | |
| el.innerHTML = `<pre class="mono">${JSON.stringify(j.data,null,2)}</pre>`; | |
| } else { | |
| el.innerHTML = `<div class="muted">Summary data unavailable.</div>`; | |
| } | |
| } catch (error) { | |
| console.error("Error loading summary:", error); | |
| document.getElementById('summary').innerHTML = `<div class="muted">Error loading summary.</div>`; | |
| } | |
| } | |
| // ---------------- Tabs ---------------- | |
| function switchSection(name){ | |
| const sec = (id,show)=>document.getElementById(id).style.display = show?'block':'none'; | |
| document.getElementById('tab-groups').classList.toggle('active', name==='groups'); | |
| document.getElementById('tab-analysis').classList.toggle('active', name==='analysis'); | |
| document.getElementById('tab-summary').classList.toggle('active', name==='summary'); | |
| sec('section-groups', name==='groups'); | |
| sec('section-analysis', name==='analysis'); | |
| sec('section-summary', name==='summary'); | |
| if(name==='analysis') refreshLive(); | |
| if(name==='summary') loadSummary(); | |
| } | |
| // ---------------- Boot ---------------- | |
| // Load initial data and start polling | |
| refreshSystem(); | |
| loadGroups(); | |
| loadRecipients(); // Load recipients on startup | |
| loadSummary(); | |
| pollStatus(); // Start status polling | |
| pollLogs(); // Start log polling | |
| pollLive(); // Start live analysis polling | |
| </script> | |
| </body> | |
| </html> |