Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WHEC Chatbot • 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; | |
| } | |
| html, body { | |
| height: 100%; | |
| overflow: hidden; | |
| } | |
| body { | |
| font-family: 'Outfit', sans-serif; | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| 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%); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header - Fixed at top */ | |
| header { | |
| text-align: center; | |
| padding: 2rem 2rem 1rem; | |
| border-bottom: 1px solid var(--border-color); | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(10px); | |
| flex-shrink: 0; | |
| } | |
| h1 { | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-size: 2rem; | |
| font-weight: 700; | |
| background: var(--accent-gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 0.25rem; | |
| letter-spacing: -0.02em; | |
| } | |
| .subtitle { | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| font-weight: 300; | |
| } | |
| .subtitle a { | |
| color: var(--primary); | |
| text-decoration: none; | |
| } | |
| .subtitle a:hover { | |
| text-decoration: underline; | |
| } | |
| /* Action Buttons - In header */ | |
| .action-buttons { | |
| display: flex; | |
| gap: 0.75rem; | |
| justify-content: center; | |
| margin-top: 1rem; | |
| } | |
| .action-buttons button { | |
| background: rgba(139, 92, 246, 0.2); | |
| color: #c4b5fd; | |
| border: 1px solid #8b5cf6; | |
| padding: 0.5rem 1rem; | |
| border-radius: 0.5rem; | |
| font-weight: 600; | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .action-buttons button:hover:not(:disabled) { | |
| background: rgba(139, 92, 246, 0.3); | |
| border-color: #a78bfa; | |
| } | |
| .action-buttons button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| #clearBtn { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: #fca5a5; | |
| border-color: #ef4444; | |
| } | |
| #clearBtn:hover { | |
| background: rgba(239, 68, 68, 0.3); | |
| border-color: #f87171; | |
| } | |
| /* Chat Container - Scrollable middle section */ | |
| .chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Empty State - Shows when no messages */ | |
| .empty-state { | |
| margin: auto; | |
| text-align: center; | |
| max-width: 600px; | |
| padding: 2rem; | |
| } | |
| .empty-state h2 { | |
| font-size: 1.5rem; | |
| color: var(--text-main); | |
| margin-bottom: 1rem; | |
| } | |
| .empty-state p { | |
| color: var(--text-muted); | |
| margin-bottom: 1.5rem; | |
| } | |
| .suggestions { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| } | |
| .suggestion-card { | |
| background: rgba(30, 41, 59, 0.5); | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.75rem; | |
| padding: 1rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| text-align: left; | |
| } | |
| .suggestion-card:hover { | |
| background: rgba(30, 41, 59, 0.8); | |
| border-color: var(--primary); | |
| transform: translateY(-2px); | |
| } | |
| .suggestion-card .icon { | |
| font-size: 1.5rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .suggestion-card .text { | |
| color: var(--text-main); | |
| font-size: 0.9rem; | |
| } | |
| /* Chat History */ | |
| #chatHistory { | |
| max-width: 900px; | |
| width: 100%; | |
| margin: 0 auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .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); | |
| font-size: 0.95rem; | |
| } | |
| .user-question strong { | |
| color: var(--primary); | |
| } | |
| .assistant-answer { | |
| margin-bottom: 1rem; | |
| line-height: 1.8; | |
| font-size: 0.95rem; | |
| } | |
| .assistant-answer strong { | |
| color: #8b5cf6; | |
| } | |
| /* Sources Section */ | |
| .sources-section { | |
| margin-top: 1rem; | |
| } | |
| .sources-header { | |
| font-weight: 600; | |
| color: var(--primary); | |
| margin-bottom: 0.75rem; | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .source-card { | |
| background: rgba(30, 41, 59, 0.4); | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.75rem; | |
| padding: 1rem; | |
| margin-bottom: 0.75rem; | |
| transition: all 0.2s; | |
| cursor: pointer; | |
| } | |
| .source-card:hover { | |
| background: rgba(30, 41, 59, 0.8); | |
| border-color: var(--primary); | |
| transform: translateX(5px); | |
| } | |
| .source-title { | |
| font-weight: 600; | |
| color: var(--primary); | |
| margin-bottom: 0.5rem; | |
| font-size: 0.85rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .source-excerpt { | |
| font-style: italic; | |
| color: #cbd5e1; | |
| font-size: 0.85rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 0.75rem; | |
| border-radius: 0.5rem; | |
| border-left: 3px solid var(--primary); | |
| word-wrap: break-word; | |
| line-height: 1.6; | |
| } | |
| .source-meta { | |
| margin-top: 0.75rem; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .view-source-link { | |
| color: var(--primary); | |
| text-decoration: none; | |
| font-weight: 500; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.25rem; | |
| transition: all 0.2s; | |
| } | |
| .view-source-link:hover { | |
| color: #60a5fa; | |
| text-decoration: underline; | |
| } | |
| /* Images Section */ | |
| .images-section { | |
| margin-top: 1.5rem; | |
| } | |
| .images-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |
| gap: 1rem; | |
| 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: 0.75rem; | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .image-filename { | |
| font-weight: 600; | |
| color: #f1f5f9; | |
| font-size: 0.8rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| margin-bottom: 0.25rem; | |
| } | |
| .image-source { | |
| font-size: 0.7rem; | |
| color: var(--text-muted); | |
| } | |
| /* Loading indicator */ | |
| .loading-message { | |
| max-width: 900px; | |
| width: 100%; | |
| margin: 0 auto; | |
| padding: 1.5rem; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: 1rem; | |
| display: none; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .loading-message.active { | |
| display: flex; | |
| } | |
| .spinner { | |
| width: 20px; | |
| height: 20px; | |
| border: 2px solid rgba(59, 130, 246, 0.3); | |
| border-radius: 50%; | |
| border-top-color: var(--primary); | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Input Container - Fixed at bottom */ | |
| .input-container { | |
| border-top: 1px solid var(--border-color); | |
| background: rgba(15, 23, 42, 0.95); | |
| backdrop-filter: blur(10px); | |
| padding: 1.5rem 2rem; | |
| flex-shrink: 0; | |
| } | |
| .input-wrapper { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| position: relative; | |
| } | |
| .input-group { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| background: rgba(30, 41, 59, 0.7); | |
| padding: 0.5rem; | |
| border-radius: 1.5rem; | |
| border: 1px solid var(--border-color); | |
| transition: all 0.2s ease; | |
| } | |
| .input-group:focus-within { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); | |
| } | |
| .input-group textarea { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| padding: 0.75rem 1rem; | |
| font-size: 1rem; | |
| color: white; | |
| font-family: 'Outfit', sans-serif; | |
| resize: none; | |
| max-height: 200px; | |
| min-height: 24px; | |
| overflow-y: auto; | |
| } | |
| .input-group textarea:focus { | |
| outline: none; | |
| } | |
| .input-group textarea::placeholder { | |
| color: #64748b; | |
| } | |
| .send-button { | |
| background: var(--accent-gradient); | |
| color: white; | |
| border: none; | |
| padding: 0.75rem; | |
| border-radius: 1rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .send-button:hover:not(:disabled) { | |
| opacity: 0.9; | |
| transform: scale(1.05); | |
| } | |
| .send-button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* 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; | |
| } | |
| /* Animations */ | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Fixed Header --> | |
| <header> | |
| <h1>WHEC Chatbot</h1> | |
| <p class="subtitle"> | |
| AI Research Assistant for WHEC (Warrior Heat- and Exertion-Related Events Collaborative) | |
| <a href="https://www.hprc-online.org/resources-partners/whec" target="_blank">Learn more</a> | |
| </p> | |
| <div class="action-buttons" id="actionButtons" style="display: none;"> | |
| <button id="downloadBtn" onclick="downloadPDFReport()"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |
| </svg> | |
| Download Report | |
| </button> | |
| <button id="clearBtn" onclick="clearHistory()"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path> | |
| </svg> | |
| Clear Chat | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Scrollable Chat Area --> | |
| <div class="chat-container" id="chatContainer"> | |
| <!-- Empty State --> | |
| <div class="empty-state" id="emptyState"> | |
| <h2>Welcome to WHEC Research Assistant</h2> | |
| <p>Ask questions about heat and exertion-related events, medical research, and military health topics.</p> | |
| <div class="suggestions"> | |
| <div class="suggestion-card" onclick="askSuggestion('What is WHEC?')"> | |
| <div class="icon">🏥</div> | |
| <div class="text">What is WHEC?</div> | |
| </div> | |
| <div class="suggestion-card" onclick="askSuggestion('What are the symptoms of heat stroke?')"> | |
| <div class="icon">🌡️</div> | |
| <div class="text">Symptoms of heat stroke</div> | |
| </div> | |
| <div class="suggestion-card" onclick="askSuggestion('How to prevent exertional heat illness?')"> | |
| <div class="icon">🛡️</div> | |
| <div class="text">Prevention strategies</div> | |
| </div> | |
| <div class="suggestion-card" onclick="askSuggestion('What is the Heat Toolkit?')"> | |
| <div class="icon">📚</div> | |
| <div class="text">About Heat Toolkit</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat History --> | |
| <div id="chatHistory"></div> | |
| <!-- Loading Indicator --> | |
| <div class="loading-message" id="loadingMessage"> | |
| <div class="spinner"></div> | |
| <span>Searching documents and generating response...</span> | |
| </div> | |
| </div> | |
| <!-- Fixed Input at Bottom --> | |
| <div class="input-container"> | |
| <div class="input-wrapper"> | |
| <div class="input-group"> | |
| <textarea | |
| id="messageInput" | |
| placeholder="Ask about exertion-related injuries, heat illness, or military health topics..." | |
| rows="1" | |
| ></textarea> | |
| <button class="send-button" id="sendButton" onclick="sendMessage()"> | |
| <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="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const messageInput = document.getElementById('messageInput'); | |
| const sendButton = document.getElementById('sendButton'); | |
| const chatHistory = document.getElementById('chatHistory'); | |
| const chatContainer = document.getElementById('chatContainer'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const loadingMessage = document.getElementById('loadingMessage'); | |
| const actionButtons = document.getElementById('actionButtons'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| let conversationData = []; | |
| // Auto-resize textarea | |
| messageInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 200) + 'px'; | |
| }); | |
| // Send on Enter (Shift+Enter for new line) | |
| messageInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function askSuggestion(question) { | |
| messageInput.value = question; | |
| sendMessage(); | |
| } | |
| async function sendMessage() { | |
| const question = messageInput.value.trim(); | |
| if (!question) return; | |
| // Hide empty state | |
| if (emptyState) { | |
| emptyState.style.display = 'none'; | |
| } | |
| // Clear input | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| // Disable input | |
| messageInput.disabled = true; | |
| sendButton.disabled = true; | |
| // Show loading | |
| loadingMessage.classList.add('active'); | |
| // Scroll to bottom | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| 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(); | |
| // Store conversation | |
| conversationData.push({ | |
| question: question, | |
| answer: data.answer, | |
| images: data.images || [], | |
| texts: data.texts || [], | |
| timestamp: new Date().toISOString() | |
| }); | |
| // Show action buttons | |
| actionButtons.style.display = 'flex'; | |
| // Create message block | |
| 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); | |
| // Text sources | |
| if (data.texts && data.texts.length > 0) { | |
| const sourcesSection = document.createElement('div'); | |
| sourcesSection.className = 'sources-section'; | |
| const sourcesHeader = document.createElement('div'); | |
| sourcesHeader.className = 'sources-header'; | |
| sourcesHeader.innerHTML = ` | |
| <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |
| </svg> | |
| Referenced Sources | |
| `; | |
| sourcesSection.appendChild(sourcesHeader); | |
| const topTexts = data.texts.slice(0, 3); | |
| topTexts.forEach((txt) => { | |
| const div = document.createElement('div'); | |
| div.className = 'source-card'; | |
| const truncatedText = txt.text.length > 250 ? txt.text.substring(0, 250) + '...' : txt.text; | |
| const sourceLink = txt.link ? `<a href="${escapeHtml(txt.link)}" target="_blank" class="view-source-link" onclick="event.stopPropagation()"> | |
| View in document → | |
| </a>` : ''; | |
| div.innerHTML = ` | |
| <div class="source-title"> | |
| <svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path> | |
| </svg> | |
| ${escapeHtml(txt.file || 'Document')} | |
| </div> | |
| <div class="source-excerpt">"${escapeHtml(truncatedText)}"</div> | |
| <div class="source-meta"> | |
| <span>Page ${escapeHtml(String(txt.page || 'N/A'))} • ${Math.round((txt.score || 0) * 100)}% match</span> | |
| ${sourceLink} | |
| </div> | |
| `; | |
| if (txt.link) { | |
| div.style.cursor = 'pointer'; | |
| div.onclick = () => window.open(txt.link, '_blank'); | |
| } | |
| sourcesSection.appendChild(div); | |
| }); | |
| messageBlock.appendChild(sourcesSection); | |
| } | |
| // Images | |
| if (data.images && data.images.length > 0) { | |
| const relevantImages = data.images.filter(img => (img.score || 0) >= 0.3).slice(0, 2); | |
| if (relevantImages.length > 0) { | |
| const imagesSection = document.createElement('div'); | |
| imagesSection.className = 'images-section'; | |
| const imagesHeader = document.createElement('div'); | |
| imagesHeader.className = 'sources-header'; | |
| imagesHeader.innerHTML = ` | |
| <svg width="14" height="14" 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> | |
| Referenced Figures | |
| `; | |
| imagesSection.appendChild(imagesHeader); | |
| 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 class="image-source"> | |
| ${escapeHtml(img.file || 'Unknown')} • Page ${escapeHtml(String(img.page || 'N/A'))} | |
| </div> | |
| </div> | |
| `; | |
| imagesWrapper.appendChild(div); | |
| }); | |
| imagesSection.appendChild(imagesWrapper); | |
| messageBlock.appendChild(imagesSection); | |
| } | |
| } | |
| chatHistory.appendChild(messageBlock); | |
| // Scroll to bottom smoothly | |
| setTimeout(() => { | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| }, 100); | |
| } catch (error) { | |
| alert('Error: ' + error.message); | |
| console.error('Error:', error); | |
| } finally { | |
| // Hide loading | |
| loadingMessage.classList.remove('active'); | |
| // Re-enable input | |
| messageInput.disabled = false; | |
| sendButton.disabled = false; | |
| messageInput.focus(); | |
| } | |
| } | |
| async function downloadPDFReport() { | |
| if (conversationData.length === 0) { | |
| alert('No conversation to download'); | |
| return; | |
| } | |
| downloadBtn.disabled = true; | |
| const originalText = downloadBtn.innerHTML; | |
| downloadBtn.innerHTML = '<span style="font-size: 0.85em">Generating...</span>'; | |
| try { | |
| const response = await fetch('/generate-report', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ conversations: conversationData }) | |
| }); | |
| if (!response.ok) throw new Error(`Server error: ${response.status}`); | |
| const blob = await response.blob(); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `WHEC_Report_${new Date().toISOString().split('T')[0]}.pdf`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } catch (error) { | |
| alert('Error generating report: ' + error.message); | |
| } finally { | |
| downloadBtn.disabled = false; | |
| downloadBtn.innerHTML = originalText; | |
| } | |
| } | |
| function clearHistory() { | |
| if (conversationData.length === 0) return; | |
| if (!confirm('Clear all conversation history? This cannot be undone.')) return; | |
| conversationData = []; | |
| chatHistory.innerHTML = ''; | |
| actionButtons.style.display = 'none'; | |
| emptyState.style.display = 'block'; | |
| } | |
| // Focus input on load | |
| messageInput.focus(); | |
| </script> | |
| </body> | |
| </html> |