Spaces:
Running on L4
Running on L4
Capture decisions: 5 LLM Explorer bug fixes
Browse files- Dark mode button text synced on page load via demo.load() JS
- Dark mode heading colors: setProperty with !important beats stylesheet
- Entropy NaN fix: filter probs > 0 instead of p + eps
- Merged Top-N + Top-K into single Top-K slider (5-100)
- Show-steps-off highlight: strip leading whitespace before span
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- app.py +40 -29
- config.json +2 -2
- models.py +11 -11
app.py
CHANGED
|
@@ -76,6 +76,8 @@ h1, h2, h3,
|
|
| 76 |
color: #63348d !important;
|
| 77 |
}
|
| 78 |
body.dark h1, body.dark h2, body.dark h3,
|
|
|
|
|
|
|
| 79 |
.dark .gradio-container h1, .dark .gradio-container h2, .dark .gradio-container h3 {
|
| 80 |
color: #ded9f4 !important;
|
| 81 |
}
|
|
@@ -146,13 +148,30 @@ body.dark, .dark {
|
|
| 146 |
}
|
| 147 |
"""
|
| 148 |
|
| 149 |
-
# Dark mode toggle JS — toggles class and swaps button text
|
| 150 |
DARK_MODE_JS = """
|
| 151 |
() => {
|
| 152 |
document.body.classList.toggle('dark');
|
| 153 |
const isDark = document.body.classList.contains('dark');
|
| 154 |
const el = document.getElementById('dark-mode-btn');
|
| 155 |
if (el) el.innerText = isDark ? 'Light mode' : 'Dark mode';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
}
|
| 157 |
"""
|
| 158 |
|
|
@@ -250,11 +269,14 @@ def _render_step_html(step_data: dict, prompt: str, prev_generated: str) -> str:
|
|
| 250 |
def _render_final_text_html(prompt: str, generated_text: str) -> str:
|
| 251 |
"""Render final text with all generated text highlighted (show-steps OFF mode)."""
|
| 252 |
generated = generated_text[len(prompt):]
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
return f"""
|
| 255 |
<div style="border:1px solid var(--llm-card-border);border-radius:8px;padding:16px;background:var(--llm-card-bg);">
|
| 256 |
<div style="font-family:monospace;font-size:16px;line-height:1.6;word-wrap:break-word;">
|
| 257 |
-
<span style="color:var(--llm-prompt-color);">{_esc(prompt)}</span><span style="background:var(--llm-highlight-bg);color:var(--llm-highlight-color);font-weight:600;padding:2px 6px;border-radius:4px;">{
|
| 258 |
</div>
|
| 259 |
</div>"""
|
| 260 |
|
|
@@ -286,7 +308,7 @@ def _render_tokens_html(tokens: list[tuple[str, int]]) -> str:
|
|
| 286 |
# Tab 1: Probability Explorer
|
| 287 |
# ---------------------------------------------------------------------------
|
| 288 |
|
| 289 |
-
def explore_probabilities(prompt, temperature,
|
| 290 |
"""Generate tokens step by step and return formatted HTML."""
|
| 291 |
if not manager.is_ready():
|
| 292 |
return f"<p style='color:red;'>{manager.status_message()}</p>"
|
|
@@ -296,7 +318,6 @@ def explore_probabilities(prompt, temperature, top_n, top_k, steps, show_steps,
|
|
| 296 |
prompt=prompt,
|
| 297 |
steps=int(steps),
|
| 298 |
temperature=temperature,
|
| 299 |
-
top_n=int(top_n),
|
| 300 |
top_k=int(top_k),
|
| 301 |
seed=seed,
|
| 302 |
show_steps=show_steps,
|
|
@@ -353,12 +374,12 @@ def admin_load_model(model_name):
|
|
| 353 |
return status, json.dumps(cfg, indent=2)
|
| 354 |
|
| 355 |
|
| 356 |
-
def admin_save_defaults(prompt, temperature,
|
| 357 |
"""Save default settings."""
|
| 358 |
manager.update_config(
|
| 359 |
default_prompt=prompt,
|
| 360 |
default_temperature=temperature,
|
| 361 |
-
|
| 362 |
default_steps=int(steps),
|
| 363 |
default_seed=int(seed),
|
| 364 |
)
|
|
@@ -416,27 +437,14 @@ def create_app():
|
|
| 416 |
elem_classes=["param-help"],
|
| 417 |
)
|
| 418 |
|
| 419 |
-
t1_top_n = gr.Slider(
|
| 420 |
-
label="Top-N display",
|
| 421 |
-
minimum=5, maximum=30, step=1,
|
| 422 |
-
value=cfg.get("default_top_n", 10),
|
| 423 |
-
)
|
| 424 |
-
gr.Markdown(
|
| 425 |
-
"How many candidate tokens to show in the probability table. "
|
| 426 |
-
"Only affects the display, not which token gets selected.",
|
| 427 |
-
elem_classes=["param-help"],
|
| 428 |
-
)
|
| 429 |
-
|
| 430 |
t1_top_k = gr.Slider(
|
| 431 |
-
label="Top-K
|
| 432 |
-
minimum=
|
| 433 |
-
value=
|
| 434 |
)
|
| 435 |
gr.Markdown(
|
| 436 |
-
"Limits which tokens the model
|
| 437 |
-
"
|
| 438 |
-
"At 40, only the 40 most likely tokens are considered "
|
| 439 |
-
"and everything else is ignored.",
|
| 440 |
elem_classes=["param-help"],
|
| 441 |
)
|
| 442 |
|
|
@@ -486,7 +494,7 @@ def create_app():
|
|
| 486 |
|
| 487 |
t1_generate_btn.click(
|
| 488 |
fn=explore_probabilities,
|
| 489 |
-
inputs=[t1_prompt, t1_temperature,
|
| 490 |
outputs=[t1_output],
|
| 491 |
)
|
| 492 |
|
|
@@ -553,9 +561,9 @@ def create_app():
|
|
| 553 |
label="Default temperature",
|
| 554 |
value=cfg.get("default_temperature", 0.8),
|
| 555 |
)
|
| 556 |
-
|
| 557 |
-
label="Default top-
|
| 558 |
-
value=cfg.get("
|
| 559 |
precision=0,
|
| 560 |
)
|
| 561 |
admin_steps = gr.Number(
|
|
@@ -596,10 +604,13 @@ def create_app():
|
|
| 596 |
# Save defaults
|
| 597 |
admin_save_btn.click(
|
| 598 |
fn=admin_save_defaults,
|
| 599 |
-
inputs=[admin_prompt, admin_temp,
|
| 600 |
outputs=[admin_save_msg],
|
| 601 |
)
|
| 602 |
|
|
|
|
|
|
|
|
|
|
| 603 |
return demo
|
| 604 |
|
| 605 |
|
|
|
|
| 76 |
color: #63348d !important;
|
| 77 |
}
|
| 78 |
body.dark h1, body.dark h2, body.dark h3,
|
| 79 |
+
body.dark .gradio-container h1, body.dark .gradio-container h2, body.dark .gradio-container h3,
|
| 80 |
+
.dark h1, .dark h2, .dark h3,
|
| 81 |
.dark .gradio-container h1, .dark .gradio-container h2, .dark .gradio-container h3 {
|
| 82 |
color: #ded9f4 !important;
|
| 83 |
}
|
|
|
|
| 148 |
}
|
| 149 |
"""
|
| 150 |
|
| 151 |
+
# Dark mode toggle JS — toggles class and swaps button text + heading colors
|
| 152 |
DARK_MODE_JS = """
|
| 153 |
() => {
|
| 154 |
document.body.classList.toggle('dark');
|
| 155 |
const isDark = document.body.classList.contains('dark');
|
| 156 |
const el = document.getElementById('dark-mode-btn');
|
| 157 |
if (el) el.innerText = isDark ? 'Light mode' : 'Dark mode';
|
| 158 |
+
document.querySelectorAll('h1, h2, h3').forEach(h => {
|
| 159 |
+
h.style.setProperty('color', isDark ? '#ded9f4' : '#63348d', 'important');
|
| 160 |
+
});
|
| 161 |
+
}
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
# Dark mode init JS — sync button text and heading colors on page load
|
| 165 |
+
DARK_MODE_INIT_JS = """
|
| 166 |
+
() => {
|
| 167 |
+
const isDark = document.body.classList.contains('dark');
|
| 168 |
+
const el = document.getElementById('dark-mode-btn');
|
| 169 |
+
if (el) el.innerText = isDark ? 'Light mode' : 'Dark mode';
|
| 170 |
+
if (isDark) {
|
| 171 |
+
document.querySelectorAll('h1, h2, h3').forEach(h => {
|
| 172 |
+
h.style.setProperty('color', '#ded9f4', 'important');
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
}
|
| 176 |
"""
|
| 177 |
|
|
|
|
| 269 |
def _render_final_text_html(prompt: str, generated_text: str) -> str:
|
| 270 |
"""Render final text with all generated text highlighted (show-steps OFF mode)."""
|
| 271 |
generated = generated_text[len(prompt):]
|
| 272 |
+
escaped = _esc(generated)
|
| 273 |
+
stripped = escaped.lstrip()
|
| 274 |
+
leading = escaped[:len(escaped) - len(stripped)]
|
| 275 |
|
| 276 |
return f"""
|
| 277 |
<div style="border:1px solid var(--llm-card-border);border-radius:8px;padding:16px;background:var(--llm-card-bg);">
|
| 278 |
<div style="font-family:monospace;font-size:16px;line-height:1.6;word-wrap:break-word;">
|
| 279 |
+
<span style="color:var(--llm-prompt-color);">{_esc(prompt)}</span>{leading}<span style="background:var(--llm-highlight-bg);color:var(--llm-highlight-color);font-weight:600;padding:2px 6px;border-radius:4px;">{stripped}</span>
|
| 280 |
</div>
|
| 281 |
</div>"""
|
| 282 |
|
|
|
|
| 308 |
# Tab 1: Probability Explorer
|
| 309 |
# ---------------------------------------------------------------------------
|
| 310 |
|
| 311 |
+
def explore_probabilities(prompt, temperature, top_k, steps, show_steps, seed):
|
| 312 |
"""Generate tokens step by step and return formatted HTML."""
|
| 313 |
if not manager.is_ready():
|
| 314 |
return f"<p style='color:red;'>{manager.status_message()}</p>"
|
|
|
|
| 318 |
prompt=prompt,
|
| 319 |
steps=int(steps),
|
| 320 |
temperature=temperature,
|
|
|
|
| 321 |
top_k=int(top_k),
|
| 322 |
seed=seed,
|
| 323 |
show_steps=show_steps,
|
|
|
|
| 374 |
return status, json.dumps(cfg, indent=2)
|
| 375 |
|
| 376 |
|
| 377 |
+
def admin_save_defaults(prompt, temperature, top_k, steps, seed):
|
| 378 |
"""Save default settings."""
|
| 379 |
manager.update_config(
|
| 380 |
default_prompt=prompt,
|
| 381 |
default_temperature=temperature,
|
| 382 |
+
default_top_k=int(top_k),
|
| 383 |
default_steps=int(steps),
|
| 384 |
default_seed=int(seed),
|
| 385 |
)
|
|
|
|
| 437 |
elem_classes=["param-help"],
|
| 438 |
)
|
| 439 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
t1_top_k = gr.Slider(
|
| 441 |
+
label="Top-K",
|
| 442 |
+
minimum=5, maximum=100, step=1,
|
| 443 |
+
value=cfg.get("default_top_k", 10),
|
| 444 |
)
|
| 445 |
gr.Markdown(
|
| 446 |
+
"Limits which tokens the model considers and how many "
|
| 447 |
+
"appear in the probability table.",
|
|
|
|
|
|
|
| 448 |
elem_classes=["param-help"],
|
| 449 |
)
|
| 450 |
|
|
|
|
| 494 |
|
| 495 |
t1_generate_btn.click(
|
| 496 |
fn=explore_probabilities,
|
| 497 |
+
inputs=[t1_prompt, t1_temperature, t1_top_k, t1_steps, t1_show_steps, t1_seed],
|
| 498 |
outputs=[t1_output],
|
| 499 |
)
|
| 500 |
|
|
|
|
| 561 |
label="Default temperature",
|
| 562 |
value=cfg.get("default_temperature", 0.8),
|
| 563 |
)
|
| 564 |
+
admin_top_k_admin = gr.Number(
|
| 565 |
+
label="Default top-k",
|
| 566 |
+
value=cfg.get("default_top_k", 10),
|
| 567 |
precision=0,
|
| 568 |
)
|
| 569 |
admin_steps = gr.Number(
|
|
|
|
| 604 |
# Save defaults
|
| 605 |
admin_save_btn.click(
|
| 606 |
fn=admin_save_defaults,
|
| 607 |
+
inputs=[admin_prompt, admin_temp, admin_top_k_admin, admin_steps, admin_seed],
|
| 608 |
outputs=[admin_save_msg],
|
| 609 |
)
|
| 610 |
|
| 611 |
+
# Sync dark mode button text on page load
|
| 612 |
+
demo.load(fn=None, js=DARK_MODE_INIT_JS)
|
| 613 |
+
|
| 614 |
return demo
|
| 615 |
|
| 616 |
|
config.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
{
|
| 2 |
-
"model": "
|
| 3 |
"default_prompt": "The best thing about Huston-Tillotson University is",
|
| 4 |
"default_temperature": 0.8,
|
| 5 |
-
"
|
| 6 |
"default_steps": 8,
|
| 7 |
"default_seed": 42
|
| 8 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"model": "Qwen2.5-3B",
|
| 3 |
"default_prompt": "The best thing about Huston-Tillotson University is",
|
| 4 |
"default_temperature": 0.8,
|
| 5 |
+
"default_top_k": 10,
|
| 6 |
"default_steps": 8,
|
| 7 |
"default_seed": 42
|
| 8 |
}
|
models.py
CHANGED
|
@@ -73,7 +73,7 @@ def _load_config() -> dict:
|
|
| 73 |
"model": DEFAULT_MODEL,
|
| 74 |
"default_prompt": "The best thing about Huston-Tillotson University is",
|
| 75 |
"default_temperature": 0.8,
|
| 76 |
-
"
|
| 77 |
"default_steps": 8,
|
| 78 |
"default_seed": 42,
|
| 79 |
}
|
|
@@ -223,8 +223,7 @@ class ModelManager:
|
|
| 223 |
@staticmethod
|
| 224 |
def entropy_bits(probs: torch.Tensor) -> float:
|
| 225 |
"""Shannon entropy in bits."""
|
| 226 |
-
|
| 227 |
-
p = probs + eps
|
| 228 |
return float(-torch.sum(p * torch.log2(p)))
|
| 229 |
|
| 230 |
def top_k_table(
|
|
@@ -247,13 +246,15 @@ class ModelManager:
|
|
| 247 |
prompt: str,
|
| 248 |
steps: int = 8,
|
| 249 |
temperature: float = 0.8,
|
| 250 |
-
|
| 251 |
-
top_k: int = 0,
|
| 252 |
seed: int = 42,
|
| 253 |
show_steps: bool = True,
|
| 254 |
) -> list[dict]:
|
| 255 |
"""Generate tokens one at a time, returning per-step data.
|
| 256 |
|
|
|
|
|
|
|
|
|
|
| 257 |
Each step dict contains:
|
| 258 |
- step: int (1-based)
|
| 259 |
- text: accumulated text so far
|
|
@@ -273,15 +274,14 @@ class ModelManager:
|
|
| 273 |
logits = self._get_logits(text)
|
| 274 |
|
| 275 |
# Apply top-k filtering before temperature
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
logits = mask
|
| 281 |
|
| 282 |
probs = self.apply_temperature(logits, temperature)
|
| 283 |
entropy = self.entropy_bits(probs)
|
| 284 |
-
top_tokens = self.top_k_table(probs, k=
|
| 285 |
|
| 286 |
# Temperature 0 = greedy (always pick highest probability)
|
| 287 |
if temperature == 0:
|
|
|
|
| 73 |
"model": DEFAULT_MODEL,
|
| 74 |
"default_prompt": "The best thing about Huston-Tillotson University is",
|
| 75 |
"default_temperature": 0.8,
|
| 76 |
+
"default_top_k": 10,
|
| 77 |
"default_steps": 8,
|
| 78 |
"default_seed": 42,
|
| 79 |
}
|
|
|
|
| 223 |
@staticmethod
|
| 224 |
def entropy_bits(probs: torch.Tensor) -> float:
|
| 225 |
"""Shannon entropy in bits."""
|
| 226 |
+
p = probs[probs > 0]
|
|
|
|
| 227 |
return float(-torch.sum(p * torch.log2(p)))
|
| 228 |
|
| 229 |
def top_k_table(
|
|
|
|
| 246 |
prompt: str,
|
| 247 |
steps: int = 8,
|
| 248 |
temperature: float = 0.8,
|
| 249 |
+
top_k: int = 10,
|
|
|
|
| 250 |
seed: int = 42,
|
| 251 |
show_steps: bool = True,
|
| 252 |
) -> list[dict]:
|
| 253 |
"""Generate tokens one at a time, returning per-step data.
|
| 254 |
|
| 255 |
+
top_k controls both sampling (only top-k tokens considered) and
|
| 256 |
+
how many tokens appear in the probability table.
|
| 257 |
+
|
| 258 |
Each step dict contains:
|
| 259 |
- step: int (1-based)
|
| 260 |
- text: accumulated text so far
|
|
|
|
| 274 |
logits = self._get_logits(text)
|
| 275 |
|
| 276 |
# Apply top-k filtering before temperature
|
| 277 |
+
top_k_vals, top_k_idxs = torch.topk(logits, k=min(top_k, logits.shape[0]))
|
| 278 |
+
mask = torch.full_like(logits, float("-inf"))
|
| 279 |
+
mask.scatter_(0, top_k_idxs, top_k_vals)
|
| 280 |
+
logits = mask
|
|
|
|
| 281 |
|
| 282 |
probs = self.apply_temperature(logits, temperature)
|
| 283 |
entropy = self.entropy_bits(probs)
|
| 284 |
+
top_tokens = self.top_k_table(probs, k=top_k) if show_steps else []
|
| 285 |
|
| 286 |
# Temperature 0 = greedy (always pick highest probability)
|
| 287 |
if temperature == 0:
|