MiniCoder-1-Inference / index.html
Bc-AI's picture
Update index.html
94d0c5a verified
Raw
History Blame Contribute Delete
45.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MiniCoder-1 Instruct Β· Inference Console</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* ── RESET & BASE ─────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* base surfaces β€” dim instrument-panel graphite */
--bg: #0a0c0b;
--surface: #111512;
--surface2: #171d19;
--surface3: #1e2621;
--border: #253028;
--border-hi: #3a4a3f;
/* signal colors β€” three token streams, each its own channel */
--sig-prompt: #5fd0d6; /* cyan β€” prompt tokens */
--sig-think: #ffb454; /* amber β€” reasoning tokens */
--sig-gen: #8b7bff; /* violet β€” response tokens */
--sig-free: #2a352d;
--ok: #7ee787;
--danger: #ff6b6b;
--text: #e8ece6;
--text-dim: #93a099;
--text-faint: #56635b;
--radius: 10px;
--radius-sm: 6px;
--font-display: 'Space Grotesk', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
font-size: 14px;
line-height: 1.6;
}
/* faint fixed grain / vignette β€” sets the instrument-panel mood without being loud */
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 999;
background:
radial-gradient(ellipse at 50% -10%, rgba(255,255,255,0.035), transparent 55%);
mix-blend-mode: screen;
}
/* ── LAYOUT ───────────────────────────────────────────────────────── */
.app {
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: 320px 1fr;
grid-template-areas:
"header header"
"sidebar main";
height: 100vh;
overflow: hidden;
}
/* ── HEADER ───────────────────────────────────────────────────────── */
header {
grid-area: header;
display: flex;
align-items: center;
gap: 18px;
padding: 0 22px;
height: 60px;
background: var(--surface);
border-bottom: 1px solid var(--border);
z-index: 10;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 34px;
height: 34px;
background: var(--surface3);
border: 1px solid var(--border-hi);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
color: var(--sig-think);
letter-spacing: -0.5px;
flex-shrink: 0;
position: relative;
}
.logo-icon::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
box-shadow: 0 0 12px 0 rgba(255,180,84,0.25);
pointer-events: none;
}
.logo-text h1 {
font-family: var(--font-display);
font-size: 15px;
font-weight: 700;
letter-spacing: 0.2px;
color: var(--text);
}
.logo-text p {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-faint);
letter-spacing: 0.5px;
margin-top: 1px;
}
.header-divider {
width: 1px;
height: 26px;
background: var(--border);
margin: 0 2px;
}
.header-stats {
margin-left: auto;
display: flex;
gap: 10px;
align-items: center;
}
.stat-pill {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 20px;
padding: 5px 12px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
display: flex;
align-items: center;
gap: 7px;
letter-spacing: 0.2px;
}
.stat-pill .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 6px var(--ok);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* ── 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: 18px 16px;
border-bottom: 1px solid var(--border);
}
.sidebar-section:last-child { border-bottom: none; }
.section-label {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--text-faint);
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.section-label::before {
content: "";
width: 3px;
height: 3px;
background: var(--text-faint);
border-radius: 50%;
}
/* ── CONTEXT GAUGE (signature element) ───────────────────────────── */
.ctx-ring-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
padding: 4px 0 2px;
}
.ctx-ring-svg {
position: relative;
width: 152px;
height: 152px;
}
.ctx-ring-svg svg {
width: 152px;
height: 152px;
}
/* tick ring, decorative β€” analog gauge marks */
.ctx-ticks line {
stroke: var(--border-hi);
stroke-width: 1;
}
.ctx-ticks line.major {
stroke: var(--text-faint);
stroke-width: 1.5;
}
.ctx-track {
fill: none;
stroke: var(--surface3);
stroke-width: 8;
}
.ctx-prompt {
fill: none;
stroke: var(--sig-prompt);
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
filter: drop-shadow(0 0 3px rgba(95,208,214,0.5));
}
.ctx-generated {
fill: none;
stroke: var(--sig-gen);
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
filter: drop-shadow(0 0 3px rgba(139,123,255,0.5));
}
.ctx-think {
fill: none;
stroke: var(--sig-think);
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
filter: drop-shadow(0 0 3px rgba(255,180,84,0.5));
}
.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-family: var(--font-mono);
font-size: 24px;
font-weight: 700;
color: var(--text);
line-height: 1;
font-variant-numeric: tabular-nums;
}
.ctx-ring-center .ctx-denom {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-faint);
margin-top: 3px;
letter-spacing: 0.4px;
}
.ctx-legend {
display: flex;
flex-direction: column;
gap: 7px;
width: 100%;
}
.legend-row {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-row span:last-child {
margin-left: auto;
font-variant-numeric: tabular-nums;
color: var(--text-faint);
font-size: 11px;
}
/* ── THINK BUDGET BAR ─────────────────────────────────────────────── */
.think-budget {
background: var(--surface2);
border: 1px solid var(--border);
border-left: 2px solid var(--sig-think);
border-radius: var(--radius-sm);
padding: 11px 13px;
}
.think-budget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 9px;
}
.think-budget-label {
color: var(--sig-think);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
display: flex;
align-items: center;
gap: 6px;
text-transform: uppercase;
}
.think-budget-val {
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.think-bar-track {
height: 5px;
background: var(--surface3);
border-radius: 3px;
overflow: hidden;
}
.think-bar-fill {
height: 100%;
background: var(--sig-think);
box-shadow: 0 0 6px rgba(255,180,84,0.6);
border-radius: 3px;
transition: width 0.3s ease;
width: 0%;
}
.think-hint {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-faint);
margin-top: 7px;
}
/* ── CONTROLS ─────────────────────────────────────────────────────── */
label {
display: block;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
color: var(--text-faint);
margin-bottom: 7px;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.slider-wrap {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
input[type=range] {
flex: 1;
-webkit-appearance: none;
height: 3px;
border-radius: 2px;
background: var(--surface3);
outline: none;
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 13px;
height: 13px;
border-radius: 3px;
background: var(--text);
border: 2px solid var(--bg);
box-shadow: 0 0 0 1px var(--border-hi);
cursor: pointer;
transition: box-shadow 0.15s;
}
input[type=range]::-webkit-slider-thumb:hover {
box-shadow: 0 0 0 1px var(--sig-prompt), 0 0 8px rgba(95,208,214,0.5);
}
input[type=range]::-moz-range-thumb {
width: 13px;
height: 13px;
border-radius: 3px;
background: var(--text);
border: 2px solid var(--bg);
box-shadow: 0 0 0 1px var(--border-hi);
cursor: pointer;
}
.slider-val {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text);
font-variant-numeric: tabular-nums;
min-width: 34px;
text-align: right;
font-weight: 600;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 4px;
padding: 2px 5px;
}
/* ── 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: 9px 11px;
color: var(--text-dim);
font-family: var(--font-body);
font-size: 12px;
cursor: pointer;
text-align: left;
transition: all 0.15s;
line-height: 1.4;
display: flex;
align-items: center;
gap: 9px;
}
.example-btn:hover {
border-color: var(--sig-prompt);
color: var(--text);
background: var(--surface3);
transform: translateX(2px);
}
/* ── MAIN ─────────────────────────────────────────────────────────── */
main {
grid-area: main;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── STATUS BAR ───────────────────────────────────────────────────── */
.status-bar {
display: flex;
gap: 20px;
padding: 9px 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-faint);
font-variant-numeric: tabular-nums;
flex-wrap: wrap;
}
.status-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-item .status-icon {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-faint);
flex-shrink: 0;
}
.status-item .status-icon.live {
background: var(--ok);
box-shadow: 0 0 5px var(--ok);
animation: pulse 1.4s infinite;
}
.status-item .val {
color: var(--text-dim);
font-weight: 600;
}
/* ── CHAT AREA ────────────────────────────────────────────────────── */
.chat-area {
flex: 1;
overflow-y: auto;
padding: 26px 28px;
display: flex;
flex-direction: column;
gap: 18px;
}
.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: 800px;
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;
font-size: 13.5px;
}
.msg-user .msg-bubble {
background: var(--surface2);
border: 1px solid var(--border-hi);
border-right: 2px solid var(--sig-prompt);
color: var(--text);
}
.msg-assistant .msg-bubble {
background: var(--surface);
border: 1px solid var(--border);
border-left: 2px solid var(--sig-gen);
}
.msg-label {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-faint);
margin-bottom: 7px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.msg-user .msg-label { text-align: right; }
/* ── THINK BLOCK ──────────────────────────────────────────────────── */
.think-block {
background: rgba(255, 180, 84, 0.05);
border: 1px solid rgba(255, 180, 84, 0.22);
border-radius: var(--radius-sm);
margin-bottom: 12px;
overflow: hidden;
}
.think-header {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
background: rgba(255, 180, 84, 0.07);
border-bottom: 1px solid rgba(255, 180, 84, 0.15);
cursor: pointer;
user-select: none;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
color: var(--sig-think);
letter-spacing: 0.3px;
text-transform: uppercase;
}
.think-header:hover { background: rgba(255, 180, 84, 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: 13px 15px;
font-size: 12.5px;
color: var(--text-dim);
white-space: pre-wrap;
font-family: var(--font-mono);
line-height: 1.65;
max-height: 300px;
overflow-y: auto;
}
.think-body.collapsed { display: none; }
/* ── CODE BLOCK ───────────────────────────────────────────────────── */
.code-block {
background: #0d100e;
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: 7px 12px;
background: var(--surface3);
border-bottom: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--text-faint);
letter-spacing: 0.3px;
text-transform: uppercase;
}
.copy-btn {
background: none;
border: 1px solid var(--border-hi);
border-radius: 4px;
padding: 2px 9px;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 10.5px;
cursor: pointer;
transition: all 0.15s;
}
.copy-btn:hover { border-color: var(--sig-prompt); color: var(--sig-prompt); }
pre {
padding: 15px;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 12.5px;
line-height: 1.65;
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(--sig-gen);
margin-left: 2px;
vertical-align: text-bottom;
animation: blink 0.8s steps(1) infinite;
}
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* ── INPUT AREA ───────────────────────────────────────────────────── */
.input-area {
padding: 16px 26px 22px;
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: 11px 12px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-wrap:focus-within {
border-color: var(--sig-prompt);
box-shadow: 0 0 0 3px rgba(95,208,214,0.08);
}
textarea {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--text);
font-size: 13.5px;
font-family: var(--font-body);
line-height: 1.6;
resize: none;
min-height: 44px;
max-height: 200px;
overflow-y: auto;
}
textarea::placeholder { color: var(--text-faint); }
.send-btn {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-hi);
background: var(--surface3);
color: var(--sig-prompt);
font-size: 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.15s;
}
.send-btn:hover:not(:disabled) {
border-color: var(--sig-prompt);
box-shadow: 0 0 10px rgba(95,208,214,0.25);
}
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.stop-btn {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
border: 1px solid var(--danger);
background: rgba(255, 107, 107, 0.08);
color: var(--danger);
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.15s;
}
.stop-btn:hover {
background: rgba(255, 107, 107, 0.18);
}
.stop-btn.hidden { display: none; }
.input-hint {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-faint);
margin-top: 8px;
padding-left: 2px;
}
.input-hint kbd {
font-family: var(--font-mono);
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1px 5px;
color: var(--text-dim);
}
/* ── EMPTY STATE ──────────────────────────────────────────────────── */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
pointer-events: none;
}
.empty-icon {
width: 56px;
height: 56px;
border: 1px solid var(--border-hi);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 20px;
font-weight: 700;
color: var(--sig-think);
background: var(--surface2);
}
.empty-state h2 {
font-family: var(--font-display);
font-size: 19px;
font-weight: 700;
color: var(--text);
}
.empty-state p {
font-size: 13px;
color: var(--text-faint);
text-align: center;
max-width: 300px;
line-height: 1.6;
}
/* ── SCROLLBAR ────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* ── FOCUS VISIBILITY ─────────────────────────────────────────────── */
button:focus-visible, input:focus-visible, textarea:focus-visible {
outline: 2px solid var(--sig-prompt);
outline-offset: 2px;
}
/* ── 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: 280px;
border-right: none;
border-bottom: 1px solid var(--border);
}
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}
</style>
</head>
<body>
<div class="app">
<!-- ── HEADER ──────────────────────────────────────────────────────── -->
<header>
<div class="logo">
<div class="logo-icon">M1</div>
<div class="logo-text">
<h1>MiniCoder-1 Instruct</h1>
<p>213M Β· SFT Β· LOCAL INFERENCE</p>
</div>
</div>
<div class="header-divider"></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 Gauge -->
<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 152 152">
<!-- decorative tick ring -->
<g class="ctx-ticks" transform="translate(76,76)" id="ctx-ticks"></g>
<circle class="ctx-track" cx="76" cy="76" r="55" transform="rotate(-90 76 76)"/>
<!-- Prompt arc (cyan) -->
<circle class="ctx-prompt" id="ring-prompt" cx="76" cy="76" r="55"
transform="rotate(-90 76 76)"
stroke-dasharray="345.6" stroke-dashoffset="345.6"/>
<!-- Think arc (amber) -->
<circle class="ctx-think" id="ring-think" cx="76" cy="76" r="55"
transform="rotate(-90 76 76)"
stroke-dasharray="345.6" stroke-dashoffset="345.6"/>
<!-- Generated arc (violet) -->
<circle class="ctx-generated" id="ring-gen" cx="76" cy="76" r="55"
transform="rotate(-90 76 76)"
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 TOK</div>
</div>
</div>
<div class="ctx-legend">
<div class="legend-row">
<div class="legend-dot" style="background:var(--sig-prompt)"></div>
<span>Prompt</span>
<span id="leg-prompt">0 tok</span>
</div>
<div class="legend-row">
<div class="legend-dot" style="background:var(--sig-think)"></div>
<span>Thinking</span>
<span id="leg-think">0 tok</span>
</div>
<div class="legend-row">
<div class="legend-dot" style="background:var(--sig-gen)"></div>
<span>Response</span>
<span id="leg-gen">0 tok</span>
</div>
<div class="legend-row">
<div class="legend-dot" style="background:var(--sig-free)"></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">
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">Amber = 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')">
<span>🐍</span> Fibonacci with memoization
</button>
<button class="example-btn" onclick="setPrompt('Implement a binary search algorithm in Python with detailed comments')">
<span>πŸ”</span> Binary search
</button>
<button class="example-btn" onclick="setPrompt('Write a quicksort implementation and explain the time complexity')">
<span>⚑</span> Quicksort + complexity
</button>
<button class="example-btn" onclick="setPrompt('Create a Python class for a linked list with insert, delete, and search methods')">
<span>πŸ”—</span> Linked list class
</button>
<button class="example-btn" onclick="setPrompt('How do I reverse a string in Python? Show multiple methods')">
<span>πŸ”„</span> Reverse a string
</button>
<button class="example-btn" onclick="setPrompt('Explain what a decorator is in Python and write an example')">
<span>🎨</span> Python decorators
</button>
</div>
</div>
</aside>
<!-- ── MAIN ────────────────────────────────────────────────────────── -->
<main>
<!-- Status bar -->
<div class="status-bar" id="status-bar">
<div class="status-item"><span class="status-icon" id="st-icon"></span><span class="val" id="st-state">Idle</span></div>
<div class="status-item">TIME <span class="val" id="st-time">β€”</span></div>
<div class="status-item">SPEED <span class="val" id="st-tps">β€”</span> tok/s</div>
<div class="status-item">TOKENS <span class="val" id="st-tokens">β€”</span></div>
<div class="status-item">THINK <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">M1</div>
<h2>MiniCoder-1 Instruct</h2>
<p>Instruction-tuned code assistant. Ask for code, an explanation, or a debug pass.</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 class="input-hint"><kbd>Ctrl</kbd> + <kbd>Enter</kbd> to send</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;
let currentBubbleEl = null; // tracks the live assistant bubble (ids repeat across turns, so we can't rely on getElementById)
// ── DECORATIVE TICK RING (signature gauge marks) ──────────────────────────
(function drawTicks() {
const g = document.getElementById('ctx-ticks');
const rOuter = 68, rInnerMinor = 62, rInnerMajor = 59;
const count = 40;
let svgNS = "http://www.w3.org/2000/svg";
for (let i = 0; i < count; i++) {
const angle = (i / count) * 2 * Math.PI - Math.PI / 2;
const major = i % 5 === 0;
const rInner = major ? rInnerMajor : rInnerMinor;
const x1 = Math.cos(angle) * rOuter, y1 = Math.sin(angle) * rOuter;
const x2 = Math.cos(angle) * rInner, y2 = Math.sin(angle) * rInner;
const line = document.createElementNS(svgNS, 'line');
line.setAttribute('x1', x1.toFixed(2));
line.setAttribute('y1', y1.toFixed(2));
line.setAttribute('x2', x2.toFixed(2));
line.setAttribute('y2', y2.toFixed(2));
if (major) line.setAttribute('class', 'major');
g.appendChild(line);
}
})();
// ── 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">MiniCoder-1</div>
<div class="msg-bubble">
<span class="cursor"></span>
</div>
`;
document.getElementById('chat-area').appendChild(div);
currentBubbleEl = div.querySelector('.msg-bubble'); // point future updates at THIS turn's bubble
scrollBottom();
return div;
}
// ── UPDATE ASSISTANT BUBBLE ───────────────────────────────────────────────
function updateBubble(raw, streaming) {
const bubble = currentBubbleEl;
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>Reasoning${streaming && !raw.includes('</THINK>') && !raw.includes('<|/think|>') ? '...' : ''}</span>
<span>~${thinkTokEst} tok</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} tok` : 'no thinking';
document.getElementById('hdr-status').textContent = state;
document.getElementById('hdr-tps').textContent = tps || 'β€”';
const icon = document.getElementById('st-icon');
icon.classList.toggle('live', state === 'Generating...');
}
// ── 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 = currentBubbleEl;
if (bubble) bubble.innerHTML = `<span style="color:var(--danger)">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 = currentBubbleEl;
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>