nothing / app /web /index.html
devarshia5's picture
Upload 25 files
a5f2c4b verified
Raw
History Blame Contribute Delete
37.9 kB
<!DOCTYPE html>
<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 &amp; 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 &amp; 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&#10;- Always answer in formal English.&#10;- You are Riya's research assistant; focus on marketing.&#10;- Never discuss topics outside marketing.&#10;&#10;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 &amp; 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=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[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>