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; | |
| background-clip: text; | |
| margin-bottom: 0.5rem; | |
| letter-spacing: -0.02em; | |
| } | |
| .subtitle { | |
| color: var(--text-muted); | |
| font-size: 1.25rem; | |
| font-weight: 300; | |
| } | |
| .subtitle a { | |
| color: var(--primary); | |
| text-decoration: none; | |
| } | |
| .subtitle a:hover { | |
| text-decoration: underline; | |
| } | |
| /* 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); | |
| } | |
| } | |
| /* Chat History */ | |
| #chatHistory { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| } | |
| .message-block { | |
| border: 1px solid var(--border-color); | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| background: var(--bg-card); | |
| animation: fadeInUp 0.4s ease-out; | |
| } | |
| .user-question { | |
| margin-bottom: 1rem; | |
| color: var(--text-main); | |
| } | |
| .user-question strong { | |
| color: var(--primary); | |
| } | |
| .assistant-answer { | |
| margin-bottom: 1rem; | |
| line-height: 1.8; | |
| } | |
| .assistant-answer strong { | |
| color: #8b5cf6; | |
| } | |
| /* Image Grids */ | |
| .images-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); | |
| gap: 1.5rem; | |
| margin-top: 1rem; | |
| } | |
| .image-card { | |
| background: rgba(30, 41, 59, 0.6); | |
| border-radius: 0.75rem; | |
| overflow: hidden; | |
| border: 1px solid var(--border-color); | |
| transition: all 0.3s ease; | |
| } | |
| .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; | |
| } | |
| /* Text Sources */ | |
| .source-card { | |
| background: rgba(30, 41, 59, 0.4); | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.75rem; | |
| padding: 1.25rem; | |
| margin-top: 0.75rem; | |
| transition: background 0.2s; | |
| } | |
| .source-card:hover { | |
| background: rgba(30, 41, 59, 0.8); | |
| border-color: #475569; | |
| } | |
| .source-title { | |
| font-weight: 600; | |
| color: var(--primary); | |
| margin-bottom: 0.5rem; | |
| font-size: 0.95rem; | |
| } | |
| .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); | |
| word-wrap: break-word; | |
| } | |
| .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"></div> | |
| <div id="loading"> | |
| <div class="spinner"></div> | |
| <p style="color: var(--text-muted); font-size: 0.9rem;">Processing multimodal embeddings...</p> | |
| </div> | |
| </div> | |
| <script> | |
| const input = document.getElementById('questionInput'); | |
| const searchBtn = document.getElementById('searchBtn'); | |
| const loading = document.getElementById('loading'); | |
| const chatHistoryContainer = document.getElementById('chatHistory'); | |
| // Allow Enter key to submit | |
| input.addEventListener('keypress', function (e) { | |
| if (e.key === 'Enter') { | |
| askQuestion(); | |
| } | |
| }); | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| 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'; | |
| try { | |
| const response = await fetch('/query', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ question: question }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Server error: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| // Create a chat message container | |
| const messageBlock = document.createElement('div'); | |
| messageBlock.className = 'message-block'; | |
| // User question | |
| const userQuestion = document.createElement('div'); | |
| userQuestion.className = 'user-question'; | |
| userQuestion.innerHTML = `<strong>You:</strong> ${escapeHtml(question)}`; | |
| messageBlock.appendChild(userQuestion); | |
| // Assistant answer | |
| const assistantAnswer = document.createElement('div'); | |
| assistantAnswer.className = 'assistant-answer'; | |
| assistantAnswer.innerHTML = `<strong>Assistant:</strong><br>${escapeHtml(data.answer).replace(/\n/g, '<br>')}`; | |
| messageBlock.appendChild(assistantAnswer); | |
| // Display Images | |
| if (data.images && data.images.length > 0) { | |
| const relevantImages = data.images.filter(img => (img.score || 0) >= 0.3).slice(0, 3); | |
| if (relevantImages.length > 0) { | |
| const imagesWrapper = document.createElement('div'); | |
| imagesWrapper.className = 'images-grid'; | |
| relevantImages.forEach(img => { | |
| const div = document.createElement('div'); | |
| div.className = 'image-card'; | |
| div.innerHTML = ` | |
| <a href="${escapeHtml(img.path || '')}" target="_blank"> | |
| <img src="${escapeHtml(img.path || '')}" alt="${escapeHtml(img.filename || 'Image')}" onerror="this.style.display='none'"> | |
| </a> | |
| <div class="image-meta"> | |
| <div class="image-filename">${escapeHtml(img.filename || 'Unknown')}</div> | |
| </div> | |
| `; | |
| imagesWrapper.appendChild(div); | |
| }); | |
| messageBlock.appendChild(imagesWrapper); | |
| } | |
| } | |
| // Display Texts | |
| if (data.texts && data.texts.length > 0) { | |
| const topTexts = data.texts.slice(0, 3); | |
| topTexts.forEach(txt => { | |
| const div = document.createElement('div'); | |
| div.className = 'source-card'; | |
| const truncatedText = txt.text.length > 200 ? txt.text.substring(0, 200) + '...' : txt.text; | |
| div.innerHTML = ` | |
| <div class="source-title">${escapeHtml(txt.file || 'Document')}</div> | |
| <div class="source-excerpt">"${escapeHtml(truncatedText)}"</div> | |
| <div class="source-meta"> | |
| Page ${escapeHtml(String(txt.page || 'N/A'))} • ${Math.round((txt.score || 0) * 100)}% match | |
| </div> | |
| `; | |
| messageBlock.appendChild(div); | |
| }); | |
| } | |
| chatHistoryContainer.appendChild(messageBlock); | |
| // Scroll to the new message | |
| setTimeout(() => { | |
| messageBlock.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| }, 100); | |
| } catch (error) { | |
| alert('Error querying system. Please try again.'); | |
| console.error('Query 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> |