server / dashboard /index.html
Harmony18090's picture
Upload dashboard/index.html with huggingface_hub
68993d0 verified
<!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">&#x2695;</div>
<div>
<h1>MedFound AI</h1>
<span>Medical Assistant &middot; 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">&#x1F3E5;</div>
<h2>Medical AI Assistant</h2>
<p>Ask me about symptoms, conditions, medications, or general health information.
Responses are for informational purposes only&mdash;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 &#x27A4;</button>
</div>
<div class="controls">
<span class="disclaimer">&#x26A0; 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);
// Code blocks: ```...```
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Headings (### and ####)
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
// Numbered lists: lines starting with 1. 2. etc.
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>';
});
// Bullet lists: lines starting with - or *
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>';
});
// Paragraphs: split on double newlines
html = html.replace(/\n{2,}/g, '</p><p>');
// Single newlines (not inside lists/pre) become <br>
html = html.replace(/\n/g, '<br>');
// Clean up: remove <br> right after block elements
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');
// Wrap in paragraph if not starting with a block element
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>