CoolFace
Apppublic

TrinetraLabs/Placebo_AI

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
script_5.js783 linesDownload Raw Back to root
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