TrinetraLabs/Placebo_AI
0
1 2 // --- SUPABASE CLIENT INITIALIZATION ---3 let supabaseClient = null;4 let activeUser = null;5 let activeToken = null;6 7 async function initializeSupabase() {8 try {9 const response = await fetch("/config");10 const config = await response.json();11 const SUPABASE_URL = config.supabase_url;12 const SUPABASE_ANON_KEY = config.supabase_anon_key;13 14 if (window.supabase && typeof window.supabase.createClient === 'function' && SUPABASE_URL && SUPABASE_URL !== "https://your-project-id.supabase.co") {15 // Wipe any old local storage sessions lingering from before this feature16 for (let key in localStorage) {17 if (key.startsWith('sb-') && key.endsWith('-auth-token')) {18 localStorage.removeItem(key);19 }20 }21 22 supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {23 auth: {24 persistSession: false // Forces logout on every page refresh25 }26 });27 console.log("Supabase Client initialized with ephemeral sessions. Testing connectivity...");28 29 // Connection ping test with 2-second timeout30 const pingPromise = supabaseClient.auth.getSession();31 const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Supabase connection timeout")), 2000));32 33 await Promise.race([pingPromise, timeoutPromise]);34 console.log("Supabase connectivity verified successfully.");35 36 supabaseClient.auth.onAuthStateChange((event, session) => {37 console.log("Auth State Changed Event:", event);38 updateAuthState();39 });40 } else {41 console.warn("Supabase configuration is using placeholder credentials or offline.");42 }43 } catch (e) {44 console.warn("Failed to verify Supabase connectivity or load config.", e);45 supabaseClient = null;46 }47 // Sync current authentication state on load48 await updateAuthState();49 }50 51 async function updateAuthState() {52 const authButtons = document.getElementById('nav-auth-buttons');53 const userButtons = document.getElementById('nav-user-buttons');54 const displayEmail = document.getElementById('user-display-email');55 56 if (supabaseClient) {57 try {58 const { data: { session } } = await supabaseClient.auth.getSession();59 if (session) {60 activeUser = session.user;61 activeToken = session.access_token;62 } else {63 activeUser = null;64 activeToken = null;65 }66 } catch (err) {67 console.error("Error fetching Supabase session:", err);68 }69 }70 71 if (activeUser) {72 authButtons.style.display = 'none';73 userButtons.style.display = 'flex';74 displayEmail.innerText = activeUser.email;75 76 let userDefaultMode = activeUser.user_metadata?.defaultMode;77 if (!userDefaultMode) {78 document.getElementById('profession-modal').style.display = 'flex';79 } else {80 activeMode = userDefaultMode;81 updateModeButtons(activeMode);82 }83 84 // Dynamically update user limits85 const creditText = document.querySelector('.credit-info span');86 if (creditText) {87 let userCredits = activeUser.user_metadata?.credits_remaining !== undefined ? activeUser.user_metadata.credits_remaining : 500;88 if (userCredits > 500) userCredits = 500; // Cap grandfathered users at 50089 creditText.innerText = `${userCredits}/500 credits`;90 }91 } else {92 authButtons.style.display = 'flex';93 userButtons.style.display = 'none';94 activeToken = null;95 const creditText = document.querySelector('.credit-info span');96 if (creditText) {97 creditText.innerText = "Log in to check credits";98 }99 }100 }101 102 // Initialize state check103 window.addEventListener('DOMContentLoaded', () => {104 initializeSupabase();105 });106 107 const chatForm = document.getElementById('chat-form');108 const video = document.getElementById('hero-video');109 let opacity = 1;110 111 function setOpacity(val) {112 opacity = val;113 video.style.opacity = opacity;114 }115 116 if (video.readyState >= 1) {117 video.play();118 setOpacity(1);119 } else {120 video.addEventListener('loadedmetadata', () => {121 video.play();122 setOpacity(1);123 });124 }125 126 setTimeout(() => setOpacity(1), 2000);127 128 const chatInput = document.getElementById('chat-input');129 const sendBtn = document.getElementById('send-btn');130 const charCount = document.getElementById('char-count');131 const responsePanel = document.getElementById('response-panel');132 const responseText = document.getElementById('response-text');133 const sourceList = document.getElementById('source-list');134 const pageModal = document.getElementById('page-modal');135 const modalImg = document.getElementById('page-preview-img');136 const modalTitle = document.getElementById('modal-title');137 const closeModal = document.getElementById('close-modal');138 const clearBtn = document.getElementById('clear-btn');139 140 let activeMode = 'unified';141 142 function updateModeButtons(mode) {143 const buttons = document.querySelectorAll('.model-btn');144 buttons.forEach(b => {145 b.classList.remove('active');146 if (b.getAttribute('data-mode') === mode || (mode === 'unified' && b.getAttribute('data-mode') === 'all')) {147 b.classList.add('active');148 }149 });150 }151 152 document.querySelectorAll('.profession-btn').forEach(btn => {153 btn.onclick = async () => {154 const mode = btn.getAttribute('data-mode');155 if (supabaseClient && activeUser) {156 btn.innerText = "Saving...";157 const { data, error } = await supabaseClient.auth.updateUser({158 data: { defaultMode: mode }159 });160 if (!error) {161 activeUser = data.user;162 activeMode = mode;163 updateModeButtons(mode);164 document.getElementById('profession-modal').style.display = 'none';165 166 // Reset button text in case they log out and it re-shows167 btn.innerText = mode === "mbbs" ? "MBBS Student / Doctor" : 168 (mode === "pharmacy" ? "Pharmacy Student / Pharmacist" : "Unified (Both Domains)");169 }170 }171 };172 });173 174 const modelButtons = document.querySelectorAll('.model-btn');175 modelButtons.forEach(btn => {176 btn.addEventListener('click', () => {177 modelButtons.forEach(b => b.classList.remove('active'));178 btn.classList.add('active');179 activeMode = btn.getAttribute('data-mode');180 });181 });182 183 if (clearBtn) {184 clearBtn.addEventListener('click', (e) => {185 e.preventDefault();186 responseText.innerHTML = '';187 sourceList.innerHTML = '';188 responsePanel.style.display = 'none';189 chatInput.value = '';190 charCount.innerText = '0/3,000';191 });192 }193 194 chatInput.addEventListener('input', () => {195 charCount.innerText = `${chatInput.value.length}/3,000`;196 });197 198 const historyBtn = document.getElementById('history-btn');199 const promptsBtn = document.getElementById('prompts-btn');200 const sideDrawer = document.getElementById('side-drawer');201 const closeDrawer = document.getElementById('close-drawer');202 const drawerTitle = document.getElementById('drawer-title');203 const drawerItems = document.getElementById('drawer-items');204 205 const PREDEFINED_PROMPTS = [206 "Explain the clinical presentation, diagnosis, and surgical management of acute appendicitis.",207 "What are the histological features of a myocardial infarction across different time frames?",208 "Explain the mechanism of action, pharmacokinetics, and adverse effects of Aspirin.",209 "What is the principle behind Non-aqueous Titrations and which indicators are used?",210 "Explain Type II Diabetes Mellitus, including its pathophysiology and the pharmacological management using Metformin.",211 "Describe the physiological pathway of the Renin-Angiotensin-Aldosterone System (RAAS)."212 ];213 214 async function saveToHistory(query, answer = '', sources = []) {215 if (!activeUser || !supabaseClient) return;216 217 try {218 await supabaseClient.from('chat_history').insert({219 user_id: activeUser.id,220 query: query,221 answer: answer,222 sources: sources223 });224 } catch (error) {225 console.error('Error saving chat history to Supabase:', error);226 }227 }228 229 function displayStoredResult(item) {230 const queryHTML = `<div style="font-size: 16px; font-weight: 500; color: white; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.1);"><span style="opacity: 0.5; font-size: 12px; display: block; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 1px;">You Asked:</span>${item.query}</div>`;231 232 sourceList.innerHTML = '';233 item.sources.forEach(src => {234 const tag = document.createElement('span');235 tag.className = 'source-tag';236 tag.innerText = `${src.book_name} (Pg ${src.page_number})`;237 tag.style.cursor = 'default';238 sourceList.appendChild(tag);239 });240 241 responseText.innerHTML = queryHTML + DOMPurify.sanitize(applyMedicalTooltips(marked.parse(item.answer)), { ADD_ATTR: ['data-tooltip'] });242 243 responsePanel.style.display = 'flex';244 chatInput.value = item.query;245 sideDrawer.classList.remove('open');246 window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });247 }248 249 async function openDrawer(type) {250 drawerItems.innerHTML = '';251 if (type === 'history') {252 drawerTitle.innerText = 'History';253 254 if (!activeUser || !supabaseClient) {255 drawerItems.innerHTML = '<div style="opacity: 0.5; text-align: center; margin-top: 20px;">Please log in to view history.</div>';256 sideDrawer.classList.add('open');257 return;258 }259 260 drawerItems.innerHTML = '<div style="opacity: 0.5; text-align: center; margin-top: 20px;">Loading...</div>';261 sideDrawer.classList.add('open');262 263 try {264 const { data: history, error } = await supabaseClient265 .from('chat_history')266 .select('*')267 .order('created_at', { ascending: false })268 .limit(20);269 270 drawerItems.innerHTML = '';271 if (error || !history || history.length === 0) {272 drawerItems.innerHTML = '<div style="opacity: 0.5; text-align: center; margin-top: 20px;">No history yet.</div>';273 } else {274 history.forEach(item => {275 const el = document.createElement('div');276 el.className = 'drawer-item';277 const dateStr = new Date(item.created_at).toLocaleDateString();278 el.innerHTML = `${item.query} <span class="time">${dateStr}</span>`;279 el.onclick = () => displayStoredResult(item);280 drawerItems.appendChild(el);281 });282 }283 } catch (e) {284 drawerItems.innerHTML = '<div style="color: #ef4444; text-align: center; margin-top: 20px;">Failed to load history.</div>';285 }286 } else {287 drawerTitle.innerText = 'Prompts';288 PREDEFINED_PROMPTS.forEach(prompt => {289 const el = document.createElement('div');290 el.className = 'drawer-item';291 el.innerText = prompt;292 el.onclick = () => {293 chatInput.value = prompt;294 sideDrawer.classList.remove('open');295 handleSend();296 };297 drawerItems.appendChild(el);298 });299 }300 sideDrawer.classList.add('open');301 }302 303 historyBtn.onclick = () => openDrawer('history');304 promptsBtn.onclick = () => openDrawer('prompts');305 closeDrawer.onclick = () => sideDrawer.classList.remove('open');306 307 // --- AUTH OVERLAY CONTROLLERS ---308 const authModal = document.getElementById('auth-modal');309 const loginBtn = document.getElementById('login-btn');310 const signupBtn = document.getElementById('signup-btn');311 const closeAuthModal = document.getElementById('close-auth-modal');312 const authModalTitle = document.getElementById('auth-modal-title');313 314 const googleAuthBtn = document.getElementById('google-auth-btn');315 316 const otpStep1 = document.getElementById('otp-step-1');317 const authEmail = document.getElementById('auth-email');318 const authSendOtpBtn = document.getElementById('auth-send-otp-btn');319 320 const otpStep2 = document.getElementById('otp-step-2');321 const authOtpCode = document.getElementById('auth-otp-code');322 const authVerifyOtpBtn = document.getElementById('auth-verify-otp-btn');323 const authBackToStep1 = document.getElementById('auth-back-to-step1');324 325 const authMessage = document.getElementById('auth-message');326 const logoutBtn = document.getElementById('logout-btn');327 328 function resetAuthModal() {329 authMessage.innerText = '';330 authEmail.value = '';331 authOtpCode.value = '';332 otpStep1.style.display = 'flex';333 otpStep2.style.display = 'none';334 }335 336 function showAuthModal() {337 resetAuthModal();338 authModal.style.display = 'flex';339 if (!supabaseClient) {340 authMessage.style.color = '#eab308'; // Warning gold/yellow341 authMessage.innerText = 'Supabase server is offline (Error 521). Sandbox simulation mode active.';342 }343 }344 345 loginBtn.onclick = () => showAuthModal();346 signupBtn.onclick = () => showAuthModal();347 closeAuthModal.onclick = () => authModal.style.display = 'none';348 349 // Close auth modal when clicking outside content box350 authModal.onclick = (e) => {351 if (e.target === authModal) {352 authModal.style.display = 'none';353 }354 };355 356 // Go back to Step 1357 authBackToStep1.onclick = (e) => {358 e.preventDefault();359 authMessage.innerText = '';360 authOtpCode.value = '';361 otpStep1.style.display = 'flex';362 otpStep2.style.display = 'none';363 };364 365 // --- DISPOSABLE EMAIL CHECK ---366 function isDisposableEmail(email) {367 const disposableDomains = [368 'mailinator.com', 'tempmail.com', 'temp-mail.org', '10minutemail.com', 369 'yopmail.com', 'guerrillamail.com', 'dispostable.com', 'trashmail.com', 370 'getairmail.com', 'sharklasers.com', 'guerrillamailblock.com', 371 'guerrillamail.net', 'guerrillamail.org', 'guerrillamail.biz', 372 'tempmailo.com', 'generator.email', 'maildrop.cc', 'tempmail.dev',373 'tempmail.net', 'disposable.com', 'fakeinbox.com', 'disposablemail.com'374 ];375 const parts = email.split('@');376 if (parts.length < 2) return true;377 const domain = parts[1].toLowerCase().trim();378 return disposableDomains.includes(domain);379 }380 381 // --- GOOGLE OAUTH LOGIN TRIGGER ---382 googleAuthBtn.onclick = async () => {383 if (!supabaseClient) {384 authMessage.style.color = '#ef4444';385 authMessage.innerText = 'Authentication service is offline. Mock login is disabled.';386 return;387 }388 389 authMessage.style.color = '#10b981';390 authMessage.innerText = 'Redirecting to Google...';391 392 try {393 const { error } = await supabaseClient.auth.signInWithOAuth({394 provider: 'google',395 options: {396 redirectTo: window.location.origin397 }398 });399 if (error) throw error;400 } catch (err) {401 authMessage.style.color = '#ef4444';402 authMessage.innerText = err.message || 'OAuth redirection failed.';403 }404 };405 406 // --- PASSWORDLESS SEND EMAIL OTP TRIGGER ---407 authSendOtpBtn.onclick = async () => {408 const email = authEmail.value.trim();409 if (!email) {410 authMessage.style.color = '#ef4444';411 authMessage.innerText = 'Please enter a valid email address.';412 return;413 }414 415 if (isDisposableEmail(email)) {416 authMessage.style.color = '#ef4444';417 authMessage.innerText = 'Please use a valid personal, academic, or professional email address. Temporary email addresses are not supported.';418 return;419 }420 421 if (!supabaseClient) {422 authMessage.style.color = '#ef4444';423 authMessage.innerText = 'Authentication service is offline. Mock login is disabled.';424 return;425 }426 427 authSendOtpBtn.disabled = true;428 authMessage.style.color = '#10b981';429 authMessage.innerText = 'Sending verification code...';430 431 try {432 const { error } = await supabaseClient.auth.signInWithOtp({433 email: email,434 options: {435 shouldCreateUser: true // Automatically sign up new users436 }437 });438 if (error) throw error;439 440 authMessage.innerText = 'Verification code sent to your email!';441 setTimeout(() => {442 authMessage.innerText = '';443 otpStep1.style.display = 'none';444 otpStep2.style.display = 'flex';445 authOtpCode.focus();446 }, 1000);447 } catch (err) {448 authMessage.style.color = '#ef4444';449 authMessage.innerText = err.message || 'Failed to send OTP code.';450 } finally {451 authSendOtpBtn.disabled = false;452 }453 };454 455 // --- PASSWORDLESS VERIFY EMAIL OTP TRIGGER ---456 authVerifyOtpBtn.onclick = async () => {457 const email = authEmail.value.trim();458 const otpCode = authOtpCode.value.trim();459 if (!otpCode || otpCode.length !== 8) {460 authMessage.style.color = '#ef4444';461 authMessage.innerText = 'Please enter the 8-digit verification code.';462 return;463 }464 465 if (!supabaseClient) {466 authMessage.style.color = '#ef4444';467 authMessage.innerText = 'Authentication service is offline. Mock login is disabled.';468 return;469 }470 471 authVerifyOtpBtn.disabled = true;472 authMessage.style.color = '#10b981';473 authMessage.innerText = 'Verifying security code...';474 475 try {476 const { data, error } = await supabaseClient.auth.verifyOtp({477 email: email,478 token: otpCode,479 type: 'email'480 });481 if (error) throw error;482 483 authMessage.innerText = 'Authentication successful!';484 setTimeout(() => {485 authModal.style.display = 'none';486 updateAuthState();487 }, 1000);488 } catch (err) {489 authMessage.style.color = '#ef4444';490 authMessage.innerText = err.message || 'Invalid or expired verification code.';491 } finally {492 authVerifyOtpBtn.disabled = false;493 }494 };495 496 logoutBtn.onclick = async () => {497 if (supabaseClient) {498 await supabaseClient.auth.signOut();499 } else {500 activeUser = null;501 activeToken = null;502 updateAuthState();503 }504 };505 506 async function handleSend() {507 if (!activeUser) {508 showAuthModal();509 return;510 }511 512 const message = chatInput.value.trim();513 if (!message) return;514 515 // Disable inputs while processing516 chatInput.disabled = true;517 sendBtn.disabled = true;518 document.querySelector('.input-wrapper').classList.add('disabled');519 520 // Hide feedback widget and reset its state521 const feedbackContainer = document.getElementById('response-feedback');522 const thumbsUpBtn = document.getElementById('thumbs-up-btn');523 const thumbsDownBtn = document.getElementById('thumbs-down-btn');524 if (feedbackContainer) feedbackContainer.style.display = 'none';525 if (thumbsUpBtn) thumbsUpBtn.classList.remove('active-up');526 if (thumbsDownBtn) thumbsDownBtn.classList.remove('active-down');527 528 saveToHistory(message);529 chatInput.value = '';530 charCount.innerText = '0/3,000';531 532 // --- CREDIT DEDUCTION LOGIC ---533 if (activeUser && supabaseClient) {534 let currentCredits = activeUser.user_metadata?.credits_remaining !== undefined ? activeUser.user_metadata.credits_remaining : 500;535 if (currentCredits > 500) currentCredits = 500; // Instantly scale legacy users down to new 500 limit536 537 if (currentCredits < 10) {538 responsePanel.style.display = 'flex';539 responseText.innerHTML = '<span style="color: #ef4444; padding: 20px; display: block; text-align: center;">Insufficient credits. You need at least 10 credits to perform a clinical search. Please upgrade your account.</span>';540 chatInput.disabled = false;541 sendBtn.disabled = false;542 document.querySelector('.input-wrapper').classList.remove('disabled');543 return;544 }545 546 let newCredits = Math.max(0, currentCredits - 10);547 548 // Optimistic UI update549 const creditText = document.querySelector('.credit-info span');550 if (creditText) creditText.innerText = `${newCredits}/500 credits`;551 552 // Background sync with Supabase User Metadata553 supabaseClient.auth.updateUser({554 data: { credits_remaining: newCredits }555 }).then(({ data, error }) => {556 if (!error && data.user) {557 activeUser = data.user;558 }559 });560 }561 562 responsePanel.style.display = 'flex';563 564 let modeDisplay = "Unified Library";565 if (activeMode === "mbbs") modeDisplay = "Dx(MBBS) Knowledge Base";566 if (activeMode === "pharmacy") modeDisplay = "Rx(Pharmacy) Knowledge Base";567 568 const queryHTML = `<div style="font-size: 16px; font-weight: 500; color: white; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.1);"><span style="opacity: 0.5; font-size: 12px; display: block; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 1px;">You Asked:</span>${message}</div>`;569 const badgeHTML = `<div style="font-size: 11px; color: var(--primary); font-weight: 600; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 12px; border-left: 2px solid var(--primary); padding-left: 8px;">Response from ${modeDisplay}</div>`;570 571 responseText.innerHTML = queryHTML + badgeHTML + `572 <div class="skeleton skeleton-title"></div>573 <div class="skeleton skeleton-text"></div>574 <div class="skeleton skeleton-text"></div>575 <div class="skeleton skeleton-short"></div>576 `;577 sourceList.innerHTML = '';578 579 try {580 const headers = { 'Content-Type': 'application/json' };581 if (activeToken) {582 headers['Authorization'] = `Bearer ${activeToken}`;583 }584 585 const response = await fetch('/chat', {586 method: 'POST',587 headers: headers,588 body: JSON.stringify({ message: message, mode: activeMode })589 });590 591 if (!response.ok) {592 if (response.status === 401) {593 responseText.innerHTML = '<span style="color: #ef4444;">Session expired or unauthorized. Please log out and log back in.</span>';594 return;595 }596 throw new Error(`HTTP Error: ${response.status}`);597 }598 599 const reader = response.body.getReader();600 const decoder = new TextDecoder();601 let fullAnswer = "";602 let sources = [];603 let sourceGalleryHTML = "";604 605 while (true) {606 const { done, value } = await reader.read();607 if (done) break;608 609 const chunk = decoder.decode(value, { stream: true });610 const lines = chunk.split('\n');611 612 for (const line of lines) {613 if (!line.trim()) continue;614 try {615 const json = JSON.parse(line);616 if (json.type === 'sources') {617 sources = json.data;618 sources.forEach(src => {619 // Keep the static tags for history, but disable the image modal620 const tag = document.createElement('span');621 tag.className = 'source-tag';622 tag.innerText = `${src.book_name} (Pg ${src.page_number})`;623 tag.style.cursor = 'default';624 sourceList.appendChild(tag);625 });626 627 // Manually update the view since sources arrive last in the stream now628 const finalHtml = DOMPurify.sanitize(applyMedicalTooltips(marked.parse(fullAnswer)), { ADD_ATTR: ['data-tooltip'] });629 responseText.innerHTML = queryHTML + badgeHTML + finalHtml;630 631 // Add PDF Export Button632 const pdfBtn = document.createElement('button');633 pdfBtn.className = 'export-pdf-btn';634 pdfBtn.innerHTML = `635 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>636 Export Clinical Report637 `;638 pdfBtn.onclick = () => {639 pdfBtn.innerHTML = "Generating PDF...";640 const opt = {641 margin: 1,642 filename: 'Clinical_Report.pdf',643 image: { type: 'jpeg', quality: 0.98 },644 html2canvas: { scale: 2, useCORS: true },645 jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }646 };647 const elementToExport = responseText.cloneNode(true);648 elementToExport.removeChild(elementToExport.lastChild); // Remove the button from the PDF649 elementToExport.style.color = '#000'; // Make text black for PDF650 elementToExport.style.background = '#fff';651 elementToExport.style.padding = '20px';652 html2pdf().set(opt).from(elementToExport).save().then(() => {653 pdfBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> Export Clinical Report`;654 });655 };656 responseText.appendChild(pdfBtn);657 658 window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });659 660 } else if (json.type === 'start_answer') {661 responseText.innerHTML = queryHTML + badgeHTML;662 fullAnswer = ""; // Reset buffer663 } else if (json.type === 'content') {664 fullAnswer += json.data;665 const parsedHtml = DOMPurify.sanitize(applyMedicalTooltips(marked.parse(fullAnswer)), { ADD_ATTR: ['data-tooltip'] });666 responseText.innerHTML = queryHTML + badgeHTML + parsedHtml;667 window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });668 }669 } catch (e) { }670 }671 }672 // Save complete result to history673 saveToHistory(message, fullAnswer, sources);674 675 // Revert to user's default mode if they did a temporary override676 if (activeUser?.user_metadata?.defaultMode) {677 activeMode = activeUser.user_metadata.defaultMode;678 updateModeButtons(activeMode);679 }680 681 // Show feedback widget when streaming completes successfully682 if (feedbackContainer) feedbackContainer.style.display = 'flex';683 } catch (error) {684 responseText.innerHTML = '<span style="color: #ef4444;">Connection error.</span>';685 } finally {686 // Re-enable inputs687 chatInput.disabled = false;688 sendBtn.disabled = false;689 document.querySelector('.input-wrapper').classList.remove('disabled');690 chatInput.focus();691 }692 }693 694 const resetModal = () => { 695 pageModal.style.display = 'none'; 696 modalImg.dataset.scale = 1;697 modalImg.style.transform = 'scale(1)';698 };699 closeModal.onclick = resetModal;700 window.onclick = (event) => { if (event.target == pageModal) resetModal(); };701 702 modalImg.addEventListener('wheel', (e) => {703 e.preventDefault();704 const delta = e.deltaY > 0 ? 0.9 : 1.1;705 const newScale = (modalImg.dataset.scale || 1) * delta;706 if (newScale >= 1 && newScale <= 5) {707 modalImg.dataset.scale = newScale;708 modalImg.style.transform = `scale(${newScale})`;709 }710 });711 712 let isDragging = false;713 let startX, startY, scrollLeft, scrollTop;714 715 const modalBody = document.querySelector('.modal-body');716 modalBody.addEventListener('mousedown', (e) => {717 isDragging = true;718 startX = e.pageX - modalBody.offsetLeft;719 startY = e.pageY - modalBody.offsetTop;720 scrollLeft = modalBody.scrollLeft;721 scrollTop = modalBody.scrollTop;722 });723 724 modalBody.addEventListener('mouseleave', () => { isDragging = false; });725 modalBody.addEventListener('mouseup', () => { isDragging = false; });726 727 modalBody.addEventListener('mousemove', (e) => {728 if (!isDragging) return;729 e.preventDefault();730 const x = e.pageX - modalBody.offsetLeft;731 const y = e.pageY - modalBody.offsetTop;732 const walkX = (x - startX) * 2;733 const walkY = (y - startY) * 2;734 modalBody.scrollLeft = scrollLeft - walkX;735 modalBody.scrollTop = scrollTop - walkY;736 });737 738 // Feedback loop logic739 const thumbsUpBtn = document.getElementById('thumbs-up-btn');740 const thumbsDownBtn = document.getElementById('thumbs-down-btn');741 const feedbackLabel = document.querySelector('.feedback-label');742 743 if (thumbsUpBtn && thumbsDownBtn) {744 thumbsUpBtn.addEventListener('click', () => {745 thumbsUpBtn.classList.toggle('active-up');746 thumbsDownBtn.classList.remove('active-down');747 if (thumbsUpBtn.classList.contains('active-up')) {748 feedbackLabel.innerText = "Thank you!";749 setTimeout(() => { feedbackLabel.innerText = "Was this helpful?"; }, 3000);750 }751 });752 753 thumbsDownBtn.addEventListener('click', () => {754 thumbsDownBtn.classList.toggle('active-down');755 thumbsUpBtn.classList.remove('active-up');756 if (thumbsDownBtn.classList.contains('active-down')) {757 feedbackLabel.innerText = "Thank you!";758 setTimeout(() => { feedbackLabel.innerText = "Was this helpful?"; }, 3000);759 }760 });761 }762 763 const medicalDictionary = {764 "myocardial infarction": "A heart attack; tissue death of the heart muscle due to lack of blood supply.",765 "hypertension": "High blood pressure, which can lead to heart disease and stroke.",766 "tachycardia": "A rapid heart rate, usually defined as greater than 100 beats per minute.",767 "ischemia": "An inadequate blood supply to an organ or part of the body, especially the heart muscles.",768 "hyperlipidemia": "High levels of fat particles (lipids) in the blood.",769 "arrhythmia": "An irregular heartbeat or abnormal heart rhythm."770 };771 772 function applyMedicalTooltips(html) {773 let modifiedHtml = html;774 for (const [term, definition] of Object.entries(medicalDictionary)) {775 const regex = new RegExp(`\\b(${term})\\b`, 'gi');776 modifiedHtml = modifiedHtml.replace(regex, `<span class="medical-tooltip" data-tooltip="${definition}">$1</span>`);777 }778 return modifiedHtml;779 }780 781 sendBtn.addEventListener('click', handleSend);782 chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleSend(); });783 