Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Medical Assistant</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <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=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: 'Plus Jakarta Sans', sans-serif; } | |
| .chat-container { height: calc(100vh - 200px); } | |
| .message { animation: fadeIn 0.3s ease-in; } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| .cursor { | |
| animation: blink 1s infinite; | |
| color: #3b82f6; | |
| } | |
| @keyframes blink { | |
| 0%, 49% { opacity: 1; } | |
| 50%, 100% { opacity: 0; } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="max-w-4xl mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-3xl font-semibold text-gray-800 mb-2">Medical Assistant</h1> | |
| <p class="text-gray-500 text-sm">Ask health-related questions and get evidence-based answers</p> | |
| </header> | |
| <!-- Chat Container --> | |
| <div class="bg-white rounded-lg shadow-sm border border-gray-200"> | |
| <div id="chatbox" class="chat-container overflow-y-auto p-6 space-y-4"> | |
| <div class="message flex gap-3"> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center"> | |
| <svg class="w-5 h-5 text-blue-600" 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> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="text-gray-700 text-sm leading-relaxed">Hello! I'm your medical assistant. I can help answer your health-related questions based on medical knowledge. How can I assist you today?</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="border-t border-gray-200 p-4"> | |
| <form id="chatForm" class="flex gap-3"> | |
| <input | |
| type="text" | |
| id="messageInput" | |
| placeholder="Type your question here..." | |
| class="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" | |
| required | |
| > | |
| <button | |
| type="submit" | |
| id="sendBtn" | |
| class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" | |
| > | |
| Send | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const chatbox = document.getElementById('chatbox'); | |
| const chatForm = document.getElementById('chatForm'); | |
| const messageInput = document.getElementById('messageInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| // Session ID for conversation memory (resets on page reload) | |
| const sessionId = "{{ session_id }}"; | |
| // Auto-scroll to bottom | |
| function scrollToBottom() { | |
| chatbox.scrollTop = chatbox.scrollHeight; | |
| } | |
| // Add user message to chat | |
| function addUserMessage(message) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message flex gap-3 justify-end'; | |
| messageDiv.innerHTML = ` | |
| <div class="flex-1 max-w-2xl"> | |
| <div class="bg-blue-600 text-white px-4 py-3 rounded-lg text-sm leading-relaxed"> | |
| ${escapeHtml(message)} | |
| </div> | |
| </div> | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center"> | |
| <svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path> | |
| </svg> | |
| </div> | |
| `; | |
| chatbox.appendChild(messageDiv); | |
| scrollToBottom(); | |
| } | |
| // Add bot message to chat | |
| function addBotMessage(message) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message flex gap-3'; | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center"> | |
| <svg class="w-5 h-5 text-blue-600" 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> | |
| </div> | |
| <div class="flex-1 max-w-2xl"> | |
| <div class="bg-gray-100 px-4 py-3 rounded-lg text-sm leading-relaxed text-gray-700"> | |
| ${escapeHtml(message)} | |
| </div> | |
| </div> | |
| `; | |
| chatbox.appendChild(messageDiv); | |
| scrollToBottom(); | |
| } | |
| // Add loading indicator | |
| function addLoadingIndicator() { | |
| const loadingDiv = document.createElement('div'); | |
| loadingDiv.id = 'loading'; | |
| loadingDiv.className = 'message flex gap-3'; | |
| loadingDiv.innerHTML = ` | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center"> | |
| <svg class="w-5 h-5 text-blue-600" 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> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="bg-gray-100 px-4 py-3 rounded-lg text-sm"> | |
| <div class="flex gap-1"> | |
| <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms"></div> | |
| <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms"></div> | |
| <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms"></div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| chatbox.appendChild(loadingDiv); | |
| scrollToBottom(); | |
| } | |
| function removeLoadingIndicator() { | |
| const loading = document.getElementById('loading'); | |
| if (loading) loading.remove(); | |
| } | |
| // Create a streaming message container | |
| function createStreamingMessage(messageId) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.id = messageId; | |
| messageDiv.className = 'message flex gap-3'; | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center"> | |
| <svg class="w-5 h-5 text-blue-600" 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> | |
| </div> | |
| <div class="flex-1 max-w-2xl"> | |
| <div class="bg-gray-100 px-4 py-3 rounded-lg text-sm leading-relaxed text-gray-700"> | |
| <span class="streaming-text"></span> | |
| <span class="cursor">▋</span> | |
| </div> | |
| </div> | |
| `; | |
| chatbox.appendChild(messageDiv); | |
| scrollToBottom(); | |
| } | |
| // Update streaming message with new text | |
| function updateStreamingMessage(messageId, text) { | |
| const messageDiv = document.getElementById(messageId); | |
| if (messageDiv) { | |
| const textSpan = messageDiv.querySelector('.streaming-text'); | |
| const cursor = messageDiv.querySelector('.cursor'); | |
| if (textSpan) { | |
| textSpan.textContent = text; | |
| } | |
| // Remove cursor when done | |
| if (text.length > 0 && cursor && text.endsWith('.')) { | |
| setTimeout(() => cursor?.remove(), 500); | |
| } | |
| scrollToBottom(); | |
| } | |
| } | |
| // Escape HTML to prevent XSS | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // Handle form submission | |
| chatForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const message = messageInput.value.trim(); | |
| if (!message) return; | |
| // Disable input while processing | |
| messageInput.disabled = true; | |
| sendBtn.disabled = true; | |
| sendBtn.textContent = 'Sending...'; | |
| // Add user message | |
| addUserMessage(message); | |
| messageInput.value = ''; | |
| // Create streaming message container | |
| const streamingMessageId = 'streaming-' + Date.now(); | |
| createStreamingMessage(streamingMessageId); | |
| try { | |
| // Send message to backend with session ID | |
| const formData = new FormData(); | |
| formData.append('msg', message); | |
| formData.append('session_id', sessionId); | |
| const response = await fetch('/get', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| // Handle streaming response | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let accumulatedText = ''; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value, { stream: true }); | |
| const lines = chunk.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| try { | |
| const data = JSON.parse(line.slice(6)); | |
| if (data.error) { | |
| updateStreamingMessage(streamingMessageId, 'Sorry, an error occurred.'); | |
| break; | |
| } | |
| if (data.token && !data.done) { | |
| accumulatedText += data.token; | |
| updateStreamingMessage(streamingMessageId, accumulatedText); | |
| } | |
| if (data.done) { | |
| if (data.full_answer) { | |
| updateStreamingMessage(streamingMessageId, data.full_answer); | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Parse error:', e); | |
| } | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| updateStreamingMessage(streamingMessageId, 'Sorry, there was an error processing your request. Please try again.'); | |
| console.error('Error:', error); | |
| } finally { | |
| // Re-enable input | |
| messageInput.disabled = false; | |
| sendBtn.disabled = false; | |
| sendBtn.textContent = 'Send'; | |
| messageInput.focus(); | |
| } | |
| }); | |
| // Focus input on load | |
| messageInput.focus(); | |
| </script> | |
| </body> | |
| </html> |