mohamedhassan22's picture
Update static/index.html
4b4f6d3 verified
<!DOCTYPE html>
<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>