antern-bot / static /index.html
johnpitteera's picture
Upload folder using huggingface_hub
2dd2de0 verified
Raw
History Blame Contribute Delete
8.65 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Antern Bot</title>
<style>
:root {
--bg: #0f1115; --panel: #171a21; --panel2: #1f242e; --border: #2a303c;
--text: #e8eaed; --muted: #9aa3b2; --accent: #4f8cff; --user: #2563eb;
--bot: #232936; --danger: #ff6b6b; --code: #11151c;
}
* { box-sizing: border-box; }
body {
margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column;
}
header {
padding: 14px 20px; background: var(--panel); border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 12px;
}
header .logo { width: 30px; height: 30px; border-radius: 8px; background: linear-gradient(135deg, #4f8cff, #8a5cff); display: flex; align-items: center; justify-content: center; font-weight: 700; }
header h1 { font-size: 16px; margin: 0; }
header .sub { font-size: 12px; color: var(--muted); margin-left: 2px; }
header .spacer { flex: 1; }
header button { background: var(--panel2); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 7px 12px; cursor: pointer; font-size: 13px; }
header button:hover { border-color: var(--accent); }
#status { font-size: 12px; color: var(--muted); }
#status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); margin-right: 5px; }
#status.ok .dot { background: #38d39f; }
#status.bad .dot { background: var(--danger); }
main { flex: 1; overflow-y: auto; padding: 24px 0; }
.wrap { max-width: 820px; margin: 0 auto; padding: 0 20px; }
.msg { display: flex; margin-bottom: 18px; gap: 12px; }
.msg .avatar { width: 30px; height: 30px; border-radius: 8px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; }
.msg.user .avatar { background: var(--user); }
.msg.bot .avatar { background: linear-gradient(135deg, #4f8cff, #8a5cff); }
.bubble { background: var(--bot); border: 1px solid var(--border); border-radius: 12px; padding: 12px 15px; line-height: 1.55; white-space: pre-wrap; word-wrap: break-word; max-width: 100%; }
.msg.user .bubble { background: var(--panel2); }
.queries { margin-top: 10px; }
.queries summary { cursor: pointer; color: var(--muted); font-size: 12px; }
.queries pre { background: var(--code); border: 1px solid var(--border); border-radius: 8px; padding: 10px; overflow-x: auto; font-size: 12.5px; margin: 8px 0 0; color: #cbd5e1; }
.queries .meta { font-size: 11.5px; color: var(--muted); margin-top: 4px; }
.queries .err { color: var(--danger); }
.typing { color: var(--muted); font-style: italic; }
footer { border-top: 1px solid var(--border); background: var(--panel); padding: 14px 0; }
form { max-width: 820px; margin: 0 auto; padding: 0 20px; display: flex; gap: 10px; }
textarea { flex: 1; resize: none; background: var(--panel2); color: var(--text); border: 1px solid var(--border); border-radius: 10px; padding: 11px 13px; font-size: 14px; font-family: inherit; max-height: 160px; }
textarea:focus { outline: none; border-color: var(--accent); }
form button { background: var(--accent); color: #fff; border: none; border-radius: 10px; padding: 0 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
form button:disabled { opacity: 0.5; cursor: not-allowed; }
.hint { max-width: 820px; margin: 8px auto 0; padding: 0 20px; font-size: 11.5px; color: var(--muted); }
</style>
</head>
<body>
<header>
<div class="logo">A</div>
<div>
<h1>Antern Bot</h1>
<div class="sub">Ask about the IAmInterviewed_QA database in plain English</div>
</div>
<div class="spacer"></div>
<div id="status"><span class="dot"></span><span id="status-text">connecting…</span></div>
<button id="reset">New chat</button>
</header>
<main id="main">
<div class="wrap" id="messages"></div>
</main>
<footer>
<form id="form">
<textarea id="input" rows="1" placeholder="e.g. How many active candidates are there?" autocomplete="off"></textarea>
<button type="submit" id="send">Send</button>
</form>
<div class="hint">Read-only — the bot can only run SELECT queries. Press Enter to send, Shift+Enter for a new line.</div>
</footer>
<script>
const messagesEl = document.getElementById('messages');
const form = document.getElementById('form');
const input = document.getElementById('input');
const sendBtn = document.getElementById('send');
const resetBtn = document.getElementById('reset');
const statusEl = document.getElementById('status');
const statusText = document.getElementById('status-text');
let sessionId = null;
function esc(s) {
return (s ?? '').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
}
function addMessage(role, text) {
const msg = document.createElement('div');
msg.className = 'msg ' + role;
const avatar = role === 'user' ? 'You' : 'A';
msg.innerHTML = `<div class="avatar">${avatar}</div><div class="bubble"></div>`;
msg.querySelector('.bubble').textContent = text;
messagesEl.appendChild(msg);
scroll();
return msg;
}
function renderQueries(bubble, queries) {
if (!queries || !queries.length) return;
const det = document.createElement('details');
det.className = 'queries';
let inner = `<summary>${queries.length} quer${queries.length===1?'y':'ies'} run</summary>`;
for (const q of queries) {
inner += `<pre>${esc(q.query)}</pre>`;
if (q.error) {
inner += `<div class="meta err">${esc(q.error)}</div>`;
} else {
inner += `<div class="meta">${q.row_count} row(s)${q.truncated ? ' (truncated)' : ''}</div>`;
}
}
det.innerHTML = inner;
bubble.appendChild(det);
}
function scroll() { document.getElementById('main').scrollTop = document.getElementById('main').scrollHeight; }
async function checkHealth() {
try {
const r = await fetch('/api/health');
const d = await r.json();
if (d.ready) {
statusEl.className = 'ok';
statusText.textContent = d.model || 'ready';
} else {
statusEl.className = 'bad';
statusText.textContent = d.error || 'not ready';
}
} catch {
statusEl.className = 'bad';
statusText.textContent = 'offline';
}
}
async function send(text) {
addMessage('user', text);
sendBtn.disabled = true;
const botMsg = addMessage('bot', '');
const bubble = botMsg.querySelector('.bubble');
bubble.innerHTML = '<span class="typing">Antern Bot is thinking…</span>';
try {
const r = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, session_id: sessionId }),
});
const d = await r.json();
if (!r.ok) {
bubble.innerHTML = `<span class="typing">⚠ ${esc(d.error || 'Error')}</span>`;
} else {
sessionId = d.session_id;
bubble.textContent = d.answer || '(no answer)';
renderQueries(bubble, d.queries);
}
} catch (e) {
bubble.innerHTML = `<span class="typing">⚠ Network error: ${esc(e.message)}</span>`;
} finally {
sendBtn.disabled = false;
scroll();
}
}
form.addEventListener('submit', e => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
input.value = '';
input.style.height = 'auto';
send(text);
});
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); form.requestSubmit(); }
});
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 160) + 'px';
});
resetBtn.addEventListener('click', async () => {
if (sessionId) {
try { await fetch('/api/reset', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ session_id: sessionId }) }); } catch {}
}
sessionId = null;
messagesEl.innerHTML = '';
addMessage('bot', "Hi! I'm Antern Bot. Ask me anything about the recruitment data — candidates, requirements, applications, interviews, skills, and more.");
});
checkHealth();
addMessage('bot', "Hi! I'm Antern Bot. Ask me anything about the recruitment data — candidates, requirements, applications, interviews, skills, and more.");
</script>
</body>
</html>