| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8" /> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | <title>MedFound - Medical AI Assistant</title> |
| | <style> |
| | *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| | |
| | :root { |
| | --bg: #0f1117; |
| | --surface: #1a1d27; |
| | --surface2: #242736; |
| | --border: #2e3144; |
| | --text: #e4e4e7; |
| | --text-dim: #9ca3af; |
| | --accent: #3b82f6; |
| | --accent-hover: #2563eb; |
| | --user-bg: #1e3a5f; |
| | --bot-bg: #1f2937; |
| | --danger: #ef4444; |
| | --success: #22c55e; |
| | --radius: 12px; |
| | } |
| | |
| | html, body { height: 100%; } |
| | |
| | body { |
| | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | background: var(--bg); |
| | color: var(--text); |
| | display: flex; |
| | flex-direction: column; |
| | } |
| | |
| | header { |
| | display: flex; |
| | align-items: center; |
| | justify-content: space-between; |
| | padding: 16px 24px; |
| | background: var(--surface); |
| | border-bottom: 1px solid var(--border); |
| | flex-shrink: 0; |
| | } |
| | |
| | .logo { |
| | display: flex; |
| | align-items: center; |
| | gap: 12px; |
| | } |
| | |
| | .logo-icon { |
| | width: 40px; |
| | height: 40px; |
| | background: linear-gradient(135deg, var(--accent), #8b5cf6); |
| | border-radius: 10px; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | font-size: 20px; |
| | } |
| | |
| | .logo h1 { |
| | font-size: 20px; |
| | font-weight: 700; |
| | background: linear-gradient(135deg, #60a5fa, #a78bfa); |
| | -webkit-background-clip: text; |
| | -webkit-text-fill-color: transparent; |
| | } |
| | |
| | .logo span { |
| | font-size: 12px; |
| | color: var(--text-dim); |
| | } |
| | |
| | .status-badge { |
| | display: flex; |
| | align-items: center; |
| | gap: 6px; |
| | font-size: 13px; |
| | color: var(--text-dim); |
| | padding: 6px 14px; |
| | background: var(--surface2); |
| | border-radius: 20px; |
| | } |
| | |
| | .status-dot { |
| | width: 8px; |
| | height: 8px; |
| | border-radius: 50%; |
| | background: var(--danger); |
| | } |
| | |
| | .status-dot.online { background: var(--success); } |
| | |
| | .chat-area { |
| | flex: 1; |
| | overflow-y: auto; |
| | padding: 24px; |
| | display: flex; |
| | flex-direction: column; |
| | gap: 16px; |
| | } |
| | |
| | .welcome { |
| | text-align: center; |
| | padding: 60px 20px; |
| | max-width: 600px; |
| | margin: auto; |
| | } |
| | |
| | .welcome-icon { |
| | font-size: 64px; |
| | margin-bottom: 16px; |
| | } |
| | |
| | .welcome h2 { |
| | font-size: 24px; |
| | margin-bottom: 8px; |
| | color: var(--text); |
| | } |
| | |
| | .welcome p { |
| | color: var(--text-dim); |
| | line-height: 1.6; |
| | margin-bottom: 24px; |
| | } |
| | |
| | .suggestions { |
| | display: grid; |
| | grid-template-columns: 1fr 1fr; |
| | gap: 10px; |
| | } |
| | |
| | .suggestion { |
| | padding: 14px 16px; |
| | background: var(--surface2); |
| | border: 1px solid var(--border); |
| | border-radius: var(--radius); |
| | cursor: pointer; |
| | text-align: left; |
| | color: var(--text-dim); |
| | font-size: 13px; |
| | transition: all 0.15s; |
| | } |
| | |
| | .suggestion:hover { |
| | background: var(--surface); |
| | border-color: var(--accent); |
| | color: var(--text); |
| | } |
| | |
| | .message { |
| | display: flex; |
| | gap: 12px; |
| | max-width: 800px; |
| | width: 100%; |
| | margin: 0 auto; |
| | animation: fadeIn 0.3s ease; |
| | } |
| | |
| | @keyframes fadeIn { |
| | from { opacity: 0; transform: translateY(8px); } |
| | to { opacity: 1; transform: translateY(0); } |
| | } |
| | |
| | .message.user { flex-direction: row-reverse; } |
| | |
| | .avatar { |
| | width: 36px; |
| | height: 36px; |
| | border-radius: 10px; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | font-size: 16px; |
| | flex-shrink: 0; |
| | } |
| | |
| | .message.bot .avatar { background: linear-gradient(135deg, var(--accent), #8b5cf6); } |
| | .message.user .avatar { background: var(--user-bg); } |
| | |
| | .bubble { |
| | padding: 14px 18px; |
| | border-radius: var(--radius); |
| | line-height: 1.7; |
| | font-size: 14px; |
| | max-width: 70%; |
| | word-break: break-word; |
| | } |
| | |
| | .bubble ol, .bubble ul { |
| | margin: 8px 0; |
| | padding-left: 24px; |
| | } |
| | |
| | .bubble ol li, .bubble ul li { |
| | margin-bottom: 6px; |
| | } |
| | |
| | .bubble p { |
| | margin: 6px 0; |
| | } |
| | |
| | .bubble p:first-child { margin-top: 0; } |
| | .bubble p:last-child { margin-bottom: 0; } |
| | |
| | .bubble strong { color: #93c5fd; font-weight: 600; } |
| | |
| | .bubble code { |
| | background: rgba(255,255,255,0.08); |
| | padding: 2px 6px; |
| | border-radius: 4px; |
| | font-size: 13px; |
| | } |
| | |
| | .bubble pre { |
| | background: rgba(0,0,0,0.3); |
| | padding: 12px; |
| | border-radius: 8px; |
| | overflow-x: auto; |
| | margin: 8px 0; |
| | } |
| | |
| | .bubble pre code { |
| | background: none; |
| | padding: 0; |
| | } |
| | |
| | .bubble h3, .bubble h4 { |
| | margin: 12px 0 6px; |
| | color: #93c5fd; |
| | } |
| | |
| | .message.user .bubble { white-space: pre-wrap; } |
| | |
| | .message.bot .bubble { background: var(--bot-bg); border: 1px solid var(--border); } |
| | .message.user .bubble { background: var(--user-bg); } |
| | |
| | .bubble .typing-dots span { |
| | display: inline-block; |
| | width: 7px; |
| | height: 7px; |
| | margin: 0 2px; |
| | border-radius: 50%; |
| | background: var(--text-dim); |
| | animation: bounce 1.4s infinite ease-in-out both; |
| | } |
| | |
| | .bubble .typing-dots span:nth-child(1) { animation-delay: -0.32s; } |
| | .bubble .typing-dots span:nth-child(2) { animation-delay: -0.16s; } |
| | |
| | @keyframes bounce { |
| | 0%, 80%, 100% { transform: scale(0); } |
| | 40% { transform: scale(1); } |
| | } |
| | |
| | .input-area { |
| | padding: 16px 24px 24px; |
| | background: var(--surface); |
| | border-top: 1px solid var(--border); |
| | flex-shrink: 0; |
| | } |
| | |
| | .input-wrap { |
| | display: flex; |
| | gap: 10px; |
| | max-width: 800px; |
| | margin: 0 auto; |
| | } |
| | |
| | .input-wrap textarea { |
| | flex: 1; |
| | resize: none; |
| | border: 1px solid var(--border); |
| | background: var(--surface2); |
| | color: var(--text); |
| | border-radius: var(--radius); |
| | padding: 14px 18px; |
| | font-size: 14px; |
| | font-family: inherit; |
| | line-height: 1.5; |
| | outline: none; |
| | transition: border-color 0.15s; |
| | min-height: 52px; |
| | max-height: 160px; |
| | } |
| | |
| | .input-wrap textarea:focus { border-color: var(--accent); } |
| | .input-wrap textarea::placeholder { color: var(--text-dim); } |
| | |
| | .input-wrap button { |
| | padding: 14px 20px; |
| | background: var(--accent); |
| | color: #fff; |
| | border: none; |
| | border-radius: var(--radius); |
| | font-size: 14px; |
| | font-weight: 600; |
| | cursor: pointer; |
| | transition: background 0.15s; |
| | display: flex; |
| | align-items: center; |
| | gap: 6px; |
| | white-space: nowrap; |
| | } |
| | |
| | .input-wrap button:hover { background: var(--accent-hover); } |
| | .input-wrap button:disabled { opacity: 0.5; cursor: not-allowed; } |
| | |
| | .controls { |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | max-width: 800px; |
| | margin: 8px auto 0; |
| | } |
| | |
| | .disclaimer { |
| | font-size: 11px; |
| | color: var(--text-dim); |
| | } |
| | |
| | .clear-btn { |
| | background: none; |
| | border: 1px solid var(--border); |
| | color: var(--text-dim); |
| | padding: 5px 12px; |
| | border-radius: 8px; |
| | font-size: 12px; |
| | cursor: pointer; |
| | transition: all 0.15s; |
| | } |
| | |
| | .clear-btn:hover { border-color: var(--danger); color: var(--danger); } |
| | |
| | @media (max-width: 640px) { |
| | .suggestions { grid-template-columns: 1fr; } |
| | .bubble { max-width: 85%; } |
| | header { padding: 12px 16px; } |
| | .chat-area { padding: 16px; } |
| | .input-area { padding: 12px 16px 16px; } |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <header> |
| | <div class="logo"> |
| | <div class="logo-icon">⚕</div> |
| | <div> |
| | <h1>MedFound AI</h1> |
| | <span>Medical Assistant · Llama3-8B</span> |
| | </div> |
| | </div> |
| | <div class="status-badge"> |
| | <div class="status-dot" id="statusDot"></div> |
| | <span id="statusText">Connecting...</span> |
| | </div> |
| | </header> |
| |
|
| | <div class="chat-area" id="chatArea"> |
| | <div class="welcome" id="welcome"> |
| | <div class="welcome-icon">🏥</div> |
| | <h2>Medical AI Assistant</h2> |
| | <p>Ask me about symptoms, conditions, medications, or general health information. |
| | Responses are for informational purposes only—always consult a healthcare professional.</p> |
| | <div class="suggestions"> |
| | <div class="suggestion" onclick="useSuggestion(this)">What are common symptoms of type 2 diabetes?</div> |
| | <div class="suggestion" onclick="useSuggestion(this)">Explain the difference between viral and bacterial infections</div> |
| | <div class="suggestion" onclick="useSuggestion(this)">What are the risk factors for cardiovascular disease?</div> |
| | <div class="suggestion" onclick="useSuggestion(this)">How does hypertension affect the body over time?</div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div class="input-area"> |
| | <div class="input-wrap"> |
| | <textarea id="msgInput" rows="1" placeholder="Describe your symptoms or ask a medical question..." |
| | onkeydown="handleKey(event)" oninput="autoGrow(this)"></textarea> |
| | <button id="sendBtn" onclick="sendMessage()">Send ➤</button> |
| | </div> |
| | <div class="controls"> |
| | <span class="disclaimer">⚠ Not a substitute for professional medical advice.</span> |
| | <button class="clear-btn" onclick="clearChat()">Clear chat</button> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | const chatArea = document.getElementById('chatArea'); |
| | const msgInput = document.getElementById('msgInput'); |
| | const sendBtn = document.getElementById('sendBtn'); |
| | const statusDot = document.getElementById('statusDot'); |
| | const statusText= document.getElementById('statusText'); |
| | const welcome = document.getElementById('welcome'); |
| | |
| | let history = []; |
| | let busy = false; |
| | |
| | async function checkHealth() { |
| | try { |
| | const r = await fetch('/health'); |
| | const d = await r.json(); |
| | statusDot.classList.toggle('online', d.status === 'ok'); |
| | statusText.textContent = d.status === 'ok' |
| | ? `Online \u2022 GPU ${d.gpu_memory_used_mb} MB` |
| | : 'Error'; |
| | } catch { |
| | statusDot.classList.remove('online'); |
| | statusText.textContent = 'Offline'; |
| | } |
| | } |
| | checkHealth(); |
| | setInterval(checkHealth, 15000); |
| | |
| | function useSuggestion(el) { |
| | msgInput.value = el.textContent; |
| | msgInput.focus(); |
| | autoGrow(msgInput); |
| | } |
| | |
| | function handleKey(e) { |
| | if (e.key === 'Enter' && !e.shiftKey) { |
| | e.preventDefault(); |
| | sendMessage(); |
| | } |
| | } |
| | |
| | function autoGrow(el) { |
| | el.style.height = 'auto'; |
| | el.style.height = Math.min(el.scrollHeight, 160) + 'px'; |
| | } |
| | |
| | function appendMessage(role, content) { |
| | if (welcome) welcome.style.display = 'none'; |
| | const div = document.createElement('div'); |
| | div.className = `message ${role}`; |
| | const avatarChar = role === 'user' ? '\u{1F464}' : '\u2695'; |
| | const rendered = role === 'bot' ? renderMarkdown(content) : escapeHtml(content); |
| | div.innerHTML = ` |
| | <div class="avatar">${avatarChar}</div> |
| | <div class="bubble">${rendered}</div>`; |
| | chatArea.appendChild(div); |
| | chatArea.scrollTop = chatArea.scrollHeight; |
| | return div; |
| | } |
| | |
| | function showTyping() { |
| | if (welcome) welcome.style.display = 'none'; |
| | const div = document.createElement('div'); |
| | div.className = 'message bot'; |
| | div.id = 'typing'; |
| | div.innerHTML = ` |
| | <div class="avatar">\u2695</div> |
| | <div class="bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div>`; |
| | chatArea.appendChild(div); |
| | chatArea.scrollTop = chatArea.scrollHeight; |
| | } |
| | |
| | function removeTyping() { |
| | const t = document.getElementById('typing'); |
| | if (t) t.remove(); |
| | } |
| | |
| | function escapeHtml(s) { |
| | const d = document.createElement('div'); |
| | d.textContent = s; |
| | return d.innerHTML; |
| | } |
| | |
| | function renderMarkdown(text) { |
| | let html = escapeHtml(text); |
| | |
| | |
| | html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>'); |
| | |
| | html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); |
| | |
| | html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); |
| | |
| | html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); |
| | |
| | html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>'); |
| | html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>'); |
| | |
| | |
| | html = html.replace(/((?:^\d+[\.\)]\s+.+$\n?)+)/gm, function(block) { |
| | const items = block.trim().split('\n').map(line => |
| | '<li>' + line.replace(/^\d+[\.\)]\s+/, '') + '</li>' |
| | ).join(''); |
| | return '<ol>' + items + '</ol>'; |
| | }); |
| | |
| | |
| | html = html.replace(/((?:^[\-\*]\s+.+$\n?)+)/gm, function(block) { |
| | const items = block.trim().split('\n').map(line => |
| | '<li>' + line.replace(/^[\-\*]\s+/, '') + '</li>' |
| | ).join(''); |
| | return '<ul>' + items + '</ul>'; |
| | }); |
| | |
| | |
| | html = html.replace(/\n{2,}/g, '</p><p>'); |
| | |
| | html = html.replace(/\n/g, '<br>'); |
| | |
| | html = html.replace(/(<\/?(ol|ul|li|h[34]|pre|p)>)\s*<br>/g, '$1'); |
| | html = html.replace(/<br>\s*(<(ol|ul|li|h[34]|pre|p)[ >])/g, '$1'); |
| | |
| | |
| | if (!/^\s*<(ol|ul|h[34]|pre|p)/.test(html)) { |
| | html = '<p>' + html + '</p>'; |
| | } |
| | |
| | return html; |
| | } |
| | |
| | async function sendMessage() { |
| | const text = msgInput.value.trim(); |
| | if (!text || busy) return; |
| | |
| | busy = true; |
| | sendBtn.disabled = true; |
| | msgInput.value = ''; |
| | msgInput.style.height = 'auto'; |
| | |
| | appendMessage('user', text); |
| | history.push({ role: 'user', content: text }); |
| | |
| | showTyping(); |
| | |
| | try { |
| | const resp = await fetch('/v1/chat', { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | messages: history, |
| | max_new_tokens: 512, |
| | temperature: 0.7, |
| | stream: false |
| | }) |
| | }); |
| | |
| | if (!resp.ok) throw new Error(`HTTP ${resp.status}`); |
| | const data = await resp.json(); |
| | |
| | removeTyping(); |
| | appendMessage('bot', data.content); |
| | history.push({ role: 'assistant', content: data.content }); |
| | } catch (err) { |
| | removeTyping(); |
| | appendMessage('bot', `Error: ${err.message}. Please try again.`); |
| | } |
| | |
| | busy = false; |
| | sendBtn.disabled = false; |
| | msgInput.focus(); |
| | } |
| | |
| | function clearChat() { |
| | history = []; |
| | chatArea.innerHTML = ''; |
| | if (welcome) { |
| | welcome.style.display = ''; |
| | chatArea.appendChild(welcome); |
| | } |
| | } |
| | </script> |
| | </body> |
| | </html> |
| |
|