Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- assets/index-CENTWb2t.css +1 -0
- assets/index-Dh-E6L2Y.js +0 -0
- index.html +2 -2
- src/main.ts +20 -6
- src/style.css +18 -1
- src/ui.ts +16 -2
assets/index-CENTWb2t.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
:root{--bg:#0a0a0a;--bg-2:#141414;--fg:#e8e8e8;--fg-dim:#888;--accent:#fff;--border:#222;--error:#ff6b6b;font-family:JetBrains Mono,SF Mono,Menlo,Consolas,monospace}*{box-sizing:border-box}body{background:var(--bg);color:var(--fg);min-height:100vh;margin:0}.mono{font-family:inherit}.topbar{border-bottom:1px solid var(--border);padding:16px 24px}.brand-name{font-weight:600}.brand-sub{color:var(--fg-dim);margin-left:8px}.grid{grid-template-columns:1fr 1.4fr;gap:16px;min-height:calc(100vh - 60px);padding:16px 24px;display:grid}.pane{flex-direction:column;gap:8px;display:flex}.pane-header{color:var(--fg-dim);justify-content:space-between;align-items:baseline;font-size:13px;display:flex}.model-indicator,.loading-indicator{color:var(--fg-dim);font-size:12px}textarea#tools{background:var(--bg-2);min-height:400px;color:var(--fg);border:1px solid var(--border);resize:none;border-radius:4px;flex:1;padding:12px;font-size:13px}input#query{background:var(--bg-2);color:var(--fg);border:1px solid var(--border);border-radius:4px;padding:10px 12px;font-size:14px}.result-pane{background:var(--bg-2);border:1px solid var(--border);white-space:pre-wrap;word-break:break-word;border-radius:4px;flex:1;margin:0;padding:12px;font-size:13px;overflow:auto}.result-pane.error{color:var(--error)}.examples{flex-wrap:wrap;gap:8px;display:flex}.example-chip{color:var(--fg);border:1px solid var(--border);cursor:pointer;background:0 0;border-radius:999px;padding:4px 12px;font-size:12px;transition:opacity .12s ease-in-out}.example-chip:hover:not(:disabled){background:var(--bg-2)}.example-chip:disabled,input#query:disabled,textarea#tools:disabled{opacity:.4;cursor:not-allowed}.loading-indicator.busy{animation:1.2s ease-in-out infinite pulse}@keyframes pulse{0%,to{opacity:.55}50%{opacity:1}}.error-inline{color:var(--error);margin-top:4px;font-size:12px}
|
assets/index-Dh-E6L2Y.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>Needle Playground (Browser)</title>
|
| 7 |
-
<script type="module" crossorigin src="/assets/index-
|
| 8 |
<link rel="modulepreload" crossorigin href="/assets/chunk-jRWAZmH_.js">
|
| 9 |
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<header class="topbar">
|
|
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>Needle Playground (Browser)</title>
|
| 7 |
+
<script type="module" crossorigin src="/assets/index-Dh-E6L2Y.js"></script>
|
| 8 |
<link rel="modulepreload" crossorigin href="/assets/chunk-jRWAZmH_.js">
|
| 9 |
+
<link rel="stylesheet" crossorigin href="/assets/index-CENTWb2t.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<header class="topbar">
|
src/main.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Tokenizer } from './tokenizer';
|
|
| 4 |
import { loadSessions } from './runtime';
|
| 5 |
import type { NeedleSessions } from './runtime';
|
| 6 |
import { generate } from './generate';
|
| 7 |
-
import { mountUI, setStatus, renderResult, renderError, readTools } from './ui';
|
| 8 |
import type { UI } from './ui';
|
| 9 |
import { TOKENIZER_URL, SPECIALS_URL } from './config';
|
| 10 |
|
|
@@ -25,14 +25,17 @@ async function fetchJson<T>(url: string): Promise<T> {
|
|
| 25 |
async function boot() {
|
| 26 |
const ui = mountUI();
|
| 27 |
try {
|
| 28 |
-
setStatus(ui, 'loading model…');
|
|
|
|
| 29 |
const [sessions, tokenizerBytes, specials] = await Promise.all([
|
| 30 |
-
loadSessions(m => setStatus(ui, m)),
|
| 31 |
fetchBytes(TOKENIZER_URL),
|
| 32 |
fetchJson<Specials>(SPECIALS_URL),
|
| 33 |
]);
|
| 34 |
const tokenizer = await createTokenizer(tokenizerBytes);
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
wireRun(ui, sessions, tokenizer, specials);
|
| 37 |
} catch (e) {
|
| 38 |
setStatus(ui, 'failed');
|
|
@@ -49,7 +52,12 @@ function wireRun(ui: UI, sessions: NeedleSessions, tokenizer: Tokenizer, special
|
|
| 49 |
const query = ui.queryEl.value.trim();
|
| 50 |
if (!query) return;
|
| 51 |
running = true;
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
try {
|
| 54 |
const result = await generate(
|
| 55 |
sessions, tokenizer, query, tools.tools,
|
|
@@ -60,14 +68,20 @@ function wireRun(ui: UI, sessions: NeedleSessions, tokenizer: Tokenizer, special
|
|
| 60 |
maxNewTokens: 256,
|
| 61 |
},
|
| 62 |
(_id, decodedSoFar) => {
|
|
|
|
| 63 |
let display = decodedSoFar;
|
| 64 |
if (display.startsWith('<tool_call>')) display = display.slice('<tool_call>'.length);
|
| 65 |
renderResult(ui, display);
|
| 66 |
},
|
| 67 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
renderResult(ui, result.text);
|
| 69 |
-
setStatus(ui,
|
| 70 |
} catch (e) {
|
|
|
|
| 71 |
renderError(ui, `Generation failed: ${(e as Error).message}`);
|
| 72 |
setStatus(ui, 'ready');
|
| 73 |
} finally {
|
|
|
|
| 4 |
import { loadSessions } from './runtime';
|
| 5 |
import type { NeedleSessions } from './runtime';
|
| 6 |
import { generate } from './generate';
|
| 7 |
+
import { mountUI, setStatus, renderResult, renderError, readTools, setInteractiveEnabled } from './ui';
|
| 8 |
import type { UI } from './ui';
|
| 9 |
import { TOKENIZER_URL, SPECIALS_URL } from './config';
|
| 10 |
|
|
|
|
| 25 |
async function boot() {
|
| 26 |
const ui = mountUI();
|
| 27 |
try {
|
| 28 |
+
setStatus(ui, 'loading model…', true);
|
| 29 |
+
const t0 = performance.now();
|
| 30 |
const [sessions, tokenizerBytes, specials] = await Promise.all([
|
| 31 |
+
loadSessions(m => setStatus(ui, m, true)),
|
| 32 |
fetchBytes(TOKENIZER_URL),
|
| 33 |
fetchJson<Specials>(SPECIALS_URL),
|
| 34 |
]);
|
| 35 |
const tokenizer = await createTokenizer(tokenizerBytes);
|
| 36 |
+
const loadSecs = ((performance.now() - t0) / 1000).toFixed(1);
|
| 37 |
+
setStatus(ui, `ready · loaded in ${loadSecs}s`);
|
| 38 |
+
setInteractiveEnabled(ui, true);
|
| 39 |
wireRun(ui, sessions, tokenizer, specials);
|
| 40 |
} catch (e) {
|
| 41 |
setStatus(ui, 'failed');
|
|
|
|
| 52 |
const query = ui.queryEl.value.trim();
|
| 53 |
if (!query) return;
|
| 54 |
running = true;
|
| 55 |
+
let tokensSoFar = 0;
|
| 56 |
+
const t0 = performance.now();
|
| 57 |
+
const tick = setInterval(() => {
|
| 58 |
+
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
|
| 59 |
+
setStatus(ui, `generating… ${elapsed}s · ${tokensSoFar} tok`, true);
|
| 60 |
+
}, 100);
|
| 61 |
try {
|
| 62 |
const result = await generate(
|
| 63 |
sessions, tokenizer, query, tools.tools,
|
|
|
|
| 68 |
maxNewTokens: 256,
|
| 69 |
},
|
| 70 |
(_id, decodedSoFar) => {
|
| 71 |
+
tokensSoFar += 1;
|
| 72 |
let display = decodedSoFar;
|
| 73 |
if (display.startsWith('<tool_call>')) display = display.slice('<tool_call>'.length);
|
| 74 |
renderResult(ui, display);
|
| 75 |
},
|
| 76 |
);
|
| 77 |
+
clearInterval(tick);
|
| 78 |
+
const elapsedMs = performance.now() - t0;
|
| 79 |
+
const elapsed = (elapsedMs / 1000).toFixed(2);
|
| 80 |
+
const tps = (result.ids.length / (elapsedMs / 1000)).toFixed(1);
|
| 81 |
renderResult(ui, result.text);
|
| 82 |
+
setStatus(ui, `ready · ${elapsed}s · ${result.ids.length} tok · ${tps} tok/s`);
|
| 83 |
} catch (e) {
|
| 84 |
+
clearInterval(tick);
|
| 85 |
renderError(ui, `Generation failed: ${(e as Error).message}`);
|
| 86 |
setStatus(ui, 'ready');
|
| 87 |
} finally {
|
src/style.css
CHANGED
|
@@ -118,12 +118,29 @@ input#query {
|
|
| 118 |
padding: 4px 12px;
|
| 119 |
font-size: 12px;
|
| 120 |
cursor: pointer;
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
-
.example-chip:hover {
|
| 124 |
background: var(--bg-2);
|
| 125 |
}
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
.error-inline {
|
| 128 |
color: var(--error);
|
| 129 |
font-size: 12px;
|
|
|
|
| 118 |
padding: 4px 12px;
|
| 119 |
font-size: 12px;
|
| 120 |
cursor: pointer;
|
| 121 |
+
transition: opacity 120ms ease-in-out;
|
| 122 |
}
|
| 123 |
|
| 124 |
+
.example-chip:hover:not(:disabled) {
|
| 125 |
background: var(--bg-2);
|
| 126 |
}
|
| 127 |
|
| 128 |
+
.example-chip:disabled,
|
| 129 |
+
input#query:disabled,
|
| 130 |
+
textarea#tools:disabled {
|
| 131 |
+
opacity: 0.4;
|
| 132 |
+
cursor: not-allowed;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Subtle pulse on the status text while model is loading or generating. */
|
| 136 |
+
.loading-indicator.busy {
|
| 137 |
+
animation: pulse 1.2s ease-in-out infinite;
|
| 138 |
+
}
|
| 139 |
+
@keyframes pulse {
|
| 140 |
+
0%, 100% { opacity: 0.55; }
|
| 141 |
+
50% { opacity: 1; }
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
.error-inline {
|
| 145 |
color: var(--error);
|
| 146 |
font-size: 12px;
|
src/ui.ts
CHANGED
|
@@ -19,11 +19,13 @@ export function mountUI(): UI {
|
|
| 19 |
// Pre-populate tools textarea with DEFAULT_TOOLS
|
| 20 |
toolsEl.value = JSON.stringify(DEFAULT_TOOLS, null, 2);
|
| 21 |
|
| 22 |
-
// Render example chips
|
|
|
|
| 23 |
EXAMPLES.forEach((ex: Example) => {
|
| 24 |
const chip = document.createElement('button');
|
| 25 |
chip.className = 'example-chip';
|
| 26 |
chip.textContent = ex.label;
|
|
|
|
| 27 |
|
| 28 |
chip.addEventListener('click', () => {
|
| 29 |
// Set query value from example
|
|
@@ -52,8 +54,20 @@ export function mountUI(): UI {
|
|
| 52 |
};
|
| 53 |
}
|
| 54 |
|
| 55 |
-
export function setStatus(ui: UI, msg: string): void {
|
| 56 |
ui.statusEl.textContent = msg;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
export function renderResult(ui: UI, rawText: string): void {
|
|
|
|
| 19 |
// Pre-populate tools textarea with DEFAULT_TOOLS
|
| 20 |
toolsEl.value = JSON.stringify(DEFAULT_TOOLS, null, 2);
|
| 21 |
|
| 22 |
+
// Render example chips — disabled until the model finishes loading
|
| 23 |
+
queryEl.disabled = true;
|
| 24 |
EXAMPLES.forEach((ex: Example) => {
|
| 25 |
const chip = document.createElement('button');
|
| 26 |
chip.className = 'example-chip';
|
| 27 |
chip.textContent = ex.label;
|
| 28 |
+
chip.disabled = true;
|
| 29 |
|
| 30 |
chip.addEventListener('click', () => {
|
| 31 |
// Set query value from example
|
|
|
|
| 54 |
};
|
| 55 |
}
|
| 56 |
|
| 57 |
+
export function setStatus(ui: UI, msg: string, busy = false): void {
|
| 58 |
ui.statusEl.textContent = msg;
|
| 59 |
+
ui.statusEl.classList.toggle('busy', busy);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Toggle the disabled state of the example chips and the query input.
|
| 64 |
+
* Chips start disabled at mount; main.ts flips them on once the model finishes
|
| 65 |
+
* loading. Toggling off again during a busy / failed state is also valid.
|
| 66 |
+
*/
|
| 67 |
+
export function setInteractiveEnabled(ui: UI, enabled: boolean): void {
|
| 68 |
+
ui.queryEl.disabled = !enabled;
|
| 69 |
+
ui.examplesEl.querySelectorAll<HTMLButtonElement>('button.example-chip')
|
| 70 |
+
.forEach(btn => { btn.disabled = !enabled; });
|
| 71 |
}
|
| 72 |
|
| 73 |
export function renderResult(ui: UI, rawText: string): void {
|