MiniCoder-1-Inference / index.html
Bc-AI's picture
Create index.html
3976a60 verified
Raw History Blame
38.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodVa-2 Β· Code Assistant</title>
<style>
/* ── RESET & BASE ─────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0f14;
--surface: #13161f;
--surface2: #1a1e2b;
--surface3: #21263a;
--border: #2a2f45;
--accent: #7c6aff;
--accent2: #4fc3f7;
--accent3: #43e97b;
--think: #f7971e;
--text: #e8eaf6;
--text2: #9099bb;
--text3: #5a6280;
--red: #ff5370;
--radius: 12px;
--radius-sm: 8px;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.6;
}
/* ── LAYOUT ───────────────────────────────────────────────────────── */
.app {
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: 340px 1fr;
grid-template-areas:
"header header"
"sidebar main";
height: 100vh;
overflow: hidden;
}
/* ── HEADER ───────────────────────────────────────────────────────── */
header {
grid-area: header;
display: flex;
align-items: center;
gap: 16px;
padding: 12px 24px;
background: var(--surface);
border-bottom: 1px solid var(--border);
z-index: 10;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 800;
color: white;
letter-spacing: -1px;
flex-shrink: 0;
}
.logo-text h1 {
font-size: 16px;
font-weight: 700;
background: linear-gradient(90deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-text p {
font-size: 11px;
color: var(--text3);
}
.header-stats {
margin-left: auto;
display: flex;
gap: 12px;
align-items: center;
}
.stat-pill {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 20px;
padding: 4px 12px;
font-size: 11px;
color: var(--text2);
display: flex;
align-items: center;
gap: 6px;
}
.stat-pill .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent3);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* ── SIDEBAR ──────────────────────────────────────────────────────── */
.sidebar {
grid-area: sidebar;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
.sidebar-section {
padding: 16px;
border-bottom: 1px solid var(--border);
}
.sidebar-section:last-child { border-bottom: none; }
.section-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text3);
margin-bottom: 12px;
}
/* ── CONTEXT RING ─────────────────────────────────────────────────── */
.ctx-ring-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 8px 0;
}
.ctx-ring-svg {
position: relative;
width: 140px;
height: 140px;
}
.ctx-ring-svg svg {
width: 140px;
height: 140px;
transform: rotate(-90deg);
}
.ctx-track {
fill: none;
stroke: var(--surface3);
stroke-width: 10;
}
.ctx-prompt {
fill: none;
stroke: var(--accent2);
stroke-width: 10;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
}
.ctx-generated {
fill: none;
stroke: var(--accent);
stroke-width: 10;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
}
.ctx-think {
fill: none;
stroke: var(--think);
stroke-width: 10;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
}
.ctx-ring-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
}
.ctx-ring-center .ctx-num {
font-size: 22px;
font-weight: 800;
color: var(--text);
line-height: 1;
}
.ctx-ring-center .ctx-denom {
font-size: 11px;
color: var(--text3);
}
.ctx-legend {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.legend-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text2);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 3px;
flex-shrink: 0;
}
.legend-row span:last-child {
margin-left: auto;
font-variant-numeric: tabular-nums;
color: var(--text3);
font-size: 11px;
}
/* ── THINK BUDGET BAR ─────────────────────────────────────────────── */
.think-budget {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
}
.think-budget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
}
.think-budget-label {
color: var(--think);
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.think-budget-val {
color: var(--text2);
font-variant-numeric: tabular-nums;
}
.think-bar-track {
height: 6px;
background: var(--surface3);
border-radius: 3px;
overflow: hidden;
}
.think-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--think), #ff6b6b);
border-radius: 3px;
transition: width 0.3s ease;
width: 0%;
}
.think-hint {
font-size: 10px;
color: var(--text3);
margin-top: 6px;
}
/* ── CONTROLS ─────────────────────────────────────────────────────── */
label {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--text3);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.slider-wrap {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
input[type=range] {
flex: 1;
-webkit-appearance: none;
height: 4px;
border-radius: 2px;
background: var(--surface3);
outline: none;
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
transition: background 0.2s;
}
input[type=range]::-webkit-slider-thumb:hover {
background: var(--accent2);
}
.slider-val {
font-size: 12px;
color: var(--text);
font-variant-numeric: tabular-nums;
min-width: 36px;
text-align: right;
font-weight: 600;
}
/* ── EXAMPLES ─────────────────────────────────────────────────────── */
.examples-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.example-btn {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 10px;
color: var(--text2);
font-size: 12px;
cursor: pointer;
text-align: left;
transition: all 0.15s;
line-height: 1.4;
}
.example-btn:hover {
border-color: var(--accent);
color: var(--text);
background: var(--surface3);
}
/* ── MAIN ─────────────────────────────────────────────────────────── */
main {
grid-area: main;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── CHAT AREA ────────────────────────────────────────────────────── */
.chat-area {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.chat-area::-webkit-scrollbar { width: 6px; }
.chat-area::-webkit-scrollbar-track { background: transparent; }
.chat-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* ── MESSAGES ─────────────────────────────────────────────────────── */
.msg {
max-width: 820px;
width: 100%;
}
.msg-user {
align-self: flex-end;
}
.msg-assistant {
align-self: flex-start;
}
.msg-bubble {
border-radius: var(--radius);
padding: 14px 18px;
line-height: 1.7;
}
.msg-user .msg-bubble {
background: linear-gradient(135deg, var(--accent), #5a4fcf);
color: white;
border-bottom-right-radius: 4px;
}
.msg-assistant .msg-bubble {
background: var(--surface);
border: 1px solid var(--border);
border-bottom-left-radius: 4px;
}
.msg-label {
font-size: 11px;
color: var(--text3);
margin-bottom: 6px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.msg-user .msg-label { text-align: right; }
/* ── THINK BLOCK ──────────────────────────────────────────────────── */
.think-block {
background: rgba(247, 151, 30, 0.06);
border: 1px solid rgba(247, 151, 30, 0.25);
border-radius: var(--radius-sm);
margin-bottom: 12px;
overflow: hidden;
}
.think-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: rgba(247, 151, 30, 0.08);
border-bottom: 1px solid rgba(247, 151, 30, 0.15);
cursor: pointer;
user-select: none;
font-size: 12px;
font-weight: 600;
color: var(--think);
}
.think-header:hover { background: rgba(247, 151, 30, 0.12); }
.think-chevron {
margin-left: auto;
transition: transform 0.2s;
font-size: 10px;
}
.think-header.collapsed .think-chevron {
transform: rotate(-90deg);
}
.think-body {
padding: 12px 14px;
font-size: 13px;
color: var(--text2);
white-space: pre-wrap;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
}
.think-body.collapsed { display: none; }
/* ── CODE BLOCK ───────────────────────────────────────────────────── */
.code-block {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
margin: 8px 0;
}
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
background: var(--surface3);
border-bottom: 1px solid var(--border);
font-size: 11px;
color: var(--text3);
}
.copy-btn {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
padding: 2px 8px;
color: var(--text3);
font-size: 11px;
cursor: pointer;
transition: all 0.15s;
}
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }
pre {
padding: 14px;
overflow-x: auto;
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--text);
margin: 0;
}
/* ── ANSWER TEXT ──────────────────────────────────────────────────── */
.answer-text {
white-space: pre-wrap;
word-break: break-word;
}
/* ── STREAMING CURSOR ─────────────────────────────────────────────── */
.cursor {
display: inline-block;
width: 2px;
height: 1em;
background: var(--accent);
margin-left: 2px;
vertical-align: text-bottom;
animation: blink 0.8s steps(1) infinite;
}
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* ── STATUS BAR ───────────────────────────────────────────────────── */
.status-bar {
display: flex;
gap: 16px;
padding: 8px 18px;
background: var(--surface);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
font-size: 11px;
color: var(--text3);
font-variant-numeric: tabular-nums;
flex-wrap: wrap;
}
.status-item {
display: flex;
align-items: center;
gap: 4px;
}
.status-item .val {
color: var(--text2);
font-weight: 600;
}
/* ── INPUT AREA ───────────────────────────────────────────────────── */
.input-area {
padding: 16px 24px 20px;
background: var(--surface);
border-top: 1px solid var(--border);
}
.input-wrap {
display: flex;
gap: 10px;
align-items: flex-end;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 12px;
transition: border-color 0.2s;
}
.input-wrap:focus-within {
border-color: var(--accent);
}
textarea {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--text);
font-size: 14px;
font-family: inherit;
line-height: 1.6;
resize: none;
min-height: 44px;
max-height: 200px;
overflow-y: auto;
}
textarea::placeholder { color: var(--text3); }
.send-btn {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
border: none;
background: linear-gradient(135deg, var(--accent), #5a4fcf);
color: white;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.15s;
}
.send-btn:hover:not(:disabled) {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(124, 106, 255, 0.4);
}
.send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.stop-btn {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
border: 1px solid var(--red);
background: rgba(255, 83, 112, 0.1);
color: var(--red);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.15s;
}
.stop-btn:hover {
background: rgba(255, 83, 112, 0.2);
}
.stop-btn.hidden { display: none; }
/* ── EMPTY STATE ──────────────────────────────────────────────────── */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
opacity: 0.6;
pointer-events: none;
}
.empty-icon {
font-size: 48px;
}
.empty-state h2 {
font-size: 20px;
font-weight: 700;
color: var(--text2);
}
.empty-state p {
font-size: 13px;
color: var(--text3);
text-align: center;
max-width: 300px;
}
/* ── SCROLLBAR ────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* ── RESPONSIVE ───────────────────────────────────────────────────── */
@media (max-width: 700px) {
.app {
grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr;
grid-template-areas: "header" "sidebar" "main";
}
.sidebar {
max-height: 260px;
border-right: none;
border-bottom: 1px solid var(--border);
}
}
</style>
</head>
<body>
<div class="app">
<!-- ── HEADER ──────────────────────────────────────────────────────── -->
<header>
<div class="logo">
<div class="logo-icon">C2</div>
<div class="logo-text">
<h1>CodVa-2</h1>
<p>213M Β· Instruction Tuned Β· SFT</p>
</div>
</div>
<div class="header-stats">
<div class="stat-pill">
<div class="dot"></div>
<span id="hdr-status">Ready</span>
</div>
<div class="stat-pill">⚑ <span id="hdr-tps">β€”</span> tok/s</div>
<div class="stat-pill">🧠 213M params</div>
</div>
</header>
<!-- ── SIDEBAR ─────────────────────────────────────────────────────── -->
<aside class="sidebar">
<!-- Context Ring -->
<div class="sidebar-section">
<div class="section-label">Context Window</div>
<div class="ctx-ring-wrap">
<div class="ctx-ring-svg">
<svg viewBox="0 0 140 140">
<circle class="ctx-track" cx="70" cy="70" r="55"/>
<!-- Prompt arc (cyan) -->
<circle class="ctx-prompt" id="ring-prompt" cx="70" cy="70" r="55"
stroke-dasharray="345.6" stroke-dashoffset="345.6"/>
<!-- Think arc (orange) -->
<circle class="ctx-think" id="ring-think" cx="70" cy="70" r="55"
stroke-dasharray="345.6" stroke-dashoffset="345.6"/>
<!-- Generated arc (purple) -->
<circle class="ctx-generated" id="ring-gen" cx="70" cy="70" r="55"
stroke-dasharray="345.6" stroke-dashoffset="345.6"/>
</svg>
<div class="ctx-ring-center">
<div class="ctx-num" id="ring-used">0</div>
<div class="ctx-denom">/ 2048</div>
</div>
</div>
<div class="ctx-legend">
<div class="legend-row">
<div class="legend-dot" style="background:var(--accent2)"></div>
<span>Prompt</span>
<span id="leg-prompt">0 tok</span>
</div>
<div class="legend-row">
<div class="legend-dot" style="background:var(--think)"></div>
<span>Thinking</span>
<span id="leg-think">0 tok</span>
</div>
<div class="legend-row">
<div class="legend-dot" style="background:var(--accent)"></div>
<span>Response</span>
<span id="leg-gen">0 tok</span>
</div>
<div class="legend-row">
<div class="legend-dot" style="background:var(--surface3)"></div>
<span>Free</span>
<span id="leg-free">2048 tok</span>
</div>
</div>
</div>
</div>
<!-- Think Budget -->
<div class="sidebar-section">
<div class="section-label">Thinking Budget</div>
<div class="think-budget">
<div class="think-budget-header">
<div class="think-budget-label">
<span>🧠</span> Think tokens
</div>
<div class="think-budget-val">
<span id="think-used">0</span> / <span id="think-max">512</span>
</div>
</div>
<div class="think-bar-track">
<div class="think-bar-fill" id="think-bar"></div>
</div>
<div class="think-hint">Orange = model reasoning tokens</div>
</div>
</div>
<!-- Parameters -->
<div class="sidebar-section">
<div class="section-label">Parameters</div>
<label>Max new tokens</label>
<div class="slider-wrap">
<input type="range" id="sl-max" min="32" max="512" step="16" value="256">
<span class="slider-val" id="vl-max">256</span>
</div>
<label>Temperature</label>
<div class="slider-wrap">
<input type="range" id="sl-temp" min="0" max="1.0" step="0.05" value="0.3">
<span class="slider-val" id="vl-temp">0.3</span>
</div>
<label>Top-P</label>
<div class="slider-wrap">
<input type="range" id="sl-topp" min="0.5" max="1.0" step="0.05" value="0.95">
<span class="slider-val" id="vl-topp">0.95</span>
</div>
<label>Top-K</label>
<div class="slider-wrap">
<input type="range" id="sl-topk" min="1" max="100" step="1" value="50">
<span class="slider-val" id="vl-topk">50</span>
</div>
</div>
<!-- Examples -->
<div class="sidebar-section">
<div class="section-label">Examples</div>
<div class="examples-list">
<button class="example-btn" onclick="setPrompt('Write a Python function to compute fibonacci numbers recursively with memoization')">
🐍 Fibonacci with memoization
</button>
<button class="example-btn" onclick="setPrompt('Implement a binary search algorithm in Python with detailed comments')">
πŸ” Binary search
</button>
<button class="example-btn" onclick="setPrompt('Write a quicksort implementation and explain the time complexity')">
⚑ Quicksort + complexity
</button>
<button class="example-btn" onclick="setPrompt('Create a Python class for a linked list with insert, delete, and search methods')">
πŸ”— Linked list class
</button>
<button class="example-btn" onclick="setPrompt('How do I reverse a string in Python? Show multiple methods')">
πŸ”„ Reverse a string
</button>
<button class="example-btn" onclick="setPrompt('Explain what a decorator is in Python and write an example')">
🎨 Python decorators
</button>
</div>
</div>
</aside>
<!-- ── MAIN ────────────────────────────────────────────────────────── -->
<main>
<!-- Status bar -->
<div class="status-bar" id="status-bar">
<div class="status-item">🟒 <span class="val" id="st-state">Idle</span></div>
<div class="status-item">⏱ <span class="val" id="st-time">β€”</span></div>
<div class="status-item">⚑ <span class="val" id="st-tps">β€”</span> tok/s</div>
<div class="status-item">πŸ”€ <span class="val" id="st-tokens">β€”</span> tokens</div>
<div class="status-item">πŸ’­ <span class="val" id="st-think">no thinking</span></div>
</div>
<!-- Chat area -->
<div class="chat-area" id="chat-area">
<div class="empty-state" id="empty-state">
<div class="empty-icon">🧠</div>
<h2>CodVa-2 SFT</h2>
<p>Instruction-tuned code assistant.<br>Ask me anything about code!</p>
</div>
</div>
<!-- Input -->
<div class="input-area">
<div class="input-wrap">
<textarea
id="prompt-input"
placeholder="Ask me to write code, explain concepts, debug..."
rows="1"
onkeydown="handleKey(event)"
oninput="autoResize(this)"
></textarea>
<button class="stop-btn hidden" id="stop-btn" onclick="stopGen()" title="Stop generation">β– </button>
<button class="send-btn" id="send-btn" onclick="sendMessage()" title="Send (Ctrl+Enter)">β–Ά</button>
</div>
</div>
</main>
</div>
<script>
// ── STATE ────────────────────────────────────────────────────────────────
const MAX_CTX = 2048;
const CIRC = 2 * Math.PI * 55; // circumference for r=55
let isStreaming = false;
let thinkTokens = 0;
let genTokens = 0;
let promptTokens = 0;
// ── SLIDERS ──────────────────────────────────────────────────────────────
function linkSlider(id, valId, decimals = 0) {
const s = document.getElementById(id);
const v = document.getElementById(valId);
s.addEventListener('input', () => {
v.textContent = parseFloat(s.value).toFixed(decimals);
});
}
linkSlider('sl-max', 'vl-max', 0);
linkSlider('sl-temp', 'vl-temp', 2);
linkSlider('sl-topp', 'vl-topp', 2);
linkSlider('sl-topk', 'vl-topk', 0);
// ── TEXTAREA AUTO-RESIZE ──────────────────────────────────────────────────
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
}
function handleKey(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
sendMessage();
}
}
// ── SET EXAMPLE PROMPT ────────────────────────────────────────────────────
function setPrompt(text) {
const ta = document.getElementById('prompt-input');
ta.value = text;
autoResize(ta);
ta.focus();
}
// ── CONTEXT RING UPDATE ───────────────────────────────────────────────────
function updateRing(pTok, thinkTok, genTok) {
const total = Math.min(pTok + thinkTok + genTok, MAX_CTX);
// Arc lengths
const pArc = (pTok / MAX_CTX) * CIRC;
const thinkArc = (thinkTok / MAX_CTX) * CIRC;
const genArc = (genTok / MAX_CTX) * CIRC;
// Prompt arc (starts at 0)
const rp = document.getElementById('ring-prompt');
rp.style.strokeDashoffset = CIRC - pArc;
// Think arc (starts after prompt)
const rt = document.getElementById('ring-think');
rt.style.strokeDasharray = `${thinkArc} ${CIRC - thinkArc}`;
rt.style.strokeDashoffset = CIRC - pArc;
// Generated arc (starts after prompt + think)
const rg = document.getElementById('ring-gen');
rg.style.strokeDasharray = `${genArc} ${CIRC - genArc}`;
rg.style.strokeDashoffset = CIRC - pArc - thinkArc;
// Center number
document.getElementById('ring-used').textContent = total;
// Legend
document.getElementById('leg-prompt').textContent = pTok + ' tok';
document.getElementById('leg-think').textContent = thinkTok + ' tok';
document.getElementById('leg-gen').textContent = genTok + ' tok';
document.getElementById('leg-free').textContent = Math.max(0, MAX_CTX - total) + ' tok';
// Think budget bar
const maxThink = parseInt(document.getElementById('sl-max').value);
const thinkPct = Math.min(100, (thinkTok / maxThink) * 100);
document.getElementById('think-bar').style.width = thinkPct + '%';
document.getElementById('think-used').textContent = thinkTok;
document.getElementById('think-max').textContent = maxThink;
}
// ── PARSE RESPONSE ────────────────────────────────────────────────────────
// Handles both <think> and <|think|> variants
function parseResponse(raw) {
// Normalize both think tag variants to same format
let normalized = raw
.replace(/<\|think\|>/g, '<THINK>')
.replace(/<\|\/think\|>/g, '</THINK>')
.replace(/<think>/gi, '<THINK>')
.replace(/<\/think>/gi, '</THINK>');
// Clean up ChatML tokens
normalized = normalized
.replace(/<\|im_start\|>\w+\n?/g, '')
.replace(/<\|im_end\|>/g, '')
.replace(/<\|endoftext\|>/g, '');
const thinkMatch = normalized.match(/<THINK>([\s\S]*?)(<\/THINK>|$)/);
let thinkContent = '';
let answerContent = normalized;
if (thinkMatch) {
thinkContent = thinkMatch[1].trim();
answerContent = normalized.replace(/<THINK>[\s\S]*?(<\/THINK>|$)/, '').trim();
}
// Rough token estimate for think content
const thinkTokEst = Math.round(thinkContent.split(/\s+/).length * 1.3);
return { thinkContent, answerContent, thinkTokEst };
}
// ── RENDER ANSWER ─────────────────────────────────────────────────────────
function renderAnswer(text, isStreaming) {
// Split by code blocks
const parts = text.split(/(```[\s\S]*?```|```[\s\S]*$)/g);
let html = '';
for (const part of parts) {
if (part.startsWith('```')) {
const lines = part.replace(/^```/, '').split('\n');
const lang = lines[0].trim() || 'code';
const code = lines.slice(1).join('\n').replace(/```$/, '').trim();
const escaped = code.replace(/</g,'&lt;').replace(/>/g,'&gt;');
html += `
<div class="code-block">
<div class="code-header">
<span>${lang}</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre>${escaped}</pre>
</div>`;
} else if (part.trim()) {
const escaped = part.replace(/</g,'&lt;').replace(/>/g,'&gt;');
html += `<div class="answer-text">${escaped}</div>`;
}
}
if (isStreaming) html += '<span class="cursor"></span>';
return html;
}
// ── COPY CODE ─────────────────────────────────────────────────────────────
function copyCode(btn) {
const code = btn.closest('.code-block').querySelector('pre').textContent;
navigator.clipboard.writeText(code).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 1500);
});
}
// ── ADD USER MESSAGE ──────────────────────────────────────────────────────
function addUserMsg(text) {
document.getElementById('empty-state')?.remove();
const div = document.createElement('div');
div.className = 'msg msg-user';
div.innerHTML = `
<div class="msg-label">You</div>
<div class="msg-bubble">${text.replace(/</g,'&lt;').replace(/>/g,'&gt;')}</div>
`;
document.getElementById('chat-area').appendChild(div);
return div;
}
// ── ADD ASSISTANT MESSAGE ─────────────────────────────────────────────────
function addAssistantMsg() {
const div = document.createElement('div');
div.className = 'msg msg-assistant';
div.innerHTML = `
<div class="msg-label">CodVa-2</div>
<div class="msg-bubble" id="current-bubble">
<span class="cursor"></span>
</div>
`;
document.getElementById('chat-area').appendChild(div);
scrollBottom();
return div;
}
// ── UPDATE ASSISTANT BUBBLE ───────────────────────────────────────────────
function updateBubble(raw, streaming) {
const bubble = document.getElementById('current-bubble');
if (!bubble) return;
const { thinkContent, answerContent, thinkTokEst } = parseResponse(raw);
let html = '';
// Think block (collapsible)
if (thinkContent) {
html += `
<div class="think-block">
<div class="think-header" onclick="toggleThink(this)">
<span>🧠</span>
<span>Thinking${streaming && !raw.includes('</THINK>') && !raw.includes('<|/think|>') ? '...' : ''}</span>
<span>~${thinkTokEst} tokens</span>
<span class="think-chevron">β–Ό</span>
</div>
<div class="think-body">${thinkContent.replace(/</g,'&lt;').replace(/>/g,'&gt;')}</div>
</div>`;
}
// Answer
html += renderAnswer(answerContent, streaming);
bubble.innerHTML = html;
}
function toggleThink(header) {
header.classList.toggle('collapsed');
header.nextElementSibling.classList.toggle('collapsed');
}
// ── SCROLL TO BOTTOM ──────────────────────────────────────────────────────
function scrollBottom() {
const ca = document.getElementById('chat-area');
ca.scrollTop = ca.scrollHeight;
}
// ── STATUS UPDATE ──────────────────────────────────────────────────────────
function updateStatus(state, tps, elapsed, tokens, thinkTok) {
document.getElementById('st-state').textContent = state;
document.getElementById('st-time').textContent = elapsed ? elapsed + 's' : 'β€”';
document.getElementById('st-tps').textContent = tps ? tps : 'β€”';
document.getElementById('st-tokens').textContent = tokens ? tokens : 'β€”';
document.getElementById('st-think').textContent = thinkTok > 0 ? `${thinkTok} think tok` : 'no thinking';
document.getElementById('hdr-status').textContent = state;
document.getElementById('hdr-tps').textContent = tps || 'β€”';
}
// ── SEND MESSAGE ──────────────────────────────────────────────────────────
async function sendMessage() {
if (isStreaming) return;
const input = document.getElementById('prompt-input');
const prompt = input.value.trim();
if (!prompt) return;
isStreaming = true;
input.value = '';
autoResize(input);
document.getElementById('send-btn').disabled = true;
document.getElementById('stop-btn').classList.remove('hidden');
addUserMsg(prompt);
addAssistantMsg();
const params = {
prompt,
max_new: parseInt(document.getElementById('sl-max').value),
temperature: parseFloat(document.getElementById('sl-temp').value),
top_p: parseFloat(document.getElementById('sl-topp').value),
top_k: parseInt(document.getElementById('sl-topk').value),
};
updateStatus('Generating...', null, null, null, 0);
thinkTokens = 0; genTokens = 0; promptTokens = 0;
try {
const resp = await fetch('/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let rawAccum = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'ctx') {
promptTokens = data.prompt_tokens;
updateRing(promptTokens, 0, 0);
}
else if (data.type === 'token') {
rawAccum = data.raw;
// Estimate think vs gen tokens from raw text
const { thinkTokEst } = parseResponse(rawAccum);
thinkTokens = thinkTokEst;
genTokens = Math.max(0, data.step - thinkTokEst);
updateRing(promptTokens, thinkTokens, genTokens);
updateBubble(rawAccum, true);
updateStatus('Generating...', data.tps, data.elapsed, data.step, thinkTokens);
scrollBottom();
}
else if (data.type === 'done') {
updateBubble(rawAccum, false);
updateStatus('Done', data.tps, data.elapsed, data.tokens, thinkTokens);
updateRing(promptTokens, thinkTokens, genTokens);
scrollBottom();
}
} catch (e) { /* skip malformed */ }
}
}
} catch (e) {
const bubble = document.getElementById('current-bubble');
if (bubble) bubble.innerHTML = `<span style="color:var(--red)">Error: ${e.message}</span>`;
updateStatus('Error', null, null, null, 0);
}
isStreaming = false;
document.getElementById('send-btn').disabled = false;
document.getElementById('stop-btn').classList.add('hidden');
}
// ── STOP ──────────────────────────────────────────────────────────────────
async function stopGen() {
await fetch('/stop', { method: 'POST' });
updateStatus('Stopped', null, null, null, thinkTokens);
const bubble = document.getElementById('current-bubble');
if (bubble) {
bubble.querySelectorAll('.cursor').forEach(c => c.remove());
}
isStreaming = false;
document.getElementById('send-btn').disabled = false;
document.getElementById('stop-btn').classList.add('hidden');
}
// ── INIT ──────────────────────────────────────────────────────────────────
updateRing(0, 0, 0);
document.getElementById('prompt-input').focus();
</script>
</body>
</html>