smart-models/Placebo_AI
0
1<!DOCTYPE html>2<html lang="en">3 4<head>5 <meta charset="UTF-8">6 <meta name="viewport" content="width=device-width, initial-scale=1.0">7 <title>Placebo AI | Medical Intelligence</title>8 <link rel="stylesheet" href="/static/style.css">9 <!-- Favicons -->10 <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">11 <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">12 <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">13 <link rel="shortcut icon" href="/static/favicon.ico">14 <link rel="manifest" href="/static/site.webmanifest">15 <style>16 #loader-container {17 position: fixed;18 top: 0;19 left: 0;20 width: 100vw;21 height: 100vh;22 background: #0a0a0a;23 z-index: 9999;24 display: flex;25 justify-content: center;26 align-items: center;27 transition: opacity 1s ease;28 }29 #main-content {30 opacity: 0;31 pointer-events: none;32 transition: opacity 1.5s ease;33 }34 35 /* Skeleton Loader Styles */36 .skeleton {37 background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.03) 75%);38 background-size: 200% 100%;39 animation: shimmer 1.5s infinite;40 border-radius: 8px;41 margin-bottom: 12px;42 }43 44 .skeleton-text { height: 18px; width: 100%; }45 .skeleton-title { height: 28px; width: 60%; margin-bottom: 20px; }46 .skeleton-short { height: 18px; width: 40%; }47 48 @keyframes shimmer {49 0% { background-position: 200% 0; }50 100% { background-position: -200% 0; }51 }52 53 /* Side Drawer Styles */54 #side-drawer {55 position: fixed;56 top: 0;57 right: -400px;58 width: 350px;59 height: 100%;60 background: rgba(15, 23, 42, 0.95);61 backdrop-filter: blur(40px);62 border-left: 1px solid rgba(255, 255, 255, 0.1);63 z-index: 2000;64 transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1);65 padding: 30px;66 box-shadow: -10px 0 30px rgba(0,0,0,0.5);67 display: flex;68 flex-direction: column;69 }70 71 #side-drawer.open { right: 0; }72 73 .drawer-header {74 display: flex;75 justify-content: space-between;76 align-items: center;77 margin-bottom: 30px;78 border-bottom: 1px solid rgba(255,255,255,0.1);79 padding-bottom: 15px;80 }81 82 .drawer-header h2 {83 font-family: 'Montserrat', sans-serif;84 font-size: 1.2rem;85 color: #10b981;86 letter-spacing: 2px;87 text-transform: uppercase;88 }89 90 #close-drawer {91 background: none;92 border: none;93 color: white;94 font-size: 2rem;95 cursor: pointer;96 opacity: 0.5;97 transition: opacity 0.2s;98 }99 100 #close-drawer:hover { opacity: 1; }101 102 .drawer-item {103 padding: 15px;104 background: rgba(255,255,255,0.05);105 border-radius: 12px;106 margin-bottom: 12px;107 cursor: pointer;108 transition: all 0.2s;109 border: 1px solid transparent;110 font-size: 0.9rem;111 line-height: 1.4;112 }113 114 .drawer-item:hover {115 background: rgba(16, 185, 129, 0.1);116 border-color: rgba(16, 185, 129, 0.3);117 transform: translateX(-5px);118 }119 120 .drawer-item .time {121 font-size: 0.7rem;122 opacity: 0.5;123 display: block;124 margin-top: 5px;125 }126 127 .drawer-content {128 display: flex;129 flex-direction: column;130 height: 100%;131 overflow: hidden;132 }133 134 #drawer-items {135 flex: 1;136 overflow-y: auto;137 padding-right: 10px;138 }139 140 /* Custom Scrollbar for drawer */141 #drawer-items::-webkit-scrollbar {142 width: 6px;143 }144 #drawer-items::-webkit-scrollbar-track {145 background: rgba(255,255,255,0.02);146 }147 #drawer-items::-webkit-scrollbar-thumb {148 background: rgba(16, 185, 129, 0.3);149 border-radius: 3px;150 }151 </style>152</head>153 154<body>155 <!-- Spider Cursor Loader -->156 <div id="loader-container">157 <div style="position: absolute; text-align: center; pointer-events: none; z-index: 10;">158 <img src="/static/logo.png" width="80" height="80" style="filter: brightness(0) invert(1); margin-bottom: 20px; opacity: 0.8;">159 <div style="color: #10b981; font-family: 'Montserrat', sans-serif; letter-spacing: 4px; font-weight: 300; font-size: 14px;">INITIALIZING MEDICAL INTELLIGENCE...</div>160 </div>161 </div>162 163 <div id="main-content">164 <!-- Video Background Layer -->165 <div id="video-container">166 <video id="hero-video" autoplay loop muted playsinline>167 <source src="/static/bg_video.mp4" type="video/mp4">168 </video>169 </div>170 171 <!-- UI Content Layer -->172 <div class="content-layer">173 <nav>174 <div class="logo-area" style="display: flex; align-items: center; gap: 12px;">175 <img src="/static/logo.png" width="32" height="32" alt="Logo" style="filter: brightness(0) invert(1);">176 <a href="#" class="logo">Placebo AI</a>177 </div>178 <div class="menu-items">179 <a href="/" class="menu-item">Home</a>180 <a href="/architecture" class="menu-item">Architecture</a>181 <a href="/capabilities" class="menu-item">Capabilities</a>182 <a href="/docs" class="menu-item">Docs</a>183 <a href="/about" class="menu-item">About</a>184 </div>185 <div class="nav-buttons" id="nav-auth-buttons">186 <button class="btn-signup" id="signup-btn">Sign Up</button>187 <button class="btn-login" id="login-btn">Log In</button>188 </div>189 <div class="nav-buttons" id="nav-user-buttons" style="display: none; align-items: center; gap: 16px;">190 <span id="user-display-email" style="font-size: 13px; font-weight: 500; opacity: 0.85; color: var(--primary);">doctor@placebo.ai</span>191 <button class="btn-login" id="logout-btn" style="width: auto; padding: 0 16px;">Log Out</button>192 </div>193 </nav>194 195 <main class="hero-content">196 <div class="badge-container">197 <div class="badge-dark">198 <svg class="icon" viewBox="0 0 24 24" fill="currentColor">199 <path200 d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />201 </svg>202 <span>Placebo AI</span>203 </div>204 <span class="badge-text">AI Medical Intelligence</span>205 </div>206 207 <h1>Next-Gen Medical AI</h1>208 <p class="subtitle">Transforms complex medical textbooks into accurate, citation-backed insights</p>209 210 <!-- Search / Chat Input Box -->211 <div class="search-container">212 <div class="search-top">213 <div class="credit-info">214 <span>500/500 credits</span>215 <button class="btn-upgrade">Upgrade</button>216 </div>217 <div class="ai-powered">218 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">219 <path220 d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />221 <path d="M5 3v4" />222 <path d="M19 17v4" />223 <path d="M3 5h4" />224 <path d="M17 19h4" />225 </svg>226 <span>Powered by Trinetra Labs</span>227 </div>228 </div>229 230 <!-- Model Selector Toggle -->231 <div class="model-selector">232 <button class="model-btn active" data-mode="unified">233 <span class="indicator-dot"></span>234 Unified Library235 </button>236 <button class="model-btn" data-mode="mbbs">237 <span class="indicator-dot"></span>238 Dx(MBBS)239 </button>240 <button class="model-btn" data-mode="pharmacy">241 <span class="indicator-dot"></span>242 Rx(Pharmacy)243 </button>244 </div>245 246 <div class="input-wrapper">247 <input type="text" id="chat-input" placeholder="Type question..." autocomplete="off">248 <button class="btn-submit" id="send-btn" aria-label="Send clinical query">249 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">250 <path d="m5 12 7-7 7 7" />251 <path d="M12 19V5" />252 </svg>253 </button>254 </div>255 256 <div class="search-bottom">257 <div class="action-buttons">258 <button class="action-btn" id="history-btn">259 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">260 <circle cx="12" cy="12" r="10" />261 <path d="M12 6v6l4 2" />262 </svg>263 History264 </button>265 <button class="action-btn" id="clear-btn">266 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">267 <path d="M3 6h18" />268 <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />269 <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />270 </svg>271 Clear272 </button>273 <button class="action-btn" id="prompts-btn">274 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">275 <circle cx="11" cy="11" r="8" />276 <path d="m21 21-4.3-4.3" />277 </svg>278 Prompts279 </button>280 </div>281 <span class="char-counter" id="char-count">0/3,000</span>282 </div>283 </div>284 285 <div class="app-disclaimer" style="text-align: center; font-size: 11px; color: rgba(255, 255, 255, 0.5); margin-top: 16px; font-family: 'Montserrat', sans-serif; max-width: 800px; margin-left: auto; margin-right: auto; line-height: 1.4;">286 Disclaimer: All books used in this application belong to their respective authors and publishers. The author, publisher, and copyright owner reserve all rights. No ownership of book content is claimed by this application.287 </div>288 289 <!-- Response Panel -->290 <div id="response-panel">291 <div class="response-content" id="response-text"></div>292 <div class="source-container" id="source-list"></div>293 294 <!-- Feedback Loop -->295 <div class="response-feedback" id="response-feedback" style="display: none;">296 <span class="feedback-label">Was this helpful?</span>297 <button class="feedback-btn" id="thumbs-up-btn" aria-label="Thumbs up: Helpful answer">298 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">299 <path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />300 </svg>301 </button>302 <button class="feedback-btn" id="thumbs-down-btn" aria-label="Thumbs down: Unhelpful answer">303 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">304 <path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm12-3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3" />305 </svg>306 </button>307 </div>308 309 <!-- Disclaimer -->310 <div class="response-disclaimer">311 <svg class="icon disclaimer-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">312 <circle cx="12" cy="12" r="10" />313 <line x1="12" y1="8" x2="12" y2="12" />314 <line x1="12" y1="16" x2="12.01" y2="16" />315 </svg>316 <span>Placebo AI is a clinical retrieval system. Synthesized knowledge should be clinically verified.</span>317 </div>318 </div>319 </main>320 </div>321 </div>322 323 <!-- Page Viewer Modal -->324 <div id="page-modal">325 <div class="modal-container">326 <div class="modal-header">327 <div class="modal-title" id="modal-title">Page Preview</div>328 <button class="close-modal" id="close-modal" aria-label="Close page preview modal">×</button>329 </div>330 <div class="modal-body">331 <img id="page-preview-img" src="" alt="Page Preview">332 </div>333 </div>334 </div>335 336 <!-- Authentication Modal Overlay -->337 <div id="auth-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); backdrop-filter: blur(20px); z-index: 3000; display: none; align-items: center; justify-content: center; padding: 20px;">338 <div class="modal-container" style="max-width: 400px; border: 1px solid rgba(16, 185, 129, 0.2); box-shadow: 0 0 30px rgba(16, 185, 129, 0.1);">339 <div class="modal-header">340 <div class="modal-title" id="auth-modal-title" style="font-family: 'Surgena', serif; font-size: 24px; color: var(--white);">Authentication</div>341 <button class="close-modal" id="close-auth-modal" aria-label="Close authentication modal" style="font-size: 28px;">×</button>342 </div>343 <div class="modal-body" style="background: transparent; display: flex; flex-direction: column; gap: 16px; padding: 24px;">344 <!-- Google OAuth Sign In Button -->345 <button id="google-auth-btn" style="width: 100%; height: 48px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center; gap: 12px; color: var(--white); font-family: 'Montserrat', sans-serif; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s; box-shadow: 0 4px 12px rgba(0,0,0,0.1);" onmouseover="this.style.background='rgba(255,255,255,0.12)'; this.style.borderColor='rgba(16, 185, 129, 0.5)'" onmouseout="this.style.background='rgba(255,255,255,0.06)'; this.style.borderColor='rgba(255,255,255,0.15)'">346 <svg width="20" height="20" viewBox="0 0 24 24" style="flex-shrink: 0;">347 <path fill="#EA4335" d="M12 5.04c1.66 0 3.2.57 4.38 1.69l3.27-3.27C17.67 1.48 14.97 1 12 1 7.35 1 3.4 3.65 1.56 7.5l3.86 3C6.34 7.55 8.94 5.04 12 5.04z"/>348 <path fill="#4285F4" d="M23.49 12.27c0-.81-.07-1.59-.2-2.34H12v4.43h6.44c-.28 1.47-1.11 2.71-2.36 3.55l3.65 2.84c2.13-1.97 3.36-4.87 3.36-8.48z"/>349 <path fill="#FBBC05" d="M5.42 14.5c-.25-.74-.39-1.53-.39-2.35s.14-1.61.39-2.35L1.56 6.8C.56 8.79 0 11.01 0 12.35s.56 3.56 1.56 5.55l3.86-3z"/>350 <path fill="#34A853" d="M12 23c3.24 0 5.97-1.07 7.96-2.91l-3.65-2.84c-1.01.68-2.31 1.09-3.96 1.09-3.06 0-5.66-2.51-6.58-5.46l-3.86 3C3.4 20.35 7.35 23 12 23z"/>351 </svg>352 Continue with Google353 </button>354 355 <div style="display: flex; align-items: center; justify-content: center; width: 100%; margin: 4px 0;">356 <div style="flex: 1; height: 1px; background: rgba(255,255,255,0.08);"></div>357 <span style="font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; opacity: 0.5; color: var(--white); padding: 0 12px;">or email verification</span>358 <div style="flex: 1; height: 1px; background: rgba(255,255,255,0.08);"></div>359 </div>360 361 <!-- OTP Step 1: Send Verification Code -->362 <div id="otp-step-1" style="display: flex; flex-direction: column; gap: 16px; width: 100%;">363 <div style="display: flex; flex-direction: column; gap: 6px; width: 100%;">364 <label style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; opacity: 0.8; color: var(--white); text-align: left;">Email Address</label>365 <input type="email" id="auth-email" placeholder="doctor@placebo.ai" style="width: 100%; padding: 12px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; font-family: 'Montserrat', sans-serif; outline: none; transition: border-color 0.3s;" onfocus="this.style.borderColor='#10b981'" onblur="this.style.borderColor='rgba(255,255,255,0.1)'">366 </div>367 <button class="btn-signup" id="auth-send-otp-btn" style="width: 100%; height: 46px; font-size: 14px; margin-top: 4px;">Send Verification Code</button>368 </div>369 370 <!-- OTP Step 2: Verify 8-digit Code (Initially Hidden) -->371 <div id="otp-step-2" style="display: none; flex-direction: column; gap: 16px; width: 100%;">372 <div style="display: flex; flex-direction: column; gap: 6px; width: 100%;">373 <label style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; opacity: 0.8; color: var(--white); text-align: left;">8-Digit OTP Code</label>374 <input type="text" id="auth-otp-code" placeholder="••••••••" maxlength="8" style="width: 100%; padding: 12px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; font-family: 'Montserrat', sans-serif; font-size: 20px; font-weight: 700; letter-spacing: 8px; text-align: center; outline: none; transition: border-color 0.3s;" onfocus="this.style.borderColor='#10b981'" onblur="this.style.borderColor='rgba(255,255,255,0.1)'">375 </div>376 <button class="btn-signup" id="auth-verify-otp-btn" style="width: 100%; height: 46px; font-size: 14px; margin-top: 4px;">Verify & Log In</button>377 <div style="text-align: center; font-size: 12px; opacity: 0.7;">378 <a href="#" id="auth-back-to-step1" style="color: var(--primary); text-decoration: none; font-weight: 600;">Go back to enter email</a>379 </div>380 </div>381 382 <!-- Unified Authentication Message Output -->383 <div id="auth-message" style="text-align: center; font-size: 13px; font-weight: 500; margin-top: 4px; min-height: 18px; color: var(--primary); width: 100%;"></div>384 </div>385 </div>386 </div>387 388 <!-- Profession Selection Modal Overlay -->389 <div id="profession-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); backdrop-filter: blur(20px); z-index: 4000; display: none; align-items: center; justify-content: center; padding: 20px;">390 <div class="modal-container" style="max-width: 400px; border: 1px solid rgba(16, 185, 129, 0.2); box-shadow: 0 0 30px rgba(16, 185, 129, 0.1);">391 <div class="modal-header">392 <div class="modal-title" style="font-family: 'Surgena', serif; font-size: 24px; color: var(--white);">Welcome!</div>393 </div>394 <div class="modal-body" style="background: transparent; display: flex; flex-direction: column; gap: 16px; padding: 24px; text-align: center;">395 <p style="color: var(--white); opacity: 0.8; font-family: 'Montserrat', sans-serif; font-size: 14px; margin-bottom: 8px;">What best describes you?</p>396 <button class="profession-btn" data-mode="mbbs" style="width: 100%; padding: 12px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; font-family: 'Montserrat', sans-serif; font-weight: 600; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.background='rgba(16, 185, 129, 0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.05)'">MBBS Student / Doctor</button>397 <button class="profession-btn" data-mode="pharmacy" style="width: 100%; padding: 12px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; font-family: 'Montserrat', sans-serif; font-weight: 600; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.background='rgba(16, 185, 129, 0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.05)'">Pharmacy Student / Pharmacist</button>398 <button class="profession-btn" data-mode="unified" style="width: 100%; padding: 12px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; font-family: 'Montserrat', sans-serif; font-weight: 600; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.background='rgba(16, 185, 129, 0.2)'" onmouseout="this.style.background='rgba(255,255,255,0.05)'">Unified (Both Domains)</button>399 </div>400 </div>401 </div>402 403 <!-- Side Drawer for History/Prompts -->404 <div id="side-drawer">405 <div class="drawer-content">406 <div class="drawer-header">407 <h2 id="drawer-title">History</h2>408 <button id="close-drawer">×</button>409 </div>410 <div id="drawer-items"></div>411 </div>412 </div>413 414 <script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>415 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>416 <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>417 <script src="/static/spider-loader.js"></script>418 <script>419 // --- SUPABASE CLIENT INITIALIZATION ---420 let supabaseClient = null;421 let activeUser = null;422 let activeToken = null;423 424 async function initializeSupabase() {425 try {426 const response = await fetch("/config");427 const config = await response.json();428 const SUPABASE_URL = config.supabase_url;429 const SUPABASE_ANON_KEY = config.supabase_anon_key;430 431 if (window.supabase && typeof window.supabase.createClient === 'function' && SUPABASE_URL && SUPABASE_URL !== "https://your-project-id.supabase.co") {432 // Wipe any old local storage sessions lingering from before this feature433 for (let key in localStorage) {434 if (key.startsWith('sb-') && key.endsWith('-auth-token')) {435 localStorage.removeItem(key);436 }437 }438 439 supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {440 auth: {441 persistSession: false // Forces logout on every page refresh442 }443 });444 console.log("Supabase Client initialized with ephemeral sessions. Testing connectivity...");445 446 // Connection ping test with 2-second timeout447 const pingPromise = supabaseClient.auth.getSession();448 const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Supabase connection timeout")), 2000));449 450 await Promise.race([pingPromise, timeoutPromise]);451 console.log("Supabase connectivity verified successfully.");452 453 supabaseClient.auth.onAuthStateChange((event, session) => {454 console.log("Auth State Changed Event:", event);455 updateAuthState();456 });457 } else {458 console.warn("Supabase configuration is using placeholder credentials or offline.");459 }460 } catch (e) {461 console.warn("Failed to verify Supabase connectivity or load config.", e);462 supabaseClient = null;463 }464 // Sync current authentication state on load465 await updateAuthState();466 }467 468 async function updateAuthState() {469 const authButtons = document.getElementById('nav-auth-buttons');470 const userButtons = document.getElementById('nav-user-buttons');471 const displayEmail = document.getElementById('user-display-email');472 473 if (supabaseClient) {474 try {475 const { data: { session } } = await supabaseClient.auth.getSession();476 if (session) {477 activeUser = session.user;478 activeToken = session.access_token;479 } else {480 activeUser = null;481 activeToken = null;482 }483 } catch (err) {484 console.error("Error fetching Supabase session:", err);485 }486 }487 488 if (activeUser) {489 authButtons.style.display = 'none';490 userButtons.style.display = 'flex';491 displayEmail.innerText = activeUser.email;492 493 let userDefaultMode = activeUser.user_metadata?.defaultMode;494 if (!userDefaultMode) {495 document.getElementById('profession-modal').style.display = 'flex';496 } else {497 activeMode = userDefaultMode;498 updateModeButtons(activeMode);499 }500 501 // Dynamically update user limits502 const creditText = document.querySelector('.credit-info span');503 if (creditText) {504 let userCredits = activeUser.user_metadata?.credits_remaining !== undefined ? activeUser.user_metadata.credits_remaining : 500;505 if (userCredits > 500) userCredits = 500; // Cap grandfathered users at 500506 creditText.innerText = `${userCredits}/500 credits`;507 }508 } else {509 authButtons.style.display = 'flex';510 userButtons.style.display = 'none';511 activeToken = null;512 const creditText = document.querySelector('.credit-info span');513 if (creditText) {514 creditText.innerText = "Log in to check credits";515 }516 }517 }518 519 // Initialize state check520 window.addEventListener('DOMContentLoaded', () => {521 initializeSupabase();522 });523 524 const chatForm = document.getElementById('chat-form');525 const video = document.getElementById('hero-video');526 let opacity = 1;527 528 function setOpacity(val) {529 opacity = val;530 video.style.opacity = opacity;531 }532 533 if (video.readyState >= 1) {534 video.play();535 setOpacity(1);536 } else {537 video.addEventListener('loadedmetadata', () => {538 video.play();539 setOpacity(1);540 });541 }542 543 setTimeout(() => setOpacity(1), 2000);544 545 const chatInput = document.getElementById('chat-input');546 const sendBtn = document.getElementById('send-btn');547 const charCount = document.getElementById('char-count');548 const responsePanel = document.getElementById('response-panel');549 const responseText = document.getElementById('response-text');550 const sourceList = document.getElementById('source-list');551 const pageModal = document.getElementById('page-modal');552 const modalImg = document.getElementById('page-preview-img');553 const modalTitle = document.getElementById('modal-title');554 const closeModal = document.getElementById('close-modal');555 const clearBtn = document.getElementById('clear-btn');556 557 let activeMode = 'unified';558 559 function updateModeButtons(mode) {560 const buttons = document.querySelectorAll('.model-btn');561 buttons.forEach(b => {562 b.classList.remove('active');563 if (b.getAttribute('data-mode') === mode || (mode === 'unified' && b.getAttribute('data-mode') === 'all')) {564 b.classList.add('active');565 }566 });567 }568 569 document.querySelectorAll('.profession-btn').forEach(btn => {570 btn.onclick = async () => {571 const mode = btn.getAttribute('data-mode');572 if (supabaseClient && activeUser) {573 btn.innerText = "Saving...";574 const { data, error } = await supabaseClient.auth.updateUser({575 data: { defaultMode: mode }576 });577 if (!error) {578 activeUser = data.user;579 activeMode = mode;580 updateModeButtons(mode);581 document.getElementById('profession-modal').style.display = 'none';582 583 // Reset button text in case they log out and it re-shows584 btn.innerText = mode === "mbbs" ? "MBBS Student / Doctor" : 585 (mode === "pharmacy" ? "Pharmacy Student / Pharmacist" : "Unified (Both Domains)");586 }587 }588 };589 });590 591 const modelButtons = document.querySelectorAll('.model-btn');592 modelButtons.forEach(btn => {593 btn.addEventListener('click', () => {594 modelButtons.forEach(b => b.classList.remove('active'));595 btn.classList.add('active');596 activeMode = btn.getAttribute('data-mode');597 });598 });599 600 if (clearBtn) {601 clearBtn.addEventListener('click', (e) => {602 e.preventDefault();603 responseText.innerHTML = '';604 sourceList.innerHTML = '';605 responsePanel.style.display = 'none';606 chatInput.value = '';607 charCount.innerText = '0/3,000';608 });609 }610 611 chatInput.addEventListener('input', () => {612 charCount.innerText = `${chatInput.value.length}/3,000`;613 });614 615 const historyBtn = document.getElementById('history-btn');616 const promptsBtn = document.getElementById('prompts-btn');617 const sideDrawer = document.getElementById('side-drawer');618 const closeDrawer = document.getElementById('close-drawer');619 const drawerTitle = document.getElementById('drawer-title');620 const drawerItems = document.getElementById('drawer-items');621 622 const PREDEFINED_PROMPTS = [623 "Explain the clinical presentation, diagnosis, and surgical management of acute appendicitis.",624 "What are the histological features of a myocardial infarction across different time frames?",625 "Explain the mechanism of action, pharmacokinetics, and adverse effects of Aspirin.",626 "What is the principle behind Non-aqueous Titrations and which indicators are used?",627 "Explain Type II Diabetes Mellitus, including its pathophysiology and the pharmacological management using Metformin.",628 "Describe the physiological pathway of the Renin-Angiotensin-Aldosterone System (RAAS)."629 ];630 631 function saveToHistory(query, answer = '', sources = []) {632 const historyKey = activeUser ? `chat_history_${activeUser.id}` : 'chat_history_guest';633 let history = JSON.parse(localStorage.getItem(historyKey) || '[]');634 // Find if query exists, remove it (to bring to top)635 history = history.filter(h => h.query !== query);636 history.unshift({ 637 query, 638 answer, 639 sources,640 time: new Date().toLocaleString() 641 });642 history = history.slice(0, 20); // Keep last 20643 localStorage.setItem(historyKey, JSON.stringify(history));644 }645 646 function displayStoredResult(item) {647 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>`;648 649 let galleryHTML = '<div class="source-images-gallery" style="display: flex; gap: 10px; margin-top: 15px; overflow-x: auto; padding-bottom: 10px;">';650 sourceList.innerHTML = '';651 item.sources.forEach(src => {652 const tag = document.createElement('span');653 tag.className = 'source-tag';654 tag.innerText = `${src.book_name} (Pg ${src.page_number})`;655 tag.onclick = () => {656 modalTitle.innerText = `${src.book_name} - Page ${src.page_number}`;657 modalImg.src = `/page_image?path=${encodeURIComponent(src.image_path)}`;658 pageModal.style.display = 'flex';659 };660 sourceList.appendChild(tag);661 662 galleryHTML += `663 <div style="flex: 0 0 auto; width: 250px; border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 8px; overflow: hidden; background: rgba(0,0,0,0.2); cursor: pointer;" onclick="document.getElementById('modal-title').innerText='${src.book_name} - Page ${src.page_number}'; document.getElementById('modal-img').src='/page_image?path=${encodeURIComponent(src.image_path)}'; document.getElementById('page-modal').style.display='flex';">664 <div style="font-size: 10px; color: var(--primary); padding: 5px 8px; border-bottom: 1px solid rgba(16, 185, 129, 0.2); font-weight: 600; text-transform: uppercase;">Exact Page Reference</div>665 <img src="/page_image?path=${encodeURIComponent(src.image_path)}" style="width: 100%; height: auto; object-fit: contain; display: block;" alt="Reference Page">666 </div>667 `;668 });669 galleryHTML += '</div>';670 671 responseText.innerHTML = queryHTML + DOMPurify.sanitize(marked.parse(item.answer)) + (item.sources.length > 0 ? galleryHTML : "");672 673 responsePanel.style.display = 'flex';674 chatInput.value = item.query;675 sideDrawer.classList.remove('open');676 window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });677 }678 679 function openDrawer(type) {680 drawerItems.innerHTML = '';681 if (type === 'history') {682 drawerTitle.innerText = 'History';683 const historyKey = activeUser ? `chat_history_${activeUser.id}` : 'chat_history_guest';684 const history = JSON.parse(localStorage.getItem(historyKey) || '[]');685 if (history.length === 0) {686 drawerItems.innerHTML = '<div style="opacity: 0.5; text-align: center; margin-top: 20px;">No history yet.</div>';687 }688 history.forEach(item => {689 const el = document.createElement('div');690 el.className = 'drawer-item';691 el.innerHTML = `${item.query} <span class="time">${item.time}</span>`;692 el.onclick = () => displayStoredResult(item);693 drawerItems.appendChild(el);694 });695 } else {696 drawerTitle.innerText = 'Prompts';697 PREDEFINED_PROMPTS.forEach(prompt => {698 const el = document.createElement('div');699 el.className = 'drawer-item';700 el.innerText = prompt;701 el.onclick = () => {702 chatInput.value = prompt;703 sideDrawer.classList.remove('open');704 handleSend();705 };706 drawerItems.appendChild(el);707 });708 }709 sideDrawer.classList.add('open');710 }711 712 historyBtn.onclick = () => openDrawer('history');713 promptsBtn.onclick = () => openDrawer('prompts');714 closeDrawer.onclick = () => sideDrawer.classList.remove('open');715 716 // --- AUTH OVERLAY CONTROLLERS ---717 const authModal = document.getElementById('auth-modal');718 const loginBtn = document.getElementById('login-btn');719 const signupBtn = document.getElementById('signup-btn');720 const closeAuthModal = document.getElementById('close-auth-modal');721 const authModalTitle = document.getElementById('auth-modal-title');722 723 const googleAuthBtn = document.getElementById('google-auth-btn');724 725 const otpStep1 = document.getElementById('otp-step-1');726 const authEmail = document.getElementById('auth-email');727 const authSendOtpBtn = document.getElementById('auth-send-otp-btn');728 729 const otpStep2 = document.getElementById('otp-step-2');730 const authOtpCode = document.getElementById('auth-otp-code');731 const authVerifyOtpBtn = document.getElementById('auth-verify-otp-btn');732 const authBackToStep1 = document.getElementById('auth-back-to-step1');733 734 const authMessage = document.getElementById('auth-message');735 const logoutBtn = document.getElementById('logout-btn');736 737 function resetAuthModal() {738 authMessage.innerText = '';739 authEmail.value = '';740 authOtpCode.value = '';741 otpStep1.style.display = 'flex';742 otpStep2.style.display = 'none';743 }744 745 function showAuthModal() {746 resetAuthModal();747 authModal.style.display = 'flex';748 if (!supabaseClient) {749 authMessage.style.color = '#eab308'; // Warning gold/yellow750 authMessage.innerText = 'Supabase server is offline (Error 521). Sandbox simulation mode active.';751 }752 }753 754 loginBtn.onclick = () => showAuthModal();755 signupBtn.onclick = () => showAuthModal();756 closeAuthModal.onclick = () => authModal.style.display = 'none';757 758 // Close auth modal when clicking outside content box759 authModal.onclick = (e) => {760 if (e.target === authModal) {761 authModal.style.display = 'none';762 }763 };764 765 // Go back to Step 1766 authBackToStep1.onclick = (e) => {767 e.preventDefault();768 authMessage.innerText = '';769 authOtpCode.value = '';770 otpStep1.style.display = 'flex';771 otpStep2.style.display = 'none';772 };773 774 // --- DISPOSABLE EMAIL CHECK ---775 function isDisposableEmail(email) {776 const disposableDomains = [777 'mailinator.com', 'tempmail.com', 'temp-mail.org', '10minutemail.com', 778 'yopmail.com', 'guerrillamail.com', 'dispostable.com', 'trashmail.com', 779 'getairmail.com', 'sharklasers.com', 'guerrillamailblock.com', 780 'guerrillamail.net', 'guerrillamail.org', 'guerrillamail.biz', 781 'tempmailo.com', 'generator.email', 'maildrop.cc', 'tempmail.dev',782 'tempmail.net', 'disposable.com', 'fakeinbox.com', 'disposablemail.com'783 ];784 const parts = email.split('@');785 if (parts.length < 2) return true;786 const domain = parts[1].toLowerCase().trim();787 return disposableDomains.includes(domain);788 }789 790 // --- GOOGLE OAUTH LOGIN TRIGGER ---791 googleAuthBtn.onclick = async () => {792 if (!supabaseClient) {793 authMessage.style.color = '#ef4444';794 authMessage.innerText = 'Authentication service is offline. Mock login is disabled.';795 return;796 }797 798 authMessage.style.color = '#10b981';799 authMessage.innerText = 'Redirecting to Google...';800 801 try {802 const { error } = await supabaseClient.auth.signInWithOAuth({803 provider: 'google',804 options: {805 redirectTo: window.location.origin806 }807 });808 if (error) throw error;809 } catch (err) {810 authMessage.style.color = '#ef4444';811 authMessage.innerText = err.message || 'OAuth redirection failed.';812 }813 };814 815 // --- PASSWORDLESS SEND EMAIL OTP TRIGGER ---816 authSendOtpBtn.onclick = async () => {817 const email = authEmail.value.trim();818 if (!email) {819 authMessage.style.color = '#ef4444';820 authMessage.innerText = 'Please enter a valid email address.';821 return;822 }823 824 if (isDisposableEmail(email)) {825 authMessage.style.color = '#ef4444';826 authMessage.innerText = 'Please use a valid personal, academic, or professional email address. Temporary email addresses are not supported.';827 return;828 }829 830 if (!supabaseClient) {831 authMessage.style.color = '#ef4444';832 authMessage.innerText = 'Authentication service is offline. Mock login is disabled.';833 return;834 }835 836 authSendOtpBtn.disabled = true;837 authMessage.style.color = '#10b981';838 authMessage.innerText = 'Sending verification code...';839 840 try {841 const { error } = await supabaseClient.auth.signInWithOtp({842 email: email,843 options: {844 shouldCreateUser: true // Automatically sign up new users845 }846 });847 if (error) throw error;848 849 authMessage.innerText = 'Verification code sent to your email!';850 setTimeout(() => {851 authMessage.innerText = '';852 otpStep1.style.display = 'none';853 otpStep2.style.display = 'flex';854 authOtpCode.focus();855 }, 1000);856 } catch (err) {857 authMessage.style.color = '#ef4444';858 authMessage.innerText = err.message || 'Failed to send OTP code.';859 } finally {860 authSendOtpBtn.disabled = false;861 }862 };863 864 // --- PASSWORDLESS VERIFY EMAIL OTP TRIGGER ---865 authVerifyOtpBtn.onclick = async () => {866 const email = authEmail.value.trim();867 const otpCode = authOtpCode.value.trim();868 if (!otpCode || otpCode.length !== 8) {869 authMessage.style.color = '#ef4444';870 authMessage.innerText = 'Please enter the 8-digit verification code.';871 return;872 }873 874 if (!supabaseClient) {875 authMessage.style.color = '#ef4444';876 authMessage.innerText = 'Authentication service is offline. Mock login is disabled.';877 return;878 }879 880 authVerifyOtpBtn.disabled = true;881 authMessage.style.color = '#10b981';882 authMessage.innerText = 'Verifying security code...';883 884 try {885 const { data, error } = await supabaseClient.auth.verifyOtp({886 email: email,887 token: otpCode,888 type: 'email'889 });890 if (error) throw error;891 892 authMessage.innerText = 'Authentication successful!';893 setTimeout(() => {894 authModal.style.display = 'none';895 updateAuthState();896 }, 1000);897 } catch (err) {898 authMessage.style.color = '#ef4444';899 authMessage.innerText = err.message || 'Invalid or expired verification code.';900 } finally {901 authVerifyOtpBtn.disabled = false;902 }903 };904 905 logoutBtn.onclick = async () => {906 if (supabaseClient) {907 await supabaseClient.auth.signOut();908 } else {909 activeUser = null;910 activeToken = null;911 updateAuthState();912 }913 };914 915 async function handleSend() {916 if (!activeUser) {917 showAuthModal();918 return;919 }920 921 const message = chatInput.value.trim();922 if (!message) return;923 924 // Disable inputs while processing925 chatInput.disabled = true;926 sendBtn.disabled = true;927 document.querySelector('.input-wrapper').classList.add('disabled');928 929 // Hide feedback widget and reset its state930 const feedbackContainer = document.getElementById('response-feedback');931 const thumbsUpBtn = document.getElementById('thumbs-up-btn');932 const thumbsDownBtn = document.getElementById('thumbs-down-btn');933 if (feedbackContainer) feedbackContainer.style.display = 'none';934 if (thumbsUpBtn) thumbsUpBtn.classList.remove('active-up');935 if (thumbsDownBtn) thumbsDownBtn.classList.remove('active-down');936 937 saveToHistory(message);938 chatInput.value = '';939 charCount.innerText = '0/3,000';940 941 // --- CREDIT DEDUCTION LOGIC ---942 if (activeUser && supabaseClient) {943 let currentCredits = activeUser.user_metadata?.credits_remaining !== undefined ? activeUser.user_metadata.credits_remaining : 500;944 if (currentCredits > 500) currentCredits = 500; // Instantly scale legacy users down to new 500 limit945 946 if (currentCredits < 10) {947 responsePanel.style.display = 'flex';948 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>';949 chatInput.disabled = false;950 sendBtn.disabled = false;951 document.querySelector('.input-wrapper').classList.remove('disabled');952 return;953 }954 955 let newCredits = Math.max(0, currentCredits - 10);956 957 // Optimistic UI update958 const creditText = document.querySelector('.credit-info span');959 if (creditText) creditText.innerText = `${newCredits}/500 credits`;960 961 // Background sync with Supabase User Metadata962 supabaseClient.auth.updateUser({963 data: { credits_remaining: newCredits }964 }).then(({ data, error }) => {965 if (!error && data.user) {966 activeUser = data.user;967 }968 });969 }970 971 responsePanel.style.display = 'flex';972 973 let modeDisplay = "Unified Library";974 if (activeMode === "mbbs") modeDisplay = "Dx(MBBS) Knowledge Base";975 if (activeMode === "pharmacy") modeDisplay = "Rx(Pharmacy) Knowledge Base";976 977 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>`;978 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>`;979 980 responseText.innerHTML = queryHTML + badgeHTML + `981 <div class="skeleton skeleton-title"></div>982 <div class="skeleton skeleton-text"></div>983 <div class="skeleton skeleton-text"></div>984 <div class="skeleton skeleton-short"></div>985 `;986 sourceList.innerHTML = '';987 988 try {989 const headers = { 'Content-Type': 'application/json' };990 if (activeToken) {991 headers['Authorization'] = `Bearer ${activeToken}`;992 }993 994 const response = await fetch('/chat', {995 method: 'POST',996 headers: headers,997 body: JSON.stringify({ message: message, mode: activeMode })998 });999 1000 if (!response.ok) {1001 if (response.status === 401) {1002 responseText.innerHTML = '<span style="color: #ef4444;">Session expired or unauthorized. Please log out and log back in.</span>';1003 return;1004 }1005 throw new Error(`HTTP Error: ${response.status}`);1006 }1007 1008 const reader = response.body.getReader();1009 const decoder = new TextDecoder();1010 let fullAnswer = "";1011 let sources = [];1012 let sourceGalleryHTML = "";1013 1014 while (true) {1015 const { done, value } = await reader.read();1016 if (done) break;1017 1018 const chunk = decoder.decode(value, { stream: true });1019 const lines = chunk.split('\n');1020 1021 for (const line of lines) {1022 if (!line.trim()) continue;1023 try {1024 const json = JSON.parse(line);1025 if (json.type === 'sources') {1026 sources = json.data;1027 // Create a visual gallery for the images inline1028 sourceGalleryHTML = '<div class="source-images-gallery" style="display: flex; gap: 10px; margin-top: 15px; overflow-x: auto; padding-bottom: 10px;">';1029 sources.forEach(src => {1030 // Keep the clickable tags for history1031 const tag = document.createElement('span');1032 tag.className = 'source-tag';1033 tag.innerText = `${src.book_name} (Pg ${src.page_number})`;1034 tag.onclick = () => {1035 modalTitle.innerText = `${src.book_name} - Page ${src.page_number}`;1036 modalImg.src = `/page_image?path=${encodeURIComponent(src.image_path)}`;1037 pageModal.style.display = 'flex';1038 };1039 sourceList.appendChild(tag);1040 1041 // Auto-embed the exact image directly in the chat1042 sourceGalleryHTML += `1043 <div style="flex: 0 0 auto; width: 250px; border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 8px; overflow: hidden; background: rgba(0,0,0,0.2); cursor: pointer;" onclick="document.getElementById('modal-title').innerText='${src.book_name} - Page ${src.page_number}'; document.getElementById('modal-img').src='/page_image?path=${encodeURIComponent(src.image_path)}'; document.getElementById('page-modal').style.display='flex';">1044 <div style="font-size: 10px; color: var(--primary); padding: 5px 8px; border-bottom: 1px solid rgba(16, 185, 129, 0.2); font-weight: 600; text-transform: uppercase;">Exact Page Reference</div>1045 <img src="/page_image?path=${encodeURIComponent(src.image_path)}" style="width: 100%; height: auto; object-fit: contain; display: block;" alt="Reference Page">1046 </div>1047 `;1048 });1049 sourceGalleryHTML += '</div>';1050 1051 // Manually update the view since sources arrive last in the stream now1052 responseText.innerHTML = queryHTML + badgeHTML + DOMPurify.sanitize(marked.parse(fullAnswer)) + sourceGalleryHTML;1053 window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });1054 1055 } else if (json.type === 'start_answer') {1056 responseText.innerHTML = queryHTML + badgeHTML + sourceGalleryHTML;1057 fullAnswer = ""; // Reset buffer1058 } else if (json.type === 'content') {1059 fullAnswer += json.data;1060 responseText.innerHTML = queryHTML + badgeHTML + DOMPurify.sanitize(marked.parse(fullAnswer)) + sourceGalleryHTML;1061 window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });1062 }1063 } catch (e) { }1064 }1065 }1066 // Save complete result to history1067 saveToHistory(message, fullAnswer, sources);1068 1069 // Revert to user's default mode if they did a temporary override1070 if (activeUser?.user_metadata?.defaultMode) {1071 activeMode = activeUser.user_metadata.defaultMode;1072 updateModeButtons(activeMode);1073 }1074 1075 // Show feedback widget when streaming completes successfully1076 if (feedbackContainer) feedbackContainer.style.display = 'flex';1077 } catch (error) {1078 responseText.innerHTML = '<span style="color: #ef4444;">Connection error.</span>';1079 } finally {1080 // Re-enable inputs1081 chatInput.disabled = false;1082 sendBtn.disabled = false;1083 document.querySelector('.input-wrapper').classList.remove('disabled');1084 chatInput.focus();1085 }1086 }1087 1088 const resetModal = () => { 1089 pageModal.style.display = 'none'; 1090 modalImg.dataset.scale = 1;1091 modalImg.style.transform = 'scale(1)';1092 };1093 closeModal.onclick = resetModal;1094 window.onclick = (event) => { if (event.target == pageModal) resetModal(); };1095 1096 modalImg.addEventListener('wheel', (e) => {1097 e.preventDefault();1098 const delta = e.deltaY > 0 ? 0.9 : 1.1;1099 const newScale = (modalImg.dataset.scale || 1) * delta;1100 if (newScale >= 1 && newScale <= 5) {1101 modalImg.dataset.scale = newScale;1102 modalImg.style.transform = `scale(${newScale})`;1103 }1104 });1105 1106 let isDragging = false;1107 let startX, startY, scrollLeft, scrollTop;1108 1109 const modalBody = document.querySelector('.modal-body');1110 modalBody.addEventListener('mousedown', (e) => {1111 isDragging = true;1112 startX = e.pageX - modalBody.offsetLeft;1113 startY = e.pageY - modalBody.offsetTop;1114 scrollLeft = modalBody.scrollLeft;1115 scrollTop = modalBody.scrollTop;1116 });1117 1118 modalBody.addEventListener('mouseleave', () => { isDragging = false; });1119 modalBody.addEventListener('mouseup', () => { isDragging = false; });1120 1121 modalBody.addEventListener('mousemove', (e) => {1122 if (!isDragging) return;1123 e.preventDefault();1124 const x = e.pageX - modalBody.offsetLeft;1125 const y = e.pageY - modalBody.offsetTop;1126 const walkX = (x - startX) * 2;1127 const walkY = (y - startY) * 2;1128 modalBody.scrollLeft = scrollLeft - walkX;1129 modalBody.scrollTop = scrollTop - walkY;1130 });1131 1132 // Feedback loop logic1133 const thumbsUpBtn = document.getElementById('thumbs-up-btn');1134 const thumbsDownBtn = document.getElementById('thumbs-down-btn');1135 const feedbackLabel = document.querySelector('.feedback-label');1136 1137 if (thumbsUpBtn && thumbsDownBtn) {1138 thumbsUpBtn.addEventListener('click', () => {1139 thumbsUpBtn.classList.toggle('active-up');1140 thumbsDownBtn.classList.remove('active-down');1141 if (thumbsUpBtn.classList.contains('active-up')) {1142 feedbackLabel.innerText = "Thank you!";1143 setTimeout(() => { feedbackLabel.innerText = "Was this helpful?"; }, 3000);1144 }1145 });1146 1147 thumbsDownBtn.addEventListener('click', () => {1148 thumbsDownBtn.classList.toggle('active-down');1149 thumbsUpBtn.classList.remove('active-up');1150 if (thumbsDownBtn.classList.contains('active-down')) {1151 feedbackLabel.innerText = "Thank you!";1152 setTimeout(() => { feedbackLabel.innerText = "Was this helpful?"; }, 3000);1153 }1154 });1155 }1156 1157 sendBtn.addEventListener('click', handleSend);1158 chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleSend(); });1159 </script>1160</body>1161 1162</html>