Spaces:
Sleeping
Sleeping
| <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 => ({'&':'&','<':'<','>':'>','"':'"'}[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> | |