Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Multimodal RAG • AI Research Assistant</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #3b82f6; | |
| --primary-glow: rgba(59, 130, 246, 0.5); | |
| --bg-dark: #0f172a; | |
| --bg-card: #1e293b; | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --border-color: #334155; | |
| --accent-gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Outfit', sans-serif; | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| line-height: 1.6; | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 20%), | |
| radial-gradient(circle at 90% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 20%); | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| } | |
| header { | |
| text-align: center; | |
| padding: 4rem 0 2rem; | |
| animation: fadeInDown 0.8s ease-out; | |
| } | |
| h1 { | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-size: 3.5rem; | |
| font-weight: 700; | |
| background: var(--accent-gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 0.5rem; | |
| letter-spacing: -0.02em; | |
| } | |
| .subtitle { | |
| color: var(--text-muted); | |
| font-size: 1.25rem; | |
| font-weight: 300; | |
| } | |
| /* Search Section */ | |
| .search-container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| width: 100%; | |
| position: relative; | |
| z-index: 10; | |
| } | |
| .input-group { | |
| position: relative; | |
| display: flex; | |
| gap: 1rem; | |
| background: rgba(30, 41, 59, 0.7); | |
| padding: 0.5rem; | |
| border-radius: 1rem; | |
| border: 1px solid var(--border-color); | |
| backdrop-filter: blur(12px); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| transition: all 0.3s ease; | |
| } | |
| .input-group:focus-within { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); | |
| transform: translateY(-2px); | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| padding: 1rem 1.5rem; | |
| font-size: 1.1rem; | |
| color: white; | |
| font-family: 'Outfit', sans-serif; | |
| width: 100%; | |
| } | |
| input[type="text"]:focus { | |
| outline: none; | |
| } | |
| input[type="text"]::placeholder { | |
| color: #64748b; | |
| } | |
| button#searchBtn { | |
| background: var(--accent-gradient); | |
| color: white; | |
| border: none; | |
| padding: 0 2rem; | |
| border-radius: 0.75rem; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: opacity 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| button#searchBtn:hover { | |
| opacity: 0.9; | |
| } | |
| button#searchBtn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* Loading State */ | |
| #loading { | |
| display: none; | |
| text-align: center; | |
| padding: 2rem; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 3px solid rgba(59, 130, 246, 0.3); | |
| border-radius: 50%; | |
| border-top-color: var(--primary); | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| /* Results Area */ | |
| #results { | |
| display: none; | |
| /* Hidden by default */ | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| width: 100%; | |
| animation: fadeInUp 0.5s ease-out; | |
| } | |
| .section-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| margin-top: 2rem; | |
| border-bottom: 1px solid var(--border-color); | |
| padding-bottom: 0.5rem; | |
| } | |
| .answer-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| padding: 2rem; | |
| border-radius: 1rem; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .answer-text { | |
| font-size: 1.1rem; | |
| color: #e2e8f0; | |
| white-space: pre-wrap; | |
| line-height: 1.8; | |
| } | |
| /* Image Grids */ | |
| .images-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| .image-card { | |
| background: var(--bg-card); | |
| border-radius: 0.75rem; | |
| overflow: hidden; | |
| border: 1px solid var(--border-color); | |
| transition: all 0.3s ease; | |
| position: relative; | |
| group: card; | |
| } | |
| .image-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); | |
| border-color: var(--primary); | |
| } | |
| .image-card a { | |
| display: block; | |
| overflow: hidden; | |
| height: 180px; | |
| } | |
| .image-card img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| transition: transform 0.5s ease; | |
| } | |
| .image-card:hover img { | |
| transform: scale(1.1); | |
| } | |
| .image-meta { | |
| padding: 1rem; | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .image-filename { | |
| font-weight: 600; | |
| color: #f1f5f9; | |
| font-size: 0.95rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| margin-bottom: 0.25rem; | |
| } | |
| .image-stats { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| /* Text Sources */ | |
| .sources-grid { | |
| display: grid; | |
| gap: 1rem; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| } | |
| .source-card { | |
| background: rgba(30, 41, 59, 0.4); | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.75rem; | |
| padding: 1.25rem; | |
| transition: background 0.2s; | |
| } | |
| .source-card:hover { | |
| background: rgba(30, 41, 59, 0.8); | |
| border-color: #475569; | |
| } | |
| .source-title, | |
| .source-link { | |
| font-weight: 600; | |
| color: var(--primary); | |
| margin-bottom: 0.5rem; | |
| font-size: 0.95rem; | |
| display: block; | |
| text-decoration: none; | |
| } | |
| .source-link:hover { | |
| text-decoration: underline; | |
| color: #60a5fa; | |
| } | |
| .source-excerpt { | |
| font-style: italic; | |
| color: #cbd5e1; | |
| font-size: 0.9rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 0.75rem; | |
| border-radius: 0.5rem; | |
| border-left: 3px solid var(--primary); | |
| } | |
| .source-meta { | |
| margin-top: 0.75rem; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| text-align: right; | |
| } | |
| /* Animations */ | |
| @keyframes fadeInDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-dark); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #475569; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>WHEC - Chatbot</h1> | |
| <p class="subtitle">Based on documents at WHEC (Warrior Heat- and Exertion-Related Events Collaborative) | |
| <a href="https://www.hprc-online.org/resources-partners/whec" target="_blank">page</a> | |
| </p> | |
| </header> | |
| <div class="search-container"> | |
| <div class="input-group"> | |
| <input type="text" id="questionInput" placeholder="Ask a question about exertion-related injuries..." | |
| autocomplete="off"> | |
| <button id="searchBtn" onclick="askQuestion()"> | |
| <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> | |
| </svg> | |
| Analyze | |
| </button> | |
| </div> | |
| </div> | |
| <!-- CHAT HISTORY --> | |
| <div id="chatHistory" style="max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 2rem;"> | |
| </div> | |
| <div id="loading"> | |
| <div class="spinner"></div> | |
| <p style="color: var(--text-muted); font-size: 0.9rem;">Processing multimodal embeddings...</p> | |
| </div> | |
| <div class="answer-card"> | |
| <div id="answerText" class="answer-text"></div> | |
| </div> | |
| <!-- TEXT SOURCES SECTION --> | |
| <div id="textsSection" style="display: none;"> | |
| <div class="section-title" style="margin-top: 3rem;"> | |
| <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"> | |
| </path> | |
| </svg> | |
| Source Documents | |
| </div> | |
| <div id="textsGrid" class="sources-grid"></div> | |
| </div> | |
| <!-- IMAGES SECTION --> | |
| <div id="imagesSection" style="display: none;"> | |
| <div class="section-title" style="margin-top: 3rem;"> | |
| <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"> | |
| </path> | |
| </svg> | |
| Relevant Visual Data | |
| </div> | |
| <div id="imagesGrid" class="images-grid"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const input = document.getElementById('questionInput'); | |
| const searchBtn = document.getElementById('searchBtn'); | |
| const loading = document.getElementById('loading'); | |
| const chatHistoryContainer = document.getElementById('chatHistory'); | |
| imagesSection.style.display = 'none'; | |
| textsSection.style.display = 'none'; | |
| // Allow Enter key to submit | |
| input.addEventListener('keypress', function (e) { | |
| if (e.key === 'Enter') { | |
| askQuestion(); | |
| } | |
| }); | |
| async function askQuestion() { | |
| const question = input.value.trim(); | |
| if (!question) return; | |
| // UI State updates | |
| searchBtn.disabled = true; | |
| searchBtn.innerHTML = '<span style="font-size: 0.9em">Processing...</span>'; | |
| input.disabled = true; | |
| loading.style.display = 'block'; | |
| // Reset sections | |
| imagesSection.style.display = 'none'; | |
| textsSection.style.display = 'none'; | |
| try { | |
| const response = await fetch('/query', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ question: question }) | |
| }); | |
| if (!response.ok) throw new Error('Failed to get response'); | |
| const data = await response.json(); | |
| // Create a chat message container | |
| const messageBlock = document.createElement('div'); | |
| messageBlock.style.border = '1px solid var(--border-color)'; | |
| messageBlock.style.borderRadius = '1rem'; | |
| messageBlock.style.padding = '1.5rem'; | |
| messageBlock.style.background = 'var(--bg-card)'; | |
| messageBlock.style.animation = 'fadeInUp 0.4s ease-out'; | |
| // User question | |
| const userQuestion = document.createElement('div'); | |
| userQuestion.style.marginBottom = '1rem'; | |
| userQuestion.innerHTML = `<strong>You:</strong> ${question}`; | |
| messageBlock.appendChild(userQuestion); | |
| // Assistant answer | |
| const assistantAnswer = document.createElement('div'); | |
| assistantAnswer.style.marginBottom = '1rem'; | |
| assistantAnswer.innerHTML = `<strong>Assistant:</strong><br>${data.answer}`; | |
| messageBlock.appendChild(assistantAnswer); | |
| // 2. Display Images | |
| if (data.images && data.images.length > 0) { | |
| const imagesWrapper = document.createElement('div'); | |
| imagesWrapper.className = 'images-grid'; | |
| imagesWrapper.style.marginTop = '1rem'; | |
| data.images | |
| .filter(img => img.score >= 0.3) | |
| .slice(0, 3) | |
| .forEach(img => { | |
| const div = document.createElement('div'); | |
| div.className = 'image-card'; | |
| div.innerHTML = ` | |
| <a href="${img.path}" target="_blank"> | |
| <img src="${img.path}" alt="${img.filename}"> | |
| </a> | |
| <div class="image-meta"> | |
| <div class="image-filename">${img.filename}</div> | |
| </div> | |
| `; | |
| imagesWrapper.appendChild(div); | |
| }); | |
| messageBlock.appendChild(imagesWrapper); | |
| } | |
| // 3. Display Texts | |
| if (data.texts && data.texts.length > 0) { | |
| const sourcesWrapper = document.createElement('div'); | |
| sourcesWrapper.style.marginTop = '1rem'; | |
| data.texts.slice(0, 3).forEach(txt => { | |
| const div = document.createElement('div'); | |
| div.className = 'source-card'; | |
| div.innerHTML = ` | |
| <div class="source-title">${txt.file || 'Document'}</div> | |
| <div class="source-excerpt">"${txt.text}..."</div> | |
| <div class="source-meta"> | |
| Page ${txt.page || 'N/A'} • ${(txt.score * 100).toFixed(0)}% | |
| </div> | |
| `; | |
| sourcesWrapper.appendChild(div); | |
| }); | |
| messageBlock.appendChild(sourcesWrapper); | |
| } | |
| chatHistoryContainer.appendChild(messageBlock); | |
| chatHistoryContainer.scrollTop = chatHistoryContainer.scrollHeight; | |
| } catch (error) { | |
| alert('Error querying system. Please try again.'); | |
| console.error(error); | |
| } finally { | |
| loading.style.display = 'none'; | |
| searchBtn.disabled = false; | |
| searchBtn.innerHTML = ` | |
| <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg> | |
| Analyze | |
| `; | |
| input.disabled = false; | |
| input.focus(); | |
| input.value = ''; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |