From 12a7d92f993b352b026973bb17bb9246b30c8bf5 Mon Sep 17 00:00:00 2001 From: gramps Date: Mon, 13 Jul 2026 07:57:27 -0700 Subject: [PATCH] add TOK badge to response footer + arrow key history recall --- routers/chat.py | 8 ++++++-- templates/index.html | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/routers/chat.py b/routers/chat.py index a6fc7ca..a42ca65 100644 --- a/routers/chat.py +++ b/routers/chat.py @@ -46,6 +46,7 @@ def parse_llama_stream_chunk(line: str) -> tuple: if finish == "stop": usage = chunk.get("usage", {}) stats["tokens_per_sec"] = usage.get("tokens_per_second", 0.0) + stats["completion_tokens"] = usage.get("completion_tokens", 0) return token, finish == "stop", stats, logprobs_list if "message" in chunk and "content" in chunk["message"]: token = chunk["message"]["content"] @@ -55,6 +56,7 @@ def parse_llama_stream_chunk(line: str) -> tuple: eval_count = chunk.get("eval_count", 0) eval_duration = chunk.get("eval_duration", 0) stats["tokens_per_sec"] = (eval_count / (eval_duration / 1e9)) if eval_duration > 0 else 0 + stats["completion_tokens"] = eval_count return token, done, stats, [] except json.JSONDecodeError: pass @@ -123,6 +125,7 @@ async def chat(request: Request): full_response = [] all_logprobs = [] tokens_per_sec = 0.0 + completion_tokens = 0 inference_base = await _get_inference_url(user_message) if remember_response: @@ -145,6 +148,7 @@ async def chat(request: Request): yield f"data: {json.dumps({'token': token, 'conversation_id': conv_id})}\n\n" if done: tokens_per_sec = stats.get("tokens_per_sec", 0.0) + completion_tokens = stats.get("completion_tokens", 0) assistant_msg = "".join(full_response) perplexity = calculate_perplexity(all_logprobs) if all_logprobs else 0.0 @@ -199,7 +203,7 @@ async def chat(request: Request): db2.commit() db2.close() - yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'searched': True, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1)})}\n\n" + yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'searched': True, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1), 'tokens': completion_tokens})}\n\n" return saved_msg = assistant_msg @@ -212,7 +216,7 @@ async def chat(request: Request): db2.commit() db2.close() - yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1)})}\n\n" + yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1), 'tokens': completion_tokens})}\n\n" except httpx.RemoteProtocolError: pass diff --git a/templates/index.html b/templates/index.html index 650b6ef..2dcec0a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -173,6 +173,7 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var( .ttr-badge.high { background:rgba(231,76,60,0.15); border:1px solid rgba(231,76,60,0.3); color:var(--danger); } .ttr-badge.search { background:rgba(230,126,34,0.15); border:1px solid rgba(230,126,34,0.3); color:#e67e22; } .tps-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-family:var(--font-mono); font-size:10px; margin-left:8px; background:rgba(72,181,224,0.15); border:1px solid rgba(72,181,224,0.3); color:var(--accent); } +.tok-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-family:var(--font-mono); font-size:10px; margin-left:8px; background:rgba(52,152,219,0.15); border:1px solid rgba(52,152,219,0.3); color:#3498db; } .msg-toolbar { display:flex; gap:2px; margin-top:8px; opacity:0; transition:opacity 0.15s; align-items:center; } .message .content:hover .msg-toolbar { opacity:1; } @@ -1326,6 +1327,9 @@ async function sendMessage() { if (welcome) welcome.remove(); appendMessage('user', message, true); conversationHistory.push({ role: 'user', content: message }); + messageHistory.push(message); + if (messageHistory.length > 100) messageHistory.shift(); + historyIndex = -1; input.value = ''; input.style.height = 'auto'; updateTokenThermometer(); @@ -1394,6 +1398,7 @@ async function sendMessage() { const roleLabel = assistantDiv.querySelector('.role-label'); if (data.searched && roleLabel) roleLabel.innerHTML += '🔍 web'; if (typeof data.perplexity === 'number' && roleLabel) { const ppl = data.perplexity; roleLabel.innerHTML += `ppl: ${ppl.toFixed(1)}`; } + if (typeof data.tokens === 'number' && data.tokens > 0 && roleLabel) roleLabel.innerHTML += `tok: ${data.tokens}`; if (typeof data.tokens_per_sec === 'number' && data.tokens_per_sec > 0 && roleLabel) roleLabel.innerHTML += `${data.tokens_per_sec.toFixed(1)} t/s`; if (roleLabel && ttr > 0) { const ttrSec = ttr / 1000; @@ -1567,8 +1572,34 @@ function fallbackCopy(text, btn) { } const userInput = document.getElementById('userInput'); +const messageHistory = []; +let historyIndex = -1; +let historyDraft = ''; userInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 200) + 'px'; }); -userInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); +userInput.addEventListener('keydown', e => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); return; } + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (messageHistory.length === 0) return; + if (historyIndex === -1) historyDraft = userInput.value; + if (historyIndex < messageHistory.length - 1) { + historyIndex++; + userInput.value = messageHistory[messageHistory.length - 1 - historyIndex]; + } + userInput.selectionStart = userInput.selectionEnd = userInput.value.length; + userInput.dispatchEvent(new Event('input')); + return; + } + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (historyIndex === -1) return; + historyIndex--; + userInput.value = historyIndex === -1 ? historyDraft : messageHistory[messageHistory.length - 1 - historyIndex]; + userInput.selectionStart = userInput.selectionEnd = userInput.value.length; + userInput.dispatchEvent(new Event('input')); + return; + } +}); const toastStyle = document.createElement('style'); toastStyle.textContent = '.toast-error { position:fixed; bottom:80px; left:50%; transform:translateX(-50%); background:var(--danger); color:#fff; padding:12px 24px; border-radius:var(--radius); font-family:var(--font-body); font-size:14px; z-index:9999; animation:fadeIn 0.2s; } @keyframes fadeIn { from{opacity:0;transform:translateX(-50%) translateY(10px)} to{opacity:1;transform:translateX(-50%) translateY(0)} }'; document.head.appendChild(toastStyle);