Spaces:
Sleeping
Sleeping
| (() => { | |
| // Get API URL from configuration | |
| const getAPIBaseUrl = () => { | |
| if (window.AIMHSA && window.AIMHSA.Config) { | |
| return window.AIMHSA.Config.getApiBaseUrl(); | |
| } | |
| // Fallback to auto-detection | |
| return `https://${window.location.hostname}`; | |
| }; | |
| const API_BASE_URL = getAPIBaseUrl(); | |
| // Check authentication | |
| const account = localStorage.getItem("aimhsa_account"); | |
| const professionalData = localStorage.getItem("aimhsa_professional"); | |
| const adminData = localStorage.getItem("aimhsa_admin"); | |
| if (professionalData) { | |
| alert('You are logged in as a professional. Please logout and login as a regular user to use the chat.'); | |
| window.location.href = '/professional_dashboard.html'; | |
| return; | |
| } | |
| if (adminData) { | |
| alert('You are logged in as an admin. Please logout and login as a regular user to use the chat.'); | |
| window.location.href = '/admin_dashboard.html'; | |
| return; | |
| } | |
| if (!account) { | |
| window.location.href = '/login'; | |
| return; | |
| } | |
| // Elements | |
| const messagesEl = document.getElementById("messages"); | |
| const form = document.getElementById("form"); | |
| const queryInput = document.getElementById("query"); | |
| const sendBtn = document.getElementById("send"); | |
| const fileInput = document.getElementById("file"); | |
| const composer = form; // composer container (used for inserting preview) | |
| const historyList = document.getElementById("historyList"); | |
| const newChatBtn = document.getElementById("newChatBtn"); | |
| const clearChatBtn = document.getElementById("clearChatBtn"); | |
| const clearHistoryBtn = document.getElementById("clearHistoryBtn"); | |
| const logoutBtn = document.getElementById("logoutBtn"); | |
| const usernameEl = document.getElementById("username"); | |
| const archivedList = document.getElementById("archivedList"); | |
| let convId = localStorage.getItem("aimhsa_conv") || null; | |
| let typingEl = null; | |
| let currentPreview = null; | |
| const archivedPwById = new Map(); | |
| // Model selection: via URL (?model=xyz) or localStorage (aimhsa_model) | |
| const urlParams = new URLSearchParams(window.location.search || ""); | |
| const urlModel = (urlParams.get('model') || '').trim(); | |
| if (urlModel) { | |
| try { localStorage.setItem('aimhsa_model', urlModel); } catch (_) {} | |
| } | |
| function getSelectedModel() { | |
| try { return (localStorage.getItem('aimhsa_model') || '').trim() || null; } catch (_) { return null; } | |
| } | |
| // Set username | |
| usernameEl.textContent = account === 'null' ? 'Guest' : account; | |
| // Inject runtime CSS for animations & preview (keeps frontend simple) | |
| (function injectStyles(){ | |
| const css = ` | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity:1; transform:none; } } | |
| .fade-in { animation: fadeIn 280ms ease both; } | |
| .typing { display:flex; align-items:center; gap:8px; padding:8px 12px; border-radius:10px; width:fit-content; background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.03); } | |
| .dots { display:inline-block; width:36px; text-align:center; } | |
| .dot { display:inline-block; width:6px; height:6px; margin:0 2px; background:var(--muted); border-radius:50%; opacity:0.25; animation: blink 1s infinite; } | |
| .dot:nth-child(2){ animation-delay: .2s; } .dot:nth-child(3){ animation-delay: .4s; } | |
| @keyframes blink { 0%{opacity:.25} 50%{opacity:1} 100%{opacity:.25} } | |
| .upload-preview { display:flex; align-items:center; gap:12px; padding:8px 10px; border-radius:8px; background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.03); margin-right:auto; max-width:420px; } | |
| .upload-meta { display:flex; flex-direction:column; gap:4px; font-size:13px; color:var(--muted); } | |
| .upload-filename { font-weight:600; color:var(--text); } | |
| .upload-actions { display:flex; gap:8px; align-items:center; } | |
| .progress-bar { width:160px; height:8px; background:rgba(255,255,255,0.03); border-radius:6px; overflow:hidden; } | |
| .progress-inner { height:100%; width:0%; background:linear-gradient(90deg,var(--accent), #5b21b6); transition:width .2s ease; } | |
| .btn-small { padding:6px 8px; border-radius:8px; background:transparent; border:1px solid rgba(255,255,255,0.04); color:var(--muted); cursor:pointer; font-size:12px; } | |
| .sending { opacity:0.7; transform:scale(.98); transition:transform .12s ease, opacity .12s ease; } | |
| .msg.fade-in { transform-origin: left top; } | |
| `; | |
| const s = document.createElement("style"); | |
| s.textContent = css; | |
| document.head.appendChild(s); | |
| })(); | |
| // helper: ensure messages container scrolls to bottom after layout updates | |
| function ensureScroll() { | |
| const doScroll = () => { | |
| try { | |
| const last = messagesEl.lastElementChild; | |
| if (last && typeof last.scrollIntoView === "function") { | |
| last.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" }); | |
| } else { | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| } | |
| } catch (e) { | |
| try { messagesEl.scrollTop = messagesEl.scrollHeight; } catch (_) {} | |
| } | |
| }; | |
| requestAnimationFrame(() => { | |
| requestAnimationFrame(() => { | |
| setTimeout(doScroll, 40); | |
| }); | |
| }); | |
| } | |
| // Logout handler | |
| logoutBtn.addEventListener("click", () => { | |
| localStorage.removeItem("aimhsa_account"); | |
| localStorage.removeItem("aimhsa_conv"); | |
| localStorage.removeItem("aimhsa_professional"); | |
| localStorage.removeItem("aimhsa_admin"); | |
| window.location.href = '/login'; | |
| }); | |
| // Modern message display | |
| function appendMessage(role, text) { | |
| const msgDiv = document.createElement("div"); | |
| msgDiv.className = `msg ${role === "user" ? "user" : "bot"}`; | |
| const contentDiv = document.createElement("div"); | |
| contentDiv.className = "msg-content"; | |
| const metaDiv = document.createElement("div"); | |
| metaDiv.className = "msg-meta"; | |
| metaDiv.textContent = role === "user" ? "You" : "AIMHSA"; | |
| const textDiv = document.createElement("div"); | |
| textDiv.className = "msg-text"; | |
| textDiv.textContent = text; | |
| contentDiv.appendChild(metaDiv); | |
| contentDiv.appendChild(textDiv); | |
| msgDiv.appendChild(contentDiv); | |
| messagesEl.appendChild(msgDiv); | |
| ensureScroll(); | |
| return msgDiv; | |
| } | |
| function createTypingIndicator() { | |
| if (typingEl) return; | |
| typingEl = document.createElement("div"); | |
| typingEl.className = "msg bot"; | |
| const contentDiv = document.createElement("div"); | |
| contentDiv.className = "typing"; | |
| const dotsDiv = document.createElement("div"); | |
| dotsDiv.className = "typing-dots"; | |
| dotsDiv.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>'; | |
| contentDiv.appendChild(dotsDiv); | |
| typingEl.appendChild(contentDiv); | |
| messagesEl.appendChild(typingEl); | |
| ensureScroll(); | |
| } | |
| function removeTypingIndicator() { | |
| if (!typingEl) return; | |
| typingEl.remove(); | |
| typingEl = null; | |
| } | |
| async function api(path, opts) { | |
| // Try multiple endpoint patterns to handle both app.py and run_aimhsa.py | |
| const endpoints = [ | |
| API_BASE_URL + path, // Direct path (app.py style) | |
| API_BASE_URL + '/api' + path // API prefixed path (run_aimhsa.py style) | |
| ]; | |
| let lastError; | |
| for (const url of endpoints) { | |
| try { | |
| const res = await fetch(url, opts); | |
| if (res.ok) { | |
| return res.json(); | |
| } | |
| if (res.status === 404 && url === endpoints[0]) { | |
| // Try the next endpoint | |
| continue; | |
| } | |
| // If not 404 or this is the last endpoint, handle the error | |
| const txt = await res.text(); | |
| throw new Error(txt || res.statusText); | |
| } catch (error) { | |
| lastError = error; | |
| if (url === endpoints[0] && error.message.includes('404')) { | |
| // Try the next endpoint | |
| continue; | |
| } | |
| // If not a 404 or this is the last endpoint, throw the error | |
| throw error; | |
| } | |
| } | |
| // If we get here, all endpoints failed | |
| throw lastError || new Error('All API endpoints failed'); | |
| } | |
| async function initSession(useAccount = false) { | |
| const payload = {}; | |
| if (useAccount && account) payload.account = account; | |
| try { | |
| const resp = await api("/session", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| convId = resp.id; | |
| localStorage.setItem("aimhsa_conv", convId); | |
| await loadHistory(); | |
| await updateHistoryList(); | |
| } catch (err) { | |
| console.error("session error", err); | |
| // Fallback: create a client-side conversation ID if server session fails | |
| if (!convId) { | |
| convId = newConvId(); | |
| localStorage.setItem("aimhsa_conv", convId); | |
| } | |
| appendMessage("bot", "Session initialized. How can I help you today?"); | |
| } | |
| } | |
| // helper to generate a client-side conv id when needed (fallback) | |
| function newConvId() { | |
| if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); | |
| return "conv-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2,8); | |
| } | |
| async function loadHistory() { | |
| if (!convId) return; | |
| try { | |
| const pw = archivedPwById.get(convId); | |
| const url = "/history?id=" + encodeURIComponent(convId) + (pw ? ("&password=" + encodeURIComponent(pw)) : ""); | |
| const resp = await api(url); | |
| messagesEl.innerHTML = ""; | |
| const hist = resp.history || []; | |
| for (const m of hist) { | |
| appendMessage(m.role, m.content); | |
| } | |
| if (resp.attachments && resp.attachments.length) { | |
| resp.attachments.forEach(a => { | |
| appendMessage("bot", `Attachment (${a.filename}):\n` + (a.text.slice(0,400) + (a.text.length>400?"...[truncated]":""))); | |
| }); | |
| } | |
| ensureScroll(); | |
| } catch (err) { | |
| console.error("history load error", err); | |
| // If history fails to load, just show a welcome message | |
| if (messagesEl.children.length === 0) { | |
| appendMessage("bot", "Welcome! How can I help you today?"); | |
| } | |
| } | |
| } | |
| // Auto-resize textarea | |
| function autoResizeTextarea() { | |
| queryInput.style.height = 'auto'; | |
| const scrollHeight = queryInput.scrollHeight; | |
| const maxHeight = 120; // Match CSS max-height | |
| queryInput.style.height = Math.min(scrollHeight, maxHeight) + 'px'; | |
| } | |
| // Add textarea auto-resize listener | |
| queryInput.addEventListener('input', autoResizeTextarea); | |
| queryInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| form.dispatchEvent(new Event('submit')); | |
| } | |
| }); | |
| async function sendMessage(query) { | |
| if (!query) return; | |
| disableComposer(true); | |
| appendMessage("user", query); | |
| createTypingIndicator(); | |
| queryInput.value = ""; | |
| autoResizeTextarea(); // Reset textarea height | |
| try { | |
| // include account so server can bind new convs to the logged-in user | |
| const payload = { id: convId, query, history: [] }; | |
| if (account) payload.account = account; | |
| const model = getSelectedModel(); | |
| if (model) payload.model = model; | |
| const resp = await api("/ask", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| removeTypingIndicator(); | |
| // Handle scope rejection with special styling | |
| if (resp.scope_rejection) { | |
| const botMessage = appendMessage("assistant", resp.answer); | |
| botMessage.classList.add("scope-rejection"); | |
| // Add visual indicator for scope rejection | |
| const indicator = document.createElement("div"); | |
| indicator.className = "scope-indicator"; | |
| indicator.innerHTML = "🎯 Mental Health Focus"; | |
| indicator.style.cssText = ` | |
| font-size: 12px; | |
| color: #f59e0b; | |
| background: rgba(245, 158, 11, 0.1); | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| margin-top: 8px; | |
| display: inline-block; | |
| `; | |
| botMessage.querySelector('.msg-content').appendChild(indicator); | |
| } else { | |
| // Ensure we got a valid response | |
| if (!resp.answer || resp.answer.trim() === '') { | |
| appendMessage("assistant", "I'm here to help. Could you please rephrase your question?"); | |
| } else { | |
| appendMessage("assistant", resp.answer); | |
| } | |
| } | |
| // Risk assessment is handled in backend only (no display) | |
| // But show booking confirmation to user | |
| if (resp.emergency_booking) { | |
| displayEmergencyBooking(resp.emergency_booking); | |
| } | |
| // Handle booking question from backend | |
| if (resp.ask_booking) { | |
| displayBookingQuestion(resp.ask_booking); | |
| } | |
| if (resp.id && resp.id !== convId) { | |
| convId = resp.id; | |
| localStorage.setItem("aimhsa_conv", convId); | |
| } | |
| // refresh server-side conversation list for signed-in users | |
| if (account) await updateHistoryList(); | |
| } catch (err) { | |
| console.error("ask error", err); | |
| removeTypingIndicator(); | |
| // Provide helpful error message based on error type | |
| let errorMessage = "I'm having trouble connecting to the server. Please check your internet connection and try again."; | |
| if (err.message && err.message.includes('405')) { | |
| errorMessage = "There's a server configuration issue. Please try refreshing the page or contact support."; | |
| } else if (err.message && err.message.includes('500')) { | |
| errorMessage = "The server encountered an error. Please try again in a moment."; | |
| } | |
| appendMessage("bot", errorMessage); | |
| } finally { | |
| disableComposer(false); | |
| } | |
| } | |
| // show upload preview block when a file is selected | |
| function showUploadPreview(file) { | |
| clearUploadPreview(); | |
| const preview = document.createElement("div"); | |
| preview.className = "upload-preview fade-in"; | |
| preview.dataset.name = file.name; | |
| const icon = document.createElement("div"); | |
| icon.style.fontSize = "20px"; | |
| icon.textContent = "📄"; | |
| const meta = document.createElement("div"); | |
| meta.className = "upload-meta"; | |
| const fname = document.createElement("div"); | |
| fname.className = "upload-filename"; | |
| fname.textContent = file.name; | |
| const fsize = document.createElement("div"); | |
| fsize.className = "small"; | |
| fsize.textContent = `${(file.size/1024).toFixed(1)} KB`; | |
| meta.appendChild(fname); | |
| meta.appendChild(fsize); | |
| const actions = document.createElement("div"); | |
| actions.className = "upload-actions"; | |
| const progress = document.createElement("div"); | |
| progress.className = "progress-bar"; | |
| const inner = document.createElement("div"); | |
| inner.className = "progress-inner"; | |
| progress.appendChild(inner); | |
| const removeBtn = document.createElement("button"); | |
| removeBtn.className = "btn-small"; | |
| removeBtn.type = "button"; | |
| removeBtn.textContent = "Remove"; | |
| removeBtn.addEventListener("click", () => { | |
| fileInput.value = ""; | |
| clearUploadPreview(); | |
| }); | |
| actions.appendChild(progress); | |
| actions.appendChild(removeBtn); | |
| preview.appendChild(icon); | |
| preview.appendChild(meta); | |
| preview.appendChild(actions); | |
| // insert preview at left of composer (before send button) | |
| composer.insertBefore(preview, composer.firstChild); | |
| currentPreview = { el: preview, inner }; | |
| } | |
| function updateUploadProgress(pct) { | |
| if (!currentPreview) return; | |
| currentPreview.inner.style.width = Math.max(0, Math.min(100, pct)) + "%"; | |
| } | |
| function clearUploadPreview() { | |
| if (currentPreview && currentPreview.el) currentPreview.el.remove(); | |
| currentPreview = null; | |
| } | |
| // Use XHR for upload to track progress | |
| function uploadPdf(file) { | |
| if (!file) return; | |
| disableComposer(true); | |
| showUploadPreview(file); | |
| // Try both endpoint patterns | |
| const tryUpload = (url) => { | |
| return new Promise((resolve, reject) => { | |
| const xhr = new XMLHttpRequest(); | |
| xhr.open("POST", url, true); | |
| xhr.upload.onprogress = function(e) { | |
| if (e.lengthComputable) { | |
| const pct = Math.round((e.loaded / e.total) * 100); | |
| updateUploadProgress(pct); | |
| } | |
| }; | |
| xhr.onload = function() { | |
| try { | |
| const resText = xhr.responseText || "{}"; | |
| const data = JSON.parse(resText); | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| resolve(data); | |
| } else { | |
| reject(new Error(data.error || xhr.statusText)); | |
| } | |
| } catch (err) { | |
| reject(new Error("Upload parsing error")); | |
| } | |
| }; | |
| xhr.onerror = function() { | |
| reject(new Error("Upload network error")); | |
| }; | |
| const fd = new FormData(); | |
| fd.append("file", file, file.name); | |
| if (convId) fd.append("id", convId); | |
| if (account) fd.append("account", account); | |
| const model = getSelectedModel(); | |
| if (model) fd.append("model", model); | |
| xhr.send(fd); | |
| }); | |
| }; | |
| // Try upload_pdf endpoint first, then api/upload_pdf as fallback | |
| tryUpload(API_BASE_URL + "/upload_pdf") | |
| .catch(() => tryUpload(API_BASE_URL + "/api/upload_pdf")) | |
| .then((data) => { | |
| disableComposer(false); | |
| convId = data.id; | |
| localStorage.setItem("aimhsa_conv", convId); | |
| appendMessage("bot", `Uploaded ${data.filename}. What would you like to know about this document?`); | |
| clearUploadPreview(); | |
| if (account) updateHistoryList(); | |
| }) | |
| .catch((error) => { | |
| disableComposer(false); | |
| console.error("PDF upload failed:", error); | |
| appendMessage("bot", "PDF upload failed: " + error.message); | |
| clearUploadPreview(); | |
| }); | |
| } | |
| function disableComposer(disabled) { | |
| if (disabled) { | |
| sendBtn.disabled = true; | |
| sendBtn.classList.add("sending"); | |
| fileInput.disabled = true; | |
| queryInput.disabled = true; | |
| } else { | |
| sendBtn.disabled = false; | |
| sendBtn.classList.remove("sending"); | |
| fileInput.disabled = false; | |
| queryInput.disabled = false; | |
| } | |
| } | |
| // New chat: require account (server enforces too) | |
| newChatBtn.addEventListener('click', async () => { | |
| if (!account) { | |
| appendMessage("bot", "Please sign in to create and view saved conversations."); | |
| return; | |
| } | |
| try { | |
| const payload = { account }; | |
| const resp = await api("/conversations", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (resp && resp.id) { | |
| convId = resp.id; | |
| localStorage.setItem("aimhsa_conv", convId); | |
| messagesEl.innerHTML = ''; | |
| await updateHistoryList(); | |
| } | |
| } catch (e) { | |
| console.error("failed to create conversation", e); | |
| appendMessage("bot", "Could not start new conversation. Try again."); | |
| } | |
| }); | |
| // Clear only visual messages | |
| clearChatBtn.addEventListener("click", () => { | |
| if (!convId) return; | |
| if (confirm("Clear current messages? This will only clear the visible chat.")) { | |
| messagesEl.innerHTML = ""; | |
| appendMessage("bot", "Messages cleared. How can I help you?"); | |
| } | |
| }); | |
| // Clear server-side history | |
| clearHistoryBtn.addEventListener("click", async () => { | |
| if (!convId) return; | |
| if (confirm("Are you sure? This will permanently clear all saved messages and attachments.")) { | |
| try { | |
| await api("/clear_chat", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ id: convId }) | |
| }); | |
| // Clear both messages and conversation history | |
| messagesEl.innerHTML = ""; | |
| historyList.innerHTML = ""; | |
| // Add default "no conversations" message | |
| const note = document.createElement('div'); | |
| note.className = 'small'; | |
| note.style.padding = '12px'; | |
| note.style.color = 'var(--muted)'; | |
| note.textContent = 'No conversations yet. Start a new chat!'; | |
| historyList.appendChild(note); | |
| appendMessage("bot", "Chat history cleared. How can I help you?"); | |
| // Start a new conversation if account exists | |
| if (account && account !== 'null') { | |
| const payload = { account }; | |
| const resp = await api("/conversations", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (resp && resp.id) { | |
| convId = resp.id; | |
| localStorage.setItem("aimhsa_conv", convId); | |
| await updateHistoryList(); | |
| } | |
| } | |
| } catch (err) { | |
| console.error("Failed to clear chat history", err); | |
| appendMessage("bot", "Failed to clear chat history on server. Try again."); | |
| } | |
| } | |
| }); | |
| // show preview when file selected | |
| fileInput.addEventListener("change", (e) => { | |
| const f = fileInput.files[0]; | |
| if (f) showUploadPreview(f); | |
| else clearUploadPreview(); | |
| }); | |
| const app = document.querySelector('.app'); | |
| // Replace existing drag/drop handlers with: | |
| document.addEventListener('dragenter', (e) => { | |
| e.preventDefault(); | |
| if (!e.dataTransfer.types.includes('Files')) return; | |
| app.classList.add('dragging'); | |
| }); | |
| document.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| // Only remove if actually leaving the app | |
| if (e.target === document || e.target === app) { | |
| app.classList.remove('dragging'); | |
| } | |
| }); | |
| document.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| app.classList.remove('dragging'); | |
| const files = Array.from(e.dataTransfer.files); | |
| const pdfFile = files.find(f => f.type === 'application/pdf'); | |
| if (pdfFile) { | |
| fileInput.files = e.dataTransfer.files; | |
| const event = new Event('change'); | |
| fileInput.dispatchEvent(event); | |
| uploadPdf(pdfFile); | |
| } else { | |
| appendMessage('bot', 'Please drop a PDF file.'); | |
| } | |
| }); | |
| form.addEventListener("submit", (e) => { | |
| e.preventDefault(); | |
| const q = queryInput.value.trim(); | |
| if (!q && !fileInput.files[0]) return; | |
| const file = fileInput.files[0]; | |
| if (file) { | |
| uploadPdf(file); | |
| fileInput.value = ""; | |
| } else { | |
| // ensure a convId exists for anonymous users too | |
| if (!convId) { | |
| convId = newConvId(); | |
| localStorage.setItem("aimhsa_conv", convId); | |
| } | |
| sendMessage(q); | |
| } | |
| }); | |
| // require signed-in account for server-backed conversations; otherwise show prompt | |
| async function updateHistoryList() { | |
| historyList.innerHTML = ''; | |
| if (archivedList) archivedList.innerHTML = ''; | |
| if (!account || account === 'null') { | |
| const note = document.createElement('div'); | |
| note.className = 'small'; | |
| note.style.padding = '12px'; | |
| note.style.color = 'var(--text-muted)'; | |
| note.textContent = 'Sign in to view and manage your conversation history.'; | |
| historyList.appendChild(note); | |
| newChatBtn.disabled = true; | |
| newChatBtn.title = "Sign in to create server-backed conversations"; | |
| return; | |
| } | |
| newChatBtn.disabled = false; | |
| newChatBtn.title = ""; | |
| try { | |
| const q = "?account=" + encodeURIComponent(account); | |
| const resp = await api("/conversations" + q, { method: "GET" }); | |
| const entries = resp.conversations || []; | |
| for (const historyData of entries) { | |
| const item = document.createElement('div'); | |
| item.className = 'history-item' + (historyData.id === convId ? ' active' : ''); | |
| const preview = document.createElement('div'); | |
| preview.className = 'history-preview'; | |
| preview.textContent = historyData.preview || 'New chat'; | |
| preview.title = historyData.preview || 'New chat'; | |
| // three-dot menu button | |
| const menuBtn = document.createElement('button'); | |
| menuBtn.className = 'history-menu-btn'; | |
| menuBtn.setAttribute('aria-label', 'Conversation actions'); | |
| menuBtn.title = 'More'; | |
| menuBtn.textContent = '...'; | |
| // dropdown menu | |
| const menu = document.createElement('div'); | |
| menu.className = 'history-menu'; | |
| const renameBtn = document.createElement('button'); | |
| renameBtn.textContent = 'Rename'; | |
| const archiveBtn = document.createElement('button'); | |
| archiveBtn.textContent = 'Archive'; | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.textContent = 'Delete'; | |
| deleteBtn.className = 'danger'; | |
| menu.appendChild(renameBtn); | |
| menu.appendChild(archiveBtn); | |
| menu.appendChild(deleteBtn); | |
| // rename | |
| renameBtn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| const title = prompt('Rename conversation to:'); | |
| if (title == null || title.trim() === '') return; | |
| try { | |
| await api('/conversations/rename', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ account, id: historyData.id, preview: title }) | |
| }); | |
| await updateHistoryList(); | |
| } catch (err) { | |
| appendMessage('bot', 'Failed to rename conversation.'); | |
| } | |
| }); | |
| // selection | |
| item.addEventListener('click', () => switchConversation(historyData.id)); | |
| // open/close menu | |
| menuBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const isOpen = menu.classList.contains('open'); | |
| document.querySelectorAll('.history-menu.open').forEach(m => m.classList.remove('open')); | |
| if (!isOpen) menu.classList.add('open'); | |
| }); | |
| document.addEventListener('click', () => { | |
| menu.classList.remove('open'); | |
| }); | |
| // archive (password required) | |
| archiveBtn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| let pw = prompt('Set a password to archive this conversation (required).'); | |
| if (pw == null || pw.trim() === '') { appendMessage('bot', 'Archive cancelled: password required.'); return; } | |
| try { | |
| await api('/conversations/archive', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ account, id: historyData.id, archived: true, password: pw || '' }) | |
| }); | |
| if (historyData.id === convId) { | |
| messagesEl.innerHTML = ''; | |
| convId = null; | |
| localStorage.removeItem('aimhsa_conv'); | |
| } | |
| await updateHistoryList(); | |
| } catch (err) { | |
| console.error('archive conversation failed', err); | |
| appendMessage('bot', 'Failed to archive conversation.'); | |
| } | |
| }); | |
| // delete | |
| deleteBtn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| if (!confirm('Delete this conversation? This cannot be undone.')) return; | |
| try { | |
| await api('/conversations/delete', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ account, id: historyData.id }) | |
| }); | |
| if (historyData.id === convId) { | |
| messagesEl.innerHTML = ''; | |
| convId = null; | |
| localStorage.removeItem('aimhsa_conv'); | |
| } | |
| await updateHistoryList(); | |
| } catch (err) { | |
| console.error('delete conversation failed', err); | |
| appendMessage('bot', 'Failed to delete conversation.'); | |
| } | |
| }); | |
| item.appendChild(preview); | |
| item.appendChild(menuBtn); | |
| item.appendChild(menu); | |
| historyList.appendChild(item); | |
| } | |
| // load archived | |
| try { | |
| const ar = await api('/conversations/archived' + q, { method: 'GET' }); | |
| const archivedEntries = ar.conversations || []; | |
| for (const h of archivedEntries) { | |
| const item = document.createElement('div'); | |
| item.className = 'history-item'; | |
| const preview = document.createElement('div'); | |
| preview.className = 'history-preview'; | |
| preview.textContent = h.preview || 'New chat'; | |
| preview.title = h.preview || 'New chat'; | |
| const menuBtn = document.createElement('button'); | |
| menuBtn.className = 'history-menu-btn'; | |
| menuBtn.textContent = '...'; | |
| const menu = document.createElement('div'); | |
| menu.className = 'history-menu'; | |
| const unarchiveBtn = document.createElement('button'); | |
| unarchiveBtn.textContent = 'Unarchive'; | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.textContent = 'Delete'; | |
| deleteBtn.className = 'danger'; | |
| // do not allow rename for archived | |
| menu.appendChild(unarchiveBtn); | |
| menu.appendChild(deleteBtn); | |
| item.addEventListener('click', async () => { | |
| try { | |
| await api('/history?id=' + encodeURIComponent(h.id)); | |
| archivedPwById.delete(h.id); | |
| await switchConversation(h.id); | |
| } catch (e) { | |
| try { | |
| const pw = prompt('Enter password to open this archived conversation:'); | |
| if (pw == null) return; | |
| await api('/history?id=' + encodeURIComponent(h.id) + '&password=' + encodeURIComponent(pw)); | |
| archivedPwById.set(h.id, pw); | |
| await switchConversation(h.id); | |
| } catch (e2) { | |
| appendMessage('bot', 'Incorrect or missing password.'); | |
| } | |
| } | |
| }); | |
| menuBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const isOpen = menu.classList.contains('open'); | |
| document.querySelectorAll('.history-menu.open').forEach(m => m.classList.remove('open')); | |
| if (!isOpen) menu.classList.add('open'); | |
| }); | |
| document.addEventListener('click', () => { menu.classList.remove('open'); }); | |
| unarchiveBtn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| const pw = prompt('Enter archive password to unarchive:'); | |
| if (pw == null || pw.trim() === '') { appendMessage('bot', 'Unarchive cancelled: password required.'); return; } | |
| try { | |
| await api('/conversations/archive', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ account, id: h.id, archived: false, password: pw }) | |
| }); | |
| await updateHistoryList(); | |
| } catch (err) { | |
| appendMessage('bot', 'Failed to unarchive conversation.'); | |
| } | |
| }); | |
| deleteBtn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| if (!confirm('Delete this conversation? This cannot be undone.')) return; | |
| const pw = prompt('Enter archive password to delete:'); | |
| if (pw == null || pw.trim() === '') { appendMessage('bot', 'Delete cancelled: password required.'); return; } | |
| try { | |
| await api('/conversations/delete', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ account, id: h.id, password: pw }) | |
| }); | |
| await updateHistoryList(); | |
| } catch (err) { | |
| appendMessage('bot', 'Failed to delete conversation.'); | |
| } | |
| }); | |
| item.appendChild(preview); | |
| item.appendChild(menuBtn); | |
| item.appendChild(menu); | |
| if (archivedList) archivedList.appendChild(item); | |
| } | |
| } catch (e2) { | |
| // ignore archived load errors, show main list anyway | |
| } | |
| } catch (e) { | |
| console.warn("failed to load conversations", e); | |
| const errNote = document.createElement('div'); | |
| errNote.className = 'small'; | |
| errNote.style.padding = '12px'; | |
| errNote.style.color = 'var(--muted)'; | |
| errNote.textContent = 'Unable to load conversations.'; | |
| historyList.appendChild(errNote); | |
| } | |
| } | |
| // switch conversation -> set convId, persist selection and load history | |
| async function switchConversation(newConvId) { | |
| if (!newConvId || newConvId === convId) return; | |
| convId = newConvId; | |
| localStorage.setItem("aimhsa_conv", convId); | |
| await loadHistory(); | |
| await updateHistoryList(); | |
| } | |
| // Risk assessment is handled in backend only (no display) | |
| // But show booking confirmation to user | |
| function displayBookingQuestion(bookingQuestion) { | |
| // Create booking question card | |
| const questionCard = document.createElement('div'); | |
| questionCard.className = 'booking-question-card'; | |
| questionCard.style.cssText = ` | |
| margin: 12px 0; | |
| padding: 20px; | |
| border-radius: 8px; | |
| background: linear-gradient(135deg, #3b82f6, #1d4ed8); | |
| color: white; | |
| border: 2px solid #2563eb; | |
| box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); | |
| `; | |
| questionCard.innerHTML = ` | |
| <div style="display: flex; align-items: center; margin-bottom: 12px;"> | |
| <div style="font-size: 24px; margin-right: 12px;">💬</div> | |
| <h3 style="margin: 0; font-size: 18px; font-weight: 700;">Professional Support Available</h3> | |
| </div> | |
| <p style="margin: 0 0 16px 0; font-size: 16px; opacity: 0.9;"> | |
| ${bookingQuestion.message} | |
| </p> | |
| <div style="display: flex; gap: 12px;"> | |
| <button id="booking-yes" style=" | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 6px; | |
| background: white; | |
| color: #3b82f6; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| ">${bookingQuestion.options[0]}</button> | |
| <button id="booking-no" style=" | |
| padding: 12px 24px; | |
| border: 2px solid white; | |
| border-radius: 6px; | |
| background: transparent; | |
| color: white; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| ">${bookingQuestion.options[1]}</button> | |
| </div> | |
| `; | |
| // Insert after the last message | |
| const lastMessage = messagesEl.lastElementChild; | |
| if (lastMessage) { | |
| lastMessage.parentNode.insertBefore(questionCard, lastMessage.nextSibling); | |
| } | |
| // Add event listeners | |
| document.getElementById('booking-yes').addEventListener('click', () => { | |
| handleBookingResponse('yes'); | |
| questionCard.remove(); | |
| }); | |
| document.getElementById('booking-no').addEventListener('click', () => { | |
| handleBookingResponse('no'); | |
| questionCard.remove(); | |
| }); | |
| // Scroll to show the question | |
| questionCard.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| async function handleBookingResponse(response) { | |
| try { | |
| const res = await api('/booking_response', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| conversation_id: convId, | |
| response: response, | |
| account: localStorage.getItem('aimhsa_account') | |
| }) | |
| }); | |
| const data = res; // api() already returns parsed JSON | |
| if (response === 'yes' && data.booking) { | |
| // Show booking confirmation | |
| displayEmergencyBooking(data.booking); | |
| } else { | |
| // Show acknowledgment message | |
| appendMessage("assistant", data.message || "No problem! I'm here whenever you need support."); | |
| } | |
| } catch (error) { | |
| console.error('Booking response error:', error); | |
| appendMessage("assistant", "Sorry, there was an error processing your response. Please try again."); | |
| } | |
| } | |
| // Removed language indicator UI for a cleaner experience | |
| function displayEmergencyBooking(booking) { | |
| const scheduledTime = new Date(booking.scheduled_time * 1000).toLocaleString(); | |
| // Create emergency booking notification | |
| const bookingCard = document.createElement('div'); | |
| bookingCard.className = 'emergency-booking-card'; | |
| bookingCard.style.cssText = ` | |
| margin: 12px 0; | |
| padding: 20px; | |
| border-radius: 8px; | |
| background: linear-gradient(135deg, #dc2626, #b91c1c); | |
| color: white; | |
| border: 2px solid #ef4444; | |
| box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3); | |
| `; | |
| bookingCard.innerHTML = ` | |
| <div style="display: flex; align-items: center; margin-bottom: 12px;"> | |
| <div style="font-size: 24px; margin-right: 12px;">🚨</div> | |
| <h3 style="margin: 0; font-size: 18px; font-weight: 700;">Emergency Session Scheduled</h3> | |
| </div> | |
| <div style="background: rgba(255, 255, 255, 0.1); padding: 12px; border-radius: 6px; margin-bottom: 12px;"> | |
| <p style="margin: 0 0 8px 0; font-size: 14px;"><strong>Professional:</strong> ${booking.professional_name}</p> | |
| <p style="margin: 0 0 8px 0; font-size: 14px;"><strong>Specialization:</strong> ${booking.specialization}</p> | |
| <p style="margin: 0 0 8px 0; font-size: 14px;"><strong>Scheduled:</strong> ${scheduledTime}</p> | |
| <p style="margin: 0; font-size: 14px;"><strong>Session Type:</strong> ${booking.session_type}</p> | |
| </div> | |
| <p style="margin: 0; font-size: 14px; opacity: 0.9;"> | |
| A mental health professional has been automatically assigned to provide immediate support. | |
| They will contact you shortly to confirm the session details. | |
| </p> | |
| `; | |
| // Insert after the last message | |
| const lastMessage = messagesEl.lastElementChild; | |
| if (lastMessage) { | |
| lastMessage.parentNode.insertBefore(bookingCard, lastMessage.nextSibling); | |
| } | |
| // Scroll to show the notification | |
| bookingCard.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| // initial load: start session (account-bound when available) and refresh history list | |
| (async () => { | |
| if (account) { | |
| await initSession(true); | |
| } else { | |
| await initSession(false); | |
| } | |
| await updateHistoryList(); | |
| })(); | |
| })(); |