Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Antaram · Agentic SLM</title> | |
| <style> | |
| :root{ | |
| --bg:#0b0f16; --panel:#10151f; --panel2:#1a2130; --border:#28313f; | |
| --text:#e6edf3; --muted:#8b949e; --accent:#3b82f6; --accent2:#22c55e; | |
| --user:#1f6feb; --assist:#1a2130; --danger:#f04747; --warn:#e3a008; | |
| } | |
| *{box-sizing:border-box} | |
| body{margin:0;font-family:'Segoe UI',system-ui,sans-serif;background: | |
| radial-gradient(1200px 600px at 80% -10%,rgba(59,130,246,.10),transparent), | |
| radial-gradient(1000px 500px at 0% 110%,rgba(34,197,94,.08),transparent),var(--bg); | |
| color:var(--text);height:100vh;display:flex;flex-direction:column} | |
| header{display:flex;align-items:center;gap:12px;padding:12px 18px;background:rgba(16,21,31,.85);backdrop-filter:blur(8px);border-bottom:1px solid var(--border)} | |
| header h1{font-size:17px;margin:0;font-weight:800;letter-spacing:.2px;background:linear-gradient(90deg,#3b82f6,#22c55e);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent} | |
| header .brand{display:flex;flex-direction:column;line-height:1.15;user-select:none} | |
| header .tag{font-size:10.5px;color:var(--muted);font-weight:400} | |
| header .dot{width:9px;height:9px;border-radius:50%;background:#6e7681;box-shadow:0 0 0 0 rgba(34,197,94,.5);transition:.3s} | |
| header .dot.ok{background:var(--accent2);box-shadow:0 0 10px 1px rgba(34,197,94,.5)} | |
| header .spacer{flex:1} | |
| .pill{font-size:11px;color:var(--muted);background:var(--panel2);border:1px solid var(--border);padding:4px 9px;border-radius:20px;white-space:nowrap} | |
| button{font-family:inherit;cursor:pointer;border:1px solid var(--border);background:var(--panel2);color:var(--text);border-radius:8px;padding:6px 12px;font-size:13px;transition:.15s} | |
| button:hover{border-color:var(--accent)} | |
| button.primary{background:var(--accent);border-color:var(--accent);color:#fff} | |
| button.primary:hover{filter:brightness(1.1)} | |
| button.danger{border-color:var(--danger);color:#ffb4b4} | |
| button.danger:hover{background:rgba(240,71,71,.12)} | |
| button:disabled{opacity:.5;cursor:not-allowed} | |
| #keyInput{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:6px 10px;font-size:12px;width:150px} | |
| #keyInput:focus{outline:none;border-color:var(--accent)} | |
| .usage-panel{position:fixed;top:58px;right:18px;background:var(--panel2);border:1px solid var(--border);border-radius:12px;padding:12px 14px;font-size:12px;display:none;z-index:20;min-width:230px;box-shadow:0 12px 40px rgba(0,0,0,.5)} | |
| .usage-panel.open{display:block} | |
| .usage-panel .r{display:flex;justify-content:space-between;gap:18px;padding:3px 0;color:var(--muted)} | |
| .usage-panel .r b{color:var(--text)} | |
| #chat{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:16px;scroll-behavior:smooth} | |
| #chat::-webkit-scrollbar{width:9px}#chat::-webkit-scrollbar-thumb{background:#2b3542;border-radius:8px} | |
| .welcome{margin:auto;text-align:center;color:var(--muted);max-width:440px} | |
| .welcome h2{color:var(--text);font-weight:700;margin:0 0 6px} | |
| .welcome .chips{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:16px} | |
| .welcome .chip{background:var(--panel2);border:1px solid var(--border);border-radius:20px;padding:7px 13px;font-size:12.5px;cursor:pointer} | |
| .welcome .chip:hover{border-color:var(--accent);color:var(--text)} | |
| .msg{max-width:78%;display:flex;flex-direction:column;gap:6px;animation:rise .25s ease} | |
| @keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}} | |
| .msg.user{align-self:flex-end;align-items:flex-end} | |
| .msg.assistant{align-self:flex-start} | |
| .bubble{padding:11px 14px;border-radius:14px;line-height:1.55;word-wrap:break-word;font-size:14px} | |
| .user .bubble{background:var(--user);color:#fff;border-bottom-right-radius:4px;white-space:pre-wrap} | |
| .assistant .bubble{background:var(--assist);border:1px solid var(--border);border-bottom-left-radius:4px} | |
| .bubble pre{background:#0b0f16;border:1px solid var(--border);border-radius:8px;padding:10px;overflow-x:auto;margin:8px 0} | |
| .bubble code{background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px;font-size:12.5px;font-family:'Cascadia Code',Consolas,monospace} | |
| .bubble pre code{background:none;padding:0} | |
| .bubble a{color:#79c0ff} | |
| .row{display:flex;align-items:center;gap:7px} | |
| .assistant .row{align-items:flex-start} | |
| .iconbtn{width:24px;height:24px;border-radius:50%;display:grid;place-items:center;font-size:12px;padding:0;line-height:1;flex:none;color:var(--muted)} | |
| .iconbtn:hover{color:var(--text)} | |
| .meta{margin-top:2px;background:var(--panel2);border:1px solid var(--border);border-radius:10px;padding:10px 12px;font-size:12px;display:none;grid-template-columns:1fr 1fr;gap:8px 16px;max-width:430px} | |
| .meta.open{display:grid} | |
| .meta .item{display:flex;align-items:center;gap:7px;color:var(--muted)} | |
| .meta .item b{color:var(--text);font-weight:600} | |
| .meta .full{grid-column:1 / -1} | |
| .ico{font-size:14px} | |
| .ctxbar{height:7px;background:var(--bg);border:1px solid var(--border);border-radius:4px;overflow:hidden;width:100%;margin-top:2px} | |
| .ctxbar-fill{height:100%;background:linear-gradient(90deg,#3b82f6,#22c55e);transition:width .3s} | |
| .typing .bubble{color:var(--muted)} | |
| .dots span{display:inline-block;width:6px;height:6px;margin:0 1px;border-radius:50%;background:var(--muted);animation:blink 1.2s infinite both} | |
| .dots span:nth-child(2){animation-delay:.2s}.dots span:nth-child(3){animation-delay:.4s} | |
| @keyframes blink{0%,80%,100%{opacity:.2}40%{opacity:1}} | |
| footer{padding:14px 18px;background:rgba(16,21,31,.85);backdrop-filter:blur(8px);border-top:1px solid var(--border)} | |
| .inputbar{display:flex;gap:10px;align-items:flex-end;max-width:900px;margin:0 auto} | |
| textarea{flex:1;resize:none;background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:10px;padding:11px 13px;font-family:inherit;font-size:14px;max-height:140px;line-height:1.4} | |
| textarea:focus{outline:none;border-color:var(--accent)} | |
| .hint{font-size:11px;color:var(--muted);text-align:center;margin-top:8px} | |
| /* ---------- hidden admin console ---------- */ | |
| .overlay{position:fixed;inset:0;background:rgba(3,6,12,.72);backdrop-filter:blur(4px);z-index:100;display:none;align-items:flex-start;justify-content:center;padding:40px 16px;overflow-y:auto} | |
| .overlay.open{display:flex} | |
| .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;width:100%;max-width:760px;box-shadow:0 24px 80px rgba(0,0,0,.6)} | |
| .card .head{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--border)} | |
| .card .head h2{margin:0;font-size:16px;font-weight:700} | |
| .card .head .spacer{flex:1} | |
| .card .body{padding:18px 20px} | |
| .login{max-width:360px;margin:8px auto 4px} | |
| label{display:block;font-size:12px;color:var(--muted);margin:12px 0 5px} | |
| .fld{width:100%;background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:9px 11px;font-size:13px;font-family:inherit} | |
| .fld:focus{outline:none;border-color:var(--accent)} | |
| textarea.fld{resize:vertical;min-height:90px;line-height:1.45} | |
| .grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px 14px} | |
| .err{color:#ffb4b4;font-size:12.5px;margin-top:10px;min-height:16px} | |
| .tabs{display:flex;gap:6px;padding:0 20px;border-bottom:1px solid var(--border);background:var(--panel)} | |
| .tab{background:none;border:none;border-bottom:2px solid transparent;border-radius:0;color:var(--muted);padding:11px 8px;font-size:13px} | |
| .tab.active{color:var(--text);border-bottom-color:var(--accent)} | |
| .tab:hover{color:var(--text)} | |
| .keyreveal{background:#0b1a10;border:1px solid #1f6f3f;border-radius:10px;padding:12px;margin-top:14px;font-size:13px} | |
| .keyreveal code{display:block;background:#06120a;border:1px solid #1f6f3f;border-radius:6px;padding:9px;word-break:break-all;color:#7ee787;margin:6px 0} | |
| .sendmsg{white-space:pre-wrap;background:var(--panel2);border:1px solid var(--border);border-radius:8px;padding:10px;font-size:12.5px;color:var(--text);margin-top:8px} | |
| table{width:100%;border-collapse:collapse;font-size:12.5px;margin-top:6px} | |
| th,td{text-align:left;padding:8px 8px;border-bottom:1px solid var(--border);vertical-align:middle} | |
| th{color:var(--muted);font-weight:600} | |
| .badge{display:inline-block;font-size:10.5px;padding:2px 7px;border-radius:20px;border:1px solid var(--border);color:var(--muted)} | |
| .badge.live{color:#7ee787;border-color:#1f6f3f} | |
| .badge.dead{color:#ffb4b4;border-color:#5f2330} | |
| .badge.adm{color:#ffd479;border-color:#6b5314} | |
| .badge.ag{color:#79c0ff;border-color:#23456b} | |
| .rowacts{display:flex;gap:6px} | |
| .tiny{font-size:11px;padding:4px 8px} | |
| .muted{color:var(--muted)} | |
| .scrollx{overflow-x:auto} | |
| @media(max-width:560px){.grid2{grid-template-columns:1fr}.msg{max-width:90%}} | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <span id="dot" class="dot"></span> | |
| <div class="brand" id="brand"> | |
| <h1>Antaram</h1> | |
| <span class="tag">SLM for agentic · by Team Triangle</span> | |
| </div> | |
| <span class="pill" id="modelPill">Antaram</span> | |
| <span class="pill" id="sessPill">session: —</span> | |
| <div class="spacer"></div> | |
| <button id="usageBtn" title="Usage stats">📊 Usage</button> | |
| <input id="keyInput" placeholder="API key" /> | |
| <button id="newBtn">+ New chat</button> | |
| </header> | |
| <div id="usagePanel" class="usage-panel"></div> | |
| <div id="chat"></div> | |
| <footer> | |
| <div class="inputbar"> | |
| <button id="attachBtn" title="Attach 1 image">📎</button> | |
| <input type="file" id="fileInput" accept="image/*" style="display:none" /> | |
| <textarea id="input" rows="1" placeholder="Type a message… (Enter to send, Shift+Enter for newline)"></textarea> | |
| <button id="sendBtn" class="primary">Send</button> | |
| </div> | |
| <div id="attachInfo" class="hint" style="display:none;color:var(--accent2)"></div> | |
| <div class="hint">📎 attach 1 image · tap ⓘ on a reply for tokens, time & context. Token counts are estimates.</div> | |
| </footer> | |
| <!-- ======== HIDDEN ADMIN CONSOLE (Ctrl+Shift+A or #admin) ======== --> | |
| <div id="adminOverlay" class="overlay"> | |
| <div class="card"> | |
| <div class="head"> | |
| <span style="font-size:18px">🛡️</span> | |
| <h2 id="adminTitle">Admin Console</h2> | |
| <div class="spacer"></div> | |
| <button id="adminLogout" class="tiny" style="display:none">Logout</button> | |
| <button id="adminClose" class="tiny">✕ Close</button> | |
| </div> | |
| <!-- login view --> | |
| <div id="adminLogin" class="body"> | |
| <div class="login"> | |
| <label>Username</label> | |
| <input id="alUser" class="fld" autocomplete="off" /> | |
| <label>Password</label> | |
| <input id="alPass" class="fld" type="password" /> | |
| <div class="err" id="alErr"></div> | |
| <button id="alBtn" class="primary" style="width:100%;margin-top:10px">Sign in</button> | |
| </div> | |
| </div> | |
| <!-- authed view --> | |
| <div id="adminPanel" style="display:none"> | |
| <div class="tabs"> | |
| <button class="tab active" data-tab="issue">Issue key</button> | |
| <button class="tab" data-tab="users">Users & keys</button> | |
| <button class="tab" data-tab="sessions">Sessions</button> | |
| <button class="tab" data-tab="prompt">Global prompt</button> | |
| </div> | |
| <!-- issue key --> | |
| <div class="body tabpane" data-pane="issue"> | |
| <div class="grid2"> | |
| <div> | |
| <label>User name / label</label> | |
| <input id="ikName" class="fld" placeholder="e.g. Riya — Marketing" /> | |
| </div> | |
| <div> | |
| <label>Role</label> | |
| <select id="ikRole" class="fld"> | |
| <option value="user">user (chat + web search + image)</option> | |
| <option value="superadmin">superadmin (full access)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Expires in (days · 0 = lifetime)</label> | |
| <input id="ikTtl" class="fld" type="number" min="0" value="14" /> | |
| </div> | |
| <div> | |
| <label>Max sessions (blank = unlimited)</label> | |
| <input id="ikMax" class="fld" type="number" min="1" placeholder="unlimited" /> | |
| </div> | |
| </div> | |
| <label>AGENTS.md — per-user instructions (optional)</label> | |
| <div id="ikTmpl" style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:7px"> | |
| <span class="muted" style="font-size:11.5px;align-self:center">Templates:</span> | |
| <button type="button" class="tiny" data-tmpl="friendly">😊 Friendly</button> | |
| <button type="button" class="tiny" data-tmpl="professional">💼 Professional</button> | |
| <button type="button" class="tiny" data-tmpl="concise">⚡ Concise expert</button> | |
| <button type="button" class="tiny" data-tmpl="support">🎧 Support agent</button> | |
| <button type="button" class="tiny" data-tmpl="clear">✕ Clear</button> | |
| </div> | |
| <textarea id="ikAgents" class="fld" placeholder="# Persona / rules for THIS user only - Always answer in formal English. - You are Riya's research assistant; focus on marketing. - Never discuss topics outside marketing. Tip: pick a template above — it fills in using the name you typed."></textarea> | |
| <div class="err" id="ikErr"></div> | |
| <button id="ikBtn" class="primary" style="margin-top:12px">Create user & reveal key</button> | |
| <div id="ikReveal"></div> | |
| </div> | |
| <!-- users list --> | |
| <div class="body tabpane" data-pane="users" style="display:none"> | |
| <div style="display:flex;align-items:center;gap:10px"> | |
| <b style="font-size:13px">Issued keys</b> | |
| <span class="muted" id="ukCount"></span> | |
| <div class="spacer" style="flex:1"></div> | |
| <button id="ukRefresh" class="tiny">↻ Refresh</button> | |
| </div> | |
| <div class="scrollx"><table id="ukTable"></table></div> | |
| <!-- inline AGENTS.md editor --> | |
| <div id="agEditor" style="display:none;margin-top:14px;border-top:1px solid var(--border);padding-top:12px"> | |
| <b style="font-size:13px">Edit AGENTS.md — <span id="agName" class="muted"></span></b> | |
| <textarea id="agText" class="fld" style="margin-top:8px"></textarea> | |
| <div class="err" id="agErr"></div> | |
| <div style="display:flex;gap:8px;margin-top:8px"> | |
| <button id="agSave" class="primary tiny">Save</button> | |
| <button id="agCancel" class="tiny">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- sessions --> | |
| <div class="body tabpane" data-pane="sessions" style="display:none"> | |
| <div style="display:flex;align-items:center;gap:10px"> | |
| <b style="font-size:13px">All sessions</b> | |
| <span class="muted" id="ssCount"></span> | |
| <div class="spacer" style="flex:1"></div> | |
| <button id="ssRefresh" class="tiny">↻ Refresh</button> | |
| </div> | |
| <div class="scrollx"><table id="ssTable"></table></div> | |
| </div> | |
| <!-- global prompt --> | |
| <div class="body tabpane" data-pane="prompt" style="display:none"> | |
| <label>Global system prompt (applies to every user; hidden from end users)</label> | |
| <textarea id="spText" class="fld" style="min-height:200px"></textarea> | |
| <div class="err" id="spErr"></div> | |
| <div style="display:flex;gap:8px;margin-top:10px;align-items:center"> | |
| <button id="spSave" class="primary tiny">Save prompt</button> | |
| <button id="spReload" class="tiny">Reload</button> | |
| <span class="muted" id="spState"></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ===================== CHAT ===================== | |
| const state = { messages: [], busy:false, sessionId: newId(), ctxPct:null }; | |
| function newId(){ return 'sess-' + Math.random().toString(36).slice(2,10); } | |
| let pendingImg = null; | |
| const chatEl = document.getElementById('chat'); | |
| const inputEl = document.getElementById('input'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const sessPill = document.getElementById('sessPill'); | |
| const dot = document.getElementById('dot'); | |
| function esc(s){return (s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));} | |
| function mdInline(t){ | |
| t = esc(t); | |
| t = t.replace(/`([^`\n]+)`/g,'<code>$1</code>'); | |
| t = t.replace(/\*\*([^*\n]+)\*\*/g,'<strong>$1</strong>'); | |
| t = t.replace(/(^|[^*])\*([^*\n]+)\*/g,'$1<em>$2</em>'); | |
| t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>'); | |
| t = t.replace(/^\s*#{1,3}\s+(.*)$/gm,'<strong>$1</strong>'); | |
| t = t.replace(/^\s*[-*]\s+(.*)$/gm,'• $1'); | |
| return t.replace(/\n/g,'<br>'); | |
| } | |
| function mdRender(raw){ | |
| const parts = (raw||'').split('```'); | |
| let html=''; | |
| for(let i=0;i<parts.length;i++){ | |
| if(i % 2 === 1){ | |
| const code = parts[i].replace(/^\w*\n/,'').replace(/\n$/,''); | |
| html += '<pre><code>'+esc(code)+'</code></pre>'; | |
| } else { | |
| html += mdInline(parts[i]); | |
| } | |
| } | |
| return html; | |
| } | |
| const SUGGESTS = ['What can you do?','Search the latest AI news','Summarise quantum computing','Plan a 3-day trip to Goa']; | |
| function render(){ | |
| sessPill.textContent = 'session: ' + state.sessionId + | |
| (state.ctxPct != null ? ' · ' + state.ctxPct + '% ctx' : ''); | |
| chatEl.innerHTML = ''; | |
| if (!state.messages.length){ | |
| const w = document.createElement('div'); | |
| w.className = 'welcome'; | |
| w.innerHTML = '<h2>Hi, I’m Antaram 👋</h2><p>Your agentic SLM by Team Triangle. Ask anything — I can search the web and reason through tasks.</p>'+ | |
| '<div class="chips">'+SUGGESTS.map(s=>'<span class="chip">'+esc(s)+'</span>').join('')+'</div>'; | |
| chatEl.appendChild(w); | |
| w.querySelectorAll('.chip').forEach((c,i)=>c.onclick=()=>{inputEl.value=SUGGESTS[i];inputEl.focus();}); | |
| return; | |
| } | |
| state.messages.forEach((m) => { | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'msg ' + m.role + (m.typing ? ' typing' : ''); | |
| const row = document.createElement('div'); row.className = 'row'; | |
| const bubble = document.createElement('div'); bubble.className = 'bubble'; | |
| if (m.image){ | |
| const im = document.createElement('img'); | |
| im.src = m.image; im.style.cssText='max-width:220px;border-radius:10px;display:block;margin-bottom:6px'; | |
| bubble.appendChild(im); | |
| } | |
| if (m.typing){ | |
| bubble.innerHTML = '<span class="dots"><span></span><span></span><span></span></span>'; | |
| } else if (m.role === 'assistant'){ | |
| const span = document.createElement('span'); span.innerHTML = mdRender(m.content); bubble.appendChild(span); | |
| } else { | |
| const span = document.createElement('span'); span.textContent = m.content; bubble.appendChild(span); | |
| } | |
| row.appendChild(bubble); | |
| if (m.role === 'assistant' && !m.typing){ | |
| const copy = document.createElement('button'); | |
| copy.className='iconbtn'; copy.title='Copy'; copy.textContent='⧉'; | |
| copy.onclick=()=>{navigator.clipboard.writeText(m.content);copy.textContent='✓';setTimeout(()=>copy.textContent='⧉',1200);}; | |
| row.appendChild(copy); | |
| if (m.meta){ | |
| const info=document.createElement('button'); | |
| info.className='iconbtn';info.title='Metadata';info.textContent='ⓘ'; | |
| info.onclick=()=>{m.open=!m.open;render();}; | |
| row.appendChild(info); | |
| } | |
| } | |
| wrap.appendChild(row); | |
| if (m.role === 'assistant' && m.meta){ | |
| const meta=document.createElement('div'); | |
| meta.className='meta'+(m.open?' open':''); | |
| const pct=m.meta.context_percent != null ? m.meta.context_percent : 0; | |
| meta.innerHTML = | |
| '<div class="item"><span class="ico">⏱</span> time <b>'+m.meta.time_taken_s+'s</b></div>'+ | |
| '<div class="item"><span class="ico">🧮</span> model <b>'+m.meta.model+'</b></div>'+ | |
| '<div class="item"><span class="ico">⬆</span> tokens in <b>'+m.meta.tokens_in+'</b></div>'+ | |
| '<div class="item"><span class="ico">⬇</span> tokens out <b>'+m.meta.tokens_out+'</b></div>'+ | |
| '<div class="item"><span class="ico">🧠</span> turns <b>'+m.meta.session_turns+'</b></div>'+ | |
| '<div class="item"><span class="ico">🆔</span> conv <b>'+(m.meta.conversation_id||'').slice(0,12)+'</b></div>'+ | |
| '<div class="item full"><span class="ico">📦</span> context <b>'+pct+'% of 1M</b> · ~'+m.meta.context_tokens+' tok</div>'+ | |
| '<div class="item full"><div class="ctxbar"><div class="ctxbar-fill" style="width:'+Math.min(100,pct)+'%"></div></div></div>'; | |
| wrap.appendChild(meta); | |
| } | |
| chatEl.appendChild(wrap); | |
| }); | |
| chatEl.scrollTop = chatEl.scrollHeight; | |
| } | |
| async function send(){ | |
| const text = inputEl.value.trim(); | |
| if ((!text && !pendingImg) || state.busy) return; | |
| state.busy = true; sendBtn.disabled = true; | |
| const img = pendingImg; | |
| state.messages.push({ role:'user', content:text, image: img }); | |
| state.messages.push({ role:'assistant', content:'', typing:true }); | |
| inputEl.value=''; inputEl.style.height='auto'; | |
| pendingImg=null; attachInfo.style.display='none'; render(); | |
| const headers={'Content-Type':'application/json'}; | |
| const key=document.getElementById('keyInput').value.trim(); | |
| if (key) headers['Authorization']='Bearer '+key; | |
| const content = img | |
| ? [{type:'text',text:text||'Describe this image.'},{type:'image_url',image_url:{url:img}}] | |
| : text; | |
| try{ | |
| const res=await fetch('/v1/chat/completions',{method:'POST',headers, | |
| body:JSON.stringify({model:'antaram',session_id:state.sessionId,messages:[{role:'user',content}]})}); | |
| const data=await res.json(); | |
| state.messages.pop(); | |
| if(!res.ok){ | |
| state.messages.push({role:'assistant',content:'⚠ '+((data.error&&data.error.message)||res.status)}); | |
| }else{ | |
| state.messages.push({role:'assistant',content:data.choices[0].message.content,meta:data.x_agy,open:false}); | |
| if(data.x_agy) state.ctxPct=data.x_agy.context_percent; | |
| dot.classList.add('ok'); | |
| } | |
| }catch(e){ | |
| state.messages.pop(); | |
| state.messages.push({role:'assistant',content:'⚠ network error: '+e.message}); | |
| }finally{ | |
| state.busy=false; sendBtn.disabled=false; render(); inputEl.focus(); | |
| } | |
| } | |
| sendBtn.onclick=send; | |
| inputEl.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}}); | |
| inputEl.addEventListener('input',()=>{inputEl.style.height='auto';inputEl.style.height=Math.min(inputEl.scrollHeight,140)+'px';}); | |
| document.getElementById('newBtn').onclick=()=>{state.messages=[];state.sessionId=newId();state.ctxPct=null;dot.classList.remove('ok');render();inputEl.focus();}; | |
| const usagePanel=document.getElementById('usagePanel'); | |
| document.getElementById('usageBtn').onclick=async()=>{ | |
| if(usagePanel.classList.contains('open')){usagePanel.classList.remove('open');return;} | |
| try{ | |
| const u=await(await fetch('/usage')).json(); | |
| const rows=[['Region',u.region],['Uptime',u.uptime_seconds+'s'],['Requests',u.requests],['Errors',u.errors], | |
| ['Tokens in',u.tokens_in],['Tokens out',u.tokens_out],['Tokens total',u.tokens_total+' (est)'], | |
| ['Resp avg/min/max',u.avg_response_seconds+'/'+u.min_response_seconds+'/'+u.max_response_seconds+'s'], | |
| ['Active sessions',u.active_sessions]]; | |
| usagePanel.innerHTML='<div style="font-weight:600;margin-bottom:6px">📊 Antaram · Usage</div>'+ | |
| rows.map(r=>'<div class="r"><span>'+r[0]+'</span><b>'+r[1]+'</b></div>').join(''); | |
| usagePanel.classList.add('open'); | |
| }catch(e){usagePanel.innerHTML='usage error';usagePanel.classList.add('open');} | |
| }; | |
| const attachInfo=document.getElementById('attachInfo'); | |
| const fileInput=document.getElementById('fileInput'); | |
| document.getElementById('attachBtn').onclick=()=>fileInput.click(); | |
| fileInput.onchange=()=>{ | |
| const f=fileInput.files[0]; if(!f)return; | |
| const r=new FileReader(); | |
| r.onload=()=>{pendingImg=r.result;attachInfo.style.display='block';attachInfo.textContent='📎 '+f.name+' attached — sends with your next message';}; | |
| r.readAsDataURL(f); fileInput.value=''; | |
| }; | |
| render(); inputEl.focus(); | |
| // ===================== HIDDEN ADMIN CONSOLE ===================== | |
| const ov=document.getElementById('adminOverlay'); | |
| const elLogin=document.getElementById('adminLogin'); | |
| const elPanel=document.getElementById('adminPanel'); | |
| let adminToken=sessionStorage.getItem('antaram_admin')||''; | |
| function openAdmin(){ ov.classList.add('open'); showAdminState(); if(!adminToken) document.getElementById('alUser').focus(); } | |
| function closeAdmin(){ ov.classList.remove('open'); if(location.hash==='#admin') history.replaceState(null,'',location.pathname); } | |
| function showAdminState(){ | |
| const authed=!!adminToken; | |
| elLogin.style.display=authed?'none':'block'; | |
| elPanel.style.display=authed?'block':'none'; | |
| document.getElementById('adminLogout').style.display=authed?'inline-block':'none'; | |
| document.getElementById('adminTitle').textContent=authed?'Admin Console':'Admin · Sign in'; | |
| if(authed) loadUsers(); | |
| } | |
| // secret triggers: Ctrl+Shift+A OR #admin in the URL | |
| document.addEventListener('keydown',e=>{ | |
| if(e.ctrlKey&&e.shiftKey&&(e.key==='A'||e.key==='a')){e.preventDefault();ov.classList.contains('open')?closeAdmin():openAdmin();} | |
| if(e.key==='Escape'&&ov.classList.contains('open'))closeAdmin(); | |
| }); | |
| if(location.hash==='#admin') openAdmin(); | |
| window.addEventListener('hashchange',()=>{ if(location.hash==='#admin') openAdmin(); }); | |
| document.getElementById('adminClose').onclick=closeAdmin; | |
| async function adminFetch(path,opts){ | |
| opts=opts||{}; | |
| opts.headers=Object.assign({'Content-Type':'application/json','Authorization':'Bearer '+adminToken},opts.headers||{}); | |
| const res=await fetch(path,opts); | |
| if(res.status===401){ adminToken=''; sessionStorage.removeItem('antaram_admin'); showAdminState(); throw new Error('Session expired — sign in again.'); } | |
| return res; | |
| } | |
| // --- login --- | |
| document.getElementById('alBtn').onclick=async()=>{ | |
| const errEl=document.getElementById('alErr'); errEl.textContent=''; | |
| const username=document.getElementById('alUser').value.trim(); | |
| const password=document.getElementById('alPass').value; | |
| if(!username||!password){errEl.textContent='Enter username and password.';return;} | |
| try{ | |
| const res=await fetch('/admin/login',{method:'POST',headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({username,password})}); | |
| const data=await res.json(); | |
| if(!res.ok){errEl.textContent=data.detail||'Login failed.';return;} | |
| adminToken=data.token; sessionStorage.setItem('antaram_admin',adminToken); | |
| document.getElementById('alPass').value=''; showAdminState(); | |
| }catch(e){errEl.textContent='Network error: '+e.message;} | |
| }; | |
| document.getElementById('alPass').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('alBtn').click();}); | |
| document.getElementById('adminLogout').onclick=()=>{adminToken='';sessionStorage.removeItem('antaram_admin');showAdminState();}; | |
| // --- tabs --- | |
| document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{ | |
| document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active')); | |
| t.classList.add('active'); | |
| document.querySelectorAll('.tabpane').forEach(p=>p.style.display=p.dataset.pane===t.dataset.tab?'block':'none'); | |
| if(t.dataset.tab==='users')loadUsers(); | |
| if(t.dataset.tab==='sessions')loadSessions(); | |
| if(t.dataset.tab==='prompt')loadPrompt(); | |
| }); | |
| // --- persona templates (personalised with the typed name) --- | |
| function personaTemplate(kind,name){ | |
| const N=name||'the user'; | |
| const T={ | |
| friendly: | |
| '# Persona — Friendly assistant for '+N+'\n'+ | |
| '- Greet '+N+' warmly and keep a friendly, encouraging tone.\n'+ | |
| '- Use simple, clear language and short paragraphs.\n'+ | |
| '- An occasional relevant emoji is fine; never overdo it.', | |
| professional: | |
| '# Persona — Professional assistant for '+N+'\n'+ | |
| '- Address '+N+' formally and keep a polished, business tone.\n'+ | |
| '- Be precise, structured and objective. No slang or emojis.\n'+ | |
| '- Use bullet points and clear headings for longer answers.', | |
| concise: | |
| '# Persona — Concise expert for '+N+'\n'+ | |
| '- Give '+N+' direct, expert answers with no filler.\n'+ | |
| '- Lead with the answer, then a brief reason only if needed.\n'+ | |
| '- Keep replies short unless '+N+' asks for more detail.', | |
| support: | |
| '# Persona — Support agent for '+N+'\n'+ | |
| '- You are a polite, patient support agent helping '+N+'.\n'+ | |
| '- Acknowledge the issue, then give clear step-by-step guidance.\n'+ | |
| '- Stay calm and positive; never blame the user.' | |
| }; | |
| return T[kind]||''; | |
| } | |
| document.getElementById('ikTmpl').addEventListener('click',e=>{ | |
| const b=e.target.closest('button[data-tmpl]'); if(!b)return; | |
| const ta=document.getElementById('ikAgents'); | |
| if(b.dataset.tmpl==='clear'){ ta.value=''; ta.focus(); return; } | |
| const name=document.getElementById('ikName').value.trim(); | |
| ta.value=personaTemplate(b.dataset.tmpl,name); | |
| ta.focus(); | |
| }); | |
| // --- issue key --- | |
| document.getElementById('ikBtn').onclick=async()=>{ | |
| const errEl=document.getElementById('ikErr'); errEl.textContent=''; | |
| const reveal=document.getElementById('ikReveal'); reveal.innerHTML=''; | |
| const username=document.getElementById('ikName').value.trim(); | |
| const role=document.getElementById('ikRole').value; | |
| const ttl=document.getElementById('ikTtl').value; | |
| const maxs=document.getElementById('ikMax').value; | |
| const agents_md=document.getElementById('ikAgents').value; | |
| if(!username){errEl.textContent='Give the user a name/label.';return;} | |
| const payload={username:username,role:role,agents_md:agents_md}; | |
| if(ttl!=='') payload.ttl_days=parseInt(ttl,10); | |
| if(maxs!=='') payload.max_sessions=parseInt(maxs,10); | |
| try{ | |
| const res=await adminFetch('/admin/keys',{method:'POST',body:JSON.stringify(payload)}); | |
| const d=await res.json(); | |
| if(!res.ok){errEl.textContent=d.detail||'Failed to create key.';return;} | |
| const exp=(d.expires_at&&d.expires_at!=='lifetime')?new Date(d.expires_at).toLocaleString():'lifetime'; | |
| const endpoint=location.origin+'/v1/chat/completions'; | |
| const msg='Hi '+username+', here is your Antaram access.\n\n'+ | |
| 'Endpoint: '+endpoint+'\n'+ | |
| 'API key: '+d.api_key+'\n'+ | |
| 'Header: Authorization: Bearer '+d.api_key+'\n'+ | |
| 'Expires: '+exp+'\n\nKeep this key private.'; | |
| reveal.innerHTML='<div class="keyreveal">'+ | |
| '<b>✓ Key created for "'+esc(username)+'" — copy it now, it is shown only once.</b>'+ | |
| '<code id="rvKey">'+esc(d.api_key)+'</code>'+ | |
| '<div style="display:flex;gap:8px;flex-wrap:wrap">'+ | |
| '<button class="tiny primary" id="rvCopyKey">Copy key</button>'+ | |
| '<button class="tiny" id="rvCopyMsg">Copy message to send</button>'+ | |
| '</div>'+ | |
| '<div class="sendmsg" id="rvMsg">'+esc(msg)+'</div>'+ | |
| '</div>'; | |
| document.getElementById('rvCopyKey').onclick=()=>{navigator.clipboard.writeText(d.api_key);}; | |
| document.getElementById('rvCopyMsg').onclick=()=>{navigator.clipboard.writeText(msg);}; | |
| document.getElementById('ikName').value=''; document.getElementById('ikAgents').value=''; | |
| }catch(e){errEl.textContent=e.message;} | |
| }; | |
| // --- users / keys list --- | |
| async function loadUsers(){ | |
| const tbl=document.getElementById('ukTable'); | |
| tbl.innerHTML='<tr><td class="muted">Loading…</td></tr>'; | |
| try{ | |
| const res=await adminFetch('/admin/keys'); const d=await res.json(); | |
| const keys=d.keys||[]; | |
| document.getElementById('ukCount').textContent=keys.length+' total'; | |
| if(!keys.length){tbl.innerHTML='<tr><td class="muted">No keys yet.</td></tr>';return;} | |
| let html='<tr><th>User</th><th>Role</th><th>Sessions</th><th>Expires</th><th>Status</th><th>Actions</th></tr>'; | |
| keys.forEach(k=>{ | |
| const exp=k.expires_at?new Date(k.expires_at).toLocaleDateString():'lifetime'; | |
| const used=k.sessions_used!=null?k.sessions_used:0, cap=k.max_sessions?('/'+k.max_sessions):''; | |
| const status=k.revoked?'<span class="badge dead">revoked</span>':'<span class="badge live">active</span>'; | |
| const adm=k.role==='superadmin'?' <span class="badge adm">admin</span>':''; | |
| const ag=k.has_agents?' <span class="badge ag">AGENTS.md</span>':''; | |
| const nm=esc(k.label||'(unnamed)'); | |
| html+='<tr>'+ | |
| '<td><b>'+nm+'</b><div class="muted" style="font-size:11px">'+esc(k.key_prefix||'')+'</div></td>'+ | |
| '<td>'+k.role+adm+'</td>'+ | |
| '<td>'+used+cap+'</td>'+ | |
| '<td>'+exp+'</td>'+ | |
| '<td>'+status+ag+'</td>'+ | |
| '<td><div class="rowacts">'+ | |
| '<button class="tiny" data-act="ag" data-id="'+k.id+'" data-name="'+nm+'">AGENTS.md</button>'+ | |
| (k.revoked?'':'<button class="tiny danger" data-act="rev" data-id="'+k.id+'">Revoke</button>')+ | |
| '</div></td></tr>'; | |
| }); | |
| tbl.innerHTML=html; | |
| tbl.querySelectorAll('button[data-act]').forEach(b=>b.onclick=()=>{ | |
| const id=parseInt(b.dataset.id,10); | |
| if(b.dataset.act==='rev') revokeKey(id); | |
| else editAgents(id,b.dataset.name); | |
| }); | |
| }catch(e){tbl.innerHTML='<tr><td class="muted">'+esc(e.message)+'</td></tr>';} | |
| } | |
| document.getElementById('ukRefresh').onclick=loadUsers; | |
| async function revokeKey(id){ | |
| if(!confirm('Revoke this key? The user loses access immediately.'))return; | |
| try{ await adminFetch('/admin/keys/'+id,{method:'DELETE'}); loadUsers(); }catch(e){alert(e.message);} | |
| } | |
| // --- per-user AGENTS.md editor --- | |
| let agEditId=null; | |
| async function editAgents(id,name){ | |
| agEditId=id; | |
| const box=document.getElementById('agEditor'); box.style.display='block'; | |
| document.getElementById('agName').textContent=name||('key #'+id); | |
| document.getElementById('agErr').textContent=''; | |
| document.getElementById('agText').value='Loading…'; | |
| box.scrollIntoView({behavior:'smooth'}); | |
| try{ | |
| const res=await adminFetch('/admin/keys/'+id+'/agents'); const d=await res.json(); | |
| document.getElementById('agText').value=d.agents_md||''; | |
| }catch(e){document.getElementById('agText').value='';document.getElementById('agErr').textContent=e.message;} | |
| } | |
| document.getElementById('agCancel').onclick=()=>{document.getElementById('agEditor').style.display='none';agEditId=null;}; | |
| document.getElementById('agSave').onclick=async()=>{ | |
| if(agEditId==null)return; | |
| const errEl=document.getElementById('agErr'); errEl.textContent=''; | |
| try{ | |
| const res=await adminFetch('/admin/keys/'+agEditId+'/agents',{method:'PUT', | |
| body:JSON.stringify({agents_md:document.getElementById('agText').value})}); | |
| if(!res.ok){const d=await res.json();errEl.textContent=d.detail||'Save failed.';return;} | |
| document.getElementById('agEditor').style.display='none'; agEditId=null; loadUsers(); | |
| }catch(e){errEl.textContent=e.message;} | |
| }; | |
| // --- sessions (admin: view + delete any) --- | |
| async function loadSessions(){ | |
| const tbl=document.getElementById('ssTable'); | |
| tbl.innerHTML='<tr><td class="muted">Loading…</td></tr>'; | |
| try{ | |
| const res=await adminFetch('/admin/sessions'); const d=await res.json(); | |
| const rows=d.sessions||[]; | |
| document.getElementById('ssCount').textContent=rows.length+' total'; | |
| if(!rows.length){tbl.innerHTML='<tr><td class="muted">No sessions yet.</td></tr>';return;} | |
| let html='<tr><th>Session</th><th>Owner</th><th>Turns</th><th>Context</th><th>Last active</th><th>Actions</th></tr>'; | |
| rows.forEach(sv=>{ | |
| const la=sv.last_active?new Date(sv.last_active).toLocaleString():'—'; | |
| const tag=sv.active?'':' <span class="badge">reserved</span>'; | |
| const owner=esc(sv.owner_label||('key #'+(sv.api_key_id!=null?sv.api_key_id:'?'))); | |
| html+='<tr>'+ | |
| '<td><b>'+esc(sv.plain_session_id||sv.session_id)+'</b>'+tag+'</td>'+ | |
| '<td>'+owner+'</td>'+ | |
| '<td>'+sv.turns+'</td>'+ | |
| '<td>'+sv.context_percent+'%</td>'+ | |
| '<td>'+la+'</td>'+ | |
| '<td><button class="tiny danger" data-sid="'+encodeURIComponent(sv.session_id)+'">Delete</button></td>'+ | |
| '</tr>'; | |
| }); | |
| tbl.innerHTML=html; | |
| tbl.querySelectorAll('button[data-sid]').forEach(b=>b.onclick=()=>deleteSession(decodeURIComponent(b.dataset.sid))); | |
| }catch(e){tbl.innerHTML='<tr><td class="muted">'+esc(e.message)+'</td></tr>';} | |
| } | |
| document.getElementById('ssRefresh').onclick=loadSessions; | |
| async function deleteSession(effId){ | |
| if(!confirm('Delete this session? Its history is removed and the user loses that context.'))return; | |
| try{ await adminFetch('/admin/sessions/'+encodeURIComponent(effId),{method:'DELETE'}); loadSessions(); }catch(e){alert(e.message);} | |
| } | |
| // --- global system prompt --- | |
| async function loadPrompt(){ | |
| const st=document.getElementById('spState'); st.textContent='loading…'; | |
| try{ | |
| const res=await adminFetch('/admin/system-prompt'); const d=await res.json(); | |
| document.getElementById('spText').value=d.system_prompt||''; | |
| st.textContent=d.is_custom?'custom prompt active':'using built-in default'; | |
| }catch(e){st.textContent=e.message;} | |
| } | |
| document.getElementById('spReload').onclick=loadPrompt; | |
| document.getElementById('spSave').onclick=async()=>{ | |
| const errEl=document.getElementById('spErr'); errEl.textContent=''; | |
| const st=document.getElementById('spState'); | |
| try{ | |
| const res=await adminFetch('/admin/system-prompt',{method:'PUT', | |
| body:JSON.stringify({system_prompt:document.getElementById('spText').value})}); | |
| const d=await res.json(); | |
| if(!res.ok){errEl.textContent=d.detail||'Save failed.';return;} | |
| st.textContent='✓ saved — applies to new turns within ~30s'; | |
| }catch(e){errEl.textContent=e.message;} | |
| }; | |
| </script> | |
| </body> | |
| </html> | |