Text Generation
Transformers
Safetensors
English
Chinese
spark2_5
sparkx2_5
html-generation
web-design
visual-ui
reasoning
sft
conversational
custom_code
Instructions to use wesjos/Spark-x2.6-4B-HTML with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use wesjos/Spark-x2.6-4B-HTML with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="wesjos/Spark-x2.6-4B-HTML", trust_remote_code=True) messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("wesjos/Spark-x2.6-4B-HTML", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use wesjos/Spark-x2.6-4B-HTML with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "wesjos/Spark-x2.6-4B-HTML" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "wesjos/Spark-x2.6-4B-HTML", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/wesjos/Spark-x2.6-4B-HTML
- SGLang
How to use wesjos/Spark-x2.6-4B-HTML with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "wesjos/Spark-x2.6-4B-HTML" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "wesjos/Spark-x2.6-4B-HTML", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "wesjos/Spark-x2.6-4B-HTML" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "wesjos/Spark-x2.6-4B-HTML", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use wesjos/Spark-x2.6-4B-HTML with Docker Model Runner:
docker model run hf.co/wesjos/Spark-x2.6-4B-HTML
Download webui/static/index.html from wesjos/Spark-x2.6-4B-HTML: direct link, hf CLI and curl.
- Browser
- Download file 59.3 kB
-
https://huggingface.co/wesjos/Spark-x2.6-4B-HTML/resolve/main/webui/static/index.html
- Command line
-
hf download hf://wesjos/Spark-x2.6-4B-HTML/webui/static/index.html
-
curl -L -o index.html https://huggingface.co/wesjos/Spark-x2.6-4B-HTML/resolve/main/webui/static/index.html
59.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mario-HTML Explorer</title>
<link rel="stylesheet" href="/static/theme.css">
<style id="custom-theme-style"></style>
<style>
:root {
--bg-page: #f8fafc;
--bg-surface: #ffffff;
--bg-surface-elevated: #f1f5f9;
--border-color: #e2e8f0;
--border-focus: #2563eb;
--text-main: #0f172a;
--text-sub: #64748b;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--accent-subtle: #eff6ff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-page);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
}
/* Header */
header {
background-color: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid var(--border-color);
padding: 12px 24px;
display: flex;
align-items: center;
justify-content: space-between;
backdrop-filter: blur(8px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-main);
letter-spacing: -0.02em;
}
.brand-badge {
background: var(--accent-subtle);
color: var(--accent);
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 999px;
font-weight: 600;
border: 1px solid #bfdbfe;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.status-pill {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
padding: 4px 12px;
border-radius: 999px;
background: var(--bg-surface);
border: 1px solid var(--border-color);
color: var(--text-sub);
cursor: pointer;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: all 0.2s;
}
.status-pill:hover { border-color: var(--accent); color: var(--text-main); }
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
.status-dot.error { background: #ef4444; }
.btn-icon {
background: var(--bg-surface);
border: 1px solid var(--border-color);
color: #334155;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: all 0.2s;
}
.btn-icon:hover {
background: var(--bg-surface-elevated);
border-color: #cbd5e1;
color: var(--text-main);
}
/* Main Chat Container */
#chat-container {
flex: 1;
overflow-y: auto;
padding: 24px 16px 220px 16px;
scroll-behavior: smooth;
}
.chat-inner {
max-width: 920px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 28px;
padding-bottom: 40px;
}
/* Welcome Banner */
.welcome-card {
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 32px 24px;
text-align: center;
margin-top: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.welcome-title {
font-size: 1.45rem;
font-weight: 700;
margin-bottom: 8px;
color: var(--text-main);
}
.welcome-desc {
color: var(--text-sub);
font-size: 0.95rem;
max-width: 620px;
margin: 0 auto 24px auto;
line-height: 1.6;
}
.chips-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.prompt-chip {
background: var(--bg-surface);
border: 1px solid var(--border-color);
padding: 7px 16px;
border-radius: 20px;
font-size: 0.85rem;
color: var(--text-main);
cursor: pointer;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
transition: all 0.2s;
}
.prompt-chip:hover {
background: var(--accent-subtle);
border-color: #93c5fd;
color: var(--accent);
transform: translateY(-1px);
}
/* Message Bubbles */
.message-row {
display: flex;
flex-direction: column;
gap: 8px;
animation: fadeIn 0.25s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.message-user {
align-self: flex-end;
max-width: 80%;
}
.user-bubble {
background: var(--accent);
color: #ffffff;
padding: 12px 18px;
border-radius: 14px 14px 2px 14px;
font-size: 0.95rem;
line-height: 1.5;
word-break: break-word;
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2);
}
.message-assistant {
align-self: stretch;
max-width: 100%;
}
/* Thinking Trace Block */
.thinking-box {
background: #f8fafc;
border: 1px solid var(--border-color);
border-left: 3px solid #6366f1;
border-radius: 8px;
margin-bottom: 12px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
.thinking-summary {
padding: 9px 14px;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
font-weight: 600;
color: #475569;
font-family: var(--font-mono, monospace);
cursor: pointer;
user-select: none;
background: #f1f5f9;
}
.thinking-summary:hover {
color: var(--text-main);
}
.thinking-summary::marker { display: none; }
.thinking-content {
padding: 14px 18px;
font-family: var(--font-mono, monospace);
font-size: 0.82rem;
color: #475569;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
border-top: 1px solid var(--border-color);
max-height: 380px;
overflow-y: auto;
background: #f8fafc;
}
/* HTML Content Canvas */
.html-canvas {
background: #ffffff;
color: #1e293b;
border-radius: 14px;
padding: 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
border: 1px solid var(--border-color);
overflow-x: auto;
}
.raw-source-view {
background: #0f172a;
color: #38bdf8;
font-family: var(--font-mono, monospace);
font-size: 0.82rem;
padding: 16px;
border-radius: 8px;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
border: 1px solid #1e293b;
display: none;
margin-top: 10px;
}
/* Action bar below message */
.msg-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
padding: 8px 14px;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 0.78rem;
color: var(--text-sub);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.msg-stats {
display: flex;
align-items: center;
gap: 12px;
font-family: var(--font-mono, monospace);
font-weight: 500;
}
.msg-actions {
display: flex;
align-items: center;
gap: 8px;
}
.action-btn {
background: var(--bg-surface-elevated);
border: 1px solid var(--border-color);
color: #334155;
font-size: 0.75rem;
font-weight: 500;
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: all 0.15s;
}
.action-btn:hover {
background: #e2e8f0;
color: var(--accent);
border-color: #cbd5e1;
}
/* Input Floating Bar */
.input-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, rgba(248, 250, 252, 0) 0%, rgba(248, 250, 252, 0.95) 25%, #f8fafc 100%);
padding: 16px 16px 22px 16px;
z-index: 10;
}
.input-box {
max-width: 920px;
margin: 0 auto;
background: var(--bg-surface);
border: 1px solid #cbd5e1;
border-radius: 14px;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 8px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.08), 0 8px 10px -6px rgba(0, 0, 0, 0.04);
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-box:focus-within {
border-color: var(--accent);
box-shadow: 0 10px 25px -5px rgba(37, 99, 235, 0.12), 0 8px 10px -6px rgba(37, 99, 235, 0.08);
}
textarea {
width: 100%;
background: transparent;
border: none;
outline: none;
color: var(--text-main);
font-size: 0.95rem;
font-family: inherit;
resize: none;
min-height: 44px;
max-height: 180px;
line-height: 1.5;
}
textarea::placeholder {
color: #94a3b8;
}
.input-bottom-row {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid #f1f5f9;
padding-top: 8px;
}
.toggle-label {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: var(--text-sub);
cursor: pointer;
user-select: none;
font-weight: 500;
}
.toggle-switch {
position: relative;
width: 34px;
height: 18px;
background: #cbd5e1;
border-radius: 999px;
transition: background 0.2s;
}
.toggle-switch.active { background: #6366f1; }
.toggle-knob {
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.toggle-switch.active .toggle-knob {
transform: translateX(16px);
}
.btn-send {
background: var(--accent);
color: #ffffff;
border: none;
padding: 7px 18px;
border-radius: 8px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-send:hover { background: var(--accent-hover); }
.btn-send:disabled {
background: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
/* Settings Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-overlay.open { display: flex; }
.modal-card {
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: 14px;
width: 90%;
max-width: 620px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: modalIn 0.2s ease-out;
}
@keyframes modalIn {
from { transform: scale(0.96); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-header h3 { font-size: 1.1rem; color: var(--text-main); }
.modal-close {
background: none;
border: none;
color: var(--text-sub);
font-size: 1.2rem;
cursor: pointer;
}
.modal-close:hover { color: var(--text-main); }
.modal-body {
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.modal-footer {
padding: 14px 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 10px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-main);
}
.form-desc {
font-size: 0.75rem;
color: var(--text-sub);
}
.input-text, select {
background: #ffffff;
border: 1px solid #cbd5e1;
color: var(--text-main);
padding: 8px 12px;
border-radius: 6px;
font-size: 0.85rem;
font-family: inherit;
outline: none;
}
.input-text:focus, select:focus {
border-color: var(--accent);
}
.tab-pills {
display: flex;
gap: 6px;
background: #f1f5f9;
padding: 4px;
border-radius: 8px;
}
.tab-pill {
flex: 1;
text-align: center;
padding: 6px 12px;
font-size: 0.8rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
color: var(--text-sub);
transition: all 0.2s;
}
.tab-pill.active {
background: #ffffff;
color: var(--text-main);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.modal-nav-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
background: #f8fafc;
border-top-left-radius: 14px;
border-top-right-radius: 14px;
}
.modal-nav-tab {
flex: 1;
text-align: center;
padding: 11px 14px;
font-size: 0.82rem;
font-weight: 600;
color: var(--text-sub);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.modal-nav-tab:hover {
color: var(--text-main);
background: #f1f5f9;
}
.modal-nav-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
background: #ffffff;
}
.preset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 8px;
}
.preset-btn {
background: #f8fafc;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 9px 12px;
font-size: 0.8rem;
font-weight: 600;
color: #334155;
cursor: pointer;
text-align: center;
transition: all 0.15s;
}
.preset-btn:hover {
background: var(--accent-subtle);
border-color: #93c5fd;
color: var(--accent);
}
.preset-btn.active {
background: var(--accent-subtle);
border-color: var(--accent);
color: var(--accent);
box-shadow: 0 0 0 1px var(--accent);
}
.snippet-pills {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.snippet-pill {
background: #f1f5f9;
border: 1px solid var(--border-color);
color: #475569;
font-size: 0.72rem;
padding: 3px 8px;
border-radius: 4px;
cursor: pointer;
font-family: var(--font-mono, monospace);
}
.snippet-pill:hover {
background: #e2e8f0;
color: var(--accent);
}
.slider-row {
display: flex;
align-items: center;
gap: 12px;
}
.slider-row input[type="range"] { flex: 1; }
.slider-val {
font-family: var(--font-mono, monospace);
font-size: 0.85rem;
width: 50px;
text-align: right;
color: var(--text-main);
}
</style>
</head>
<body>
<!-- Top Navigation -->
<header>
<div class="brand">
<span>🍄 Mario-HTML Explorer</span>
<span class="brand-badge">RL Math & HTML Native</span>
</div>
<div class="header-actions">
<div class="status-pill" id="status-pill" onclick="openSettings()">
<span class="status-dot" id="status-dot"></span>
<span id="status-text">Connecting...</span>
</div>
<button class="btn-icon" onclick="clearChat()">🧹 Clear</button>
<button class="btn-icon" onclick="openSettings()">⚙️ Settings</button>
</div>
</header>
<!-- Chat View -->
<main id="chat-container">
<div class="chat-inner" id="messages-list">
<!-- Welcome Card -->
<div class="welcome-card" id="welcome-card">
<div class="welcome-title">Spark-x2.6-4B · Mario-HTML</div>
<p class="welcome-desc">
Spark-x2.6-4B + LoRA 在 830 条马里奥 HTML 数据上 SFT(1.4 Epoch)。思维链保留、正文输出规范卡片,思考强度可在输入框左侧切换。
</p>
<div class="chips-grid">
<div class="prompt-chip" onclick="sendPreset('Solve the linear equation: 2x + 5 = 17 step by step')">📐 2x + 5 = 17 Equation</div>
<div class="prompt-chip" onclick="sendPreset('Explain Binary Search algorithm with complexity and steps')">⚡ Binary Search</div>
<div class="prompt-chip" onclick="sendPreset('Compare 9.11 and 9.9 — which one is larger and why?')">⚖️ 9.11 vs 9.9</div>
<div class="prompt-chip" onclick="sendPreset('Compare Microservices vs Monolith architecture in a modern comparison grid')">🏛️ Microservices vs Monolith</div>
</div>
</div>
</div>
</main>
<!-- Floating Input Bar -->
<div class="input-wrapper">
<div class="input-box">
<!-- Attached image preview bar -->
<div id="image-preview-bar" style="display:none; padding:8px 12px; align-items:center; gap:10px; background:#f1f5f9; border-radius:8px; margin-bottom:8px; border:1px solid #e2e8f0;">
<img id="attached-img-thumb" style="max-height:48px; max-width:64px; border-radius:6px; object-fit:cover; border:1px solid #cbd5e1;">
<span id="attached-img-name" style="font-size:0.8rem; color:#475569; flex-grow:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">image.png</span>
<button onclick="removeAttachedImage()" type="button" style="background:none; border:none; color:#ef4444; font-size:1.1rem; cursor:pointer; padding:0 4px; font-weight:bold;" title="Remove image">✕</button>
</div>
<textarea id="prompt-input" placeholder="Ask a question or request a structured HTML card presentation..." rows="1" onkeydown="handleKeyDown(event)"></textarea>
<div class="input-bottom-row">
<div style="display:flex; align-items:center; gap:10px;">
<label class="toggle-label" onclick="toggleThinking()">
<span class="toggle-switch active" id="thinking-switch">
<span class="toggle-knob"></span>
</span>
<span id="thinking-label">🧠 CoT: ON</span>
</label>
<select id="effort-select" onchange="setEffort(this.value)" title="思考强度:低 = 快速出卡,高 = 深度推演"
style="background:#ffffff;border:1px solid #cbd5e1;border-radius:8px;padding:5px 8px;font-size:0.82rem;color:#334155;cursor:pointer;">
<option value="low" selected>思考: 低(快速出卡,推荐)</option>
<option value="medium">思考: 中(标准推演)</option>
<option value="xhigh">思考: 高(深度推演)</option>
</select>
</div>
<button class="btn-send" id="send-btn" onclick="handleSendOrStop()">Send ↵</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal-overlay" id="settings-modal">
<div class="modal-card">
<div class="modal-header">
<h3>Model & Inference Settings</h3>
<button class="modal-close" onclick="closeSettings()">✕</button>
</div>
<div class="modal-nav-tabs">
<div class="modal-nav-tab active" id="nav-tab-conn" onclick="switchModalNav('conn')">🔌 Model & Engine</div>
<div class="modal-nav-tab" id="nav-tab-theme" onclick="switchModalNav('theme')">🎨 Styling & Theme</div>
<div class="modal-nav-tab" id="nav-tab-params" onclick="switchModalNav('params')">⚙️ Parameters</div>
</div>
<div class="modal-body">
<!-- SECTION 1: Model & Engine -->
<div id="modal-sec-conn" style="display: flex; flex-direction: column; gap: 16px;">
<div class="form-group">
<label>Backend Provider Mode</label>
<div class="tab-pills">
<div class="tab-pill active" id="tab-api" onclick="switchBackendTab('api')">Llama.cpp API Server</div>
<div class="tab-pill" id="tab-direct" onclick="switchBackendTab('direct')">Direct Model Path</div>
</div>
</div>
<!-- API Mode Section -->
<div id="section-api" style="display: flex; flex-direction: column; gap: 12px;">
<div class="form-group">
<label>Llama.cpp Server URL</label>
<input type="text" class="input-text" id="cfg-api-url" placeholder="http://127.0.0.1:18085">
<span class="form-desc">Compatible with standard OpenAI-style <code>/v1/chat/completions</code> endpoint.</span>
</div>
</div>
<!-- Direct Mode Section -->
<div id="section-direct" style="display: none; flex-direction: column; gap: 12px;">
<div class="form-group">
<label>Model Preset</label>
<select id="cfg-model-preset" onchange="onPresetChange()">
<option value="/mnt/sdb/models/qwen3.5-4b-html-v4-grpo-math-gguf/Qwen3.5-4B-HTML-v4-GRPO-Math-Q8_0.gguf">Qwen3.5-4B-HTML-Q8_0 (Recommended, 4.3GB)</option>
<option value="/mnt/sdb/models/qwen3.5-4b-html-v4-grpo-math-gguf/Qwen3.5-4B-HTML-v4-GRPO-Math-Q4_K_M.gguf">Qwen3.5-4B-HTML-Q4_K_M (Fast, 2.6GB)</option>
<option value="custom">Custom Path...</option>
</select>
</div>
<div class="form-group">
<label>GGUF Model Path</label>
<input type="text" class="input-text" id="cfg-model-path" placeholder="/mnt/sdb/models/...">
</div>
<div style="display: flex; gap: 12px;">
<div class="form-group" style="flex:1;">
<label>Context Window</label>
<input type="number" class="input-text" id="cfg-ctx" value="65536">
</div>
<div class="form-group" style="flex:1;">
<label>GPU Offload Layers</label>
<input type="number" class="input-text" id="cfg-ngl" value="99">
</div>
</div>
<button class="btn-icon" style="background:#2563eb; color:#fff; justify-content:center; padding:8px;" onclick="loadDirectModel()">
🚀 Load Model & Restart Llama Server
</button>
</div>
</div>
<!-- SECTION 2: Styling & Theme -->
<div id="modal-sec-theme" style="display: none; flex-direction: column; gap: 16px;">
<div class="form-group">
<label>Theme Presets (One-Click Style Switch)</label>
<div class="preset-grid">
<button type="button" class="preset-btn active" id="btn-theme-default" onclick="selectThemePreset('default')">Default Modern</button>
<button type="button" class="preset-btn" id="btn-theme-apple" onclick="selectThemePreset('apple')">Apple Clean</button>
<button type="button" class="preset-btn" id="btn-theme-notion" onclick="selectThemePreset('notion')">Notion Minimal</button>
<button type="button" class="preset-btn" id="btn-theme-cyber" onclick="selectThemePreset('cyber')">Cyber Neon</button>
<button type="button" class="preset-btn" id="btn-theme-editorial" onclick="selectThemePreset('editorial')">Editorial Serif</button>
</div>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label>Custom CSS Overrides</label>
<button type="button" class="action-link" style="font-size:0.75rem;" onclick="resetCustomCss()">Reset CSS</button>
</div>
<textarea class="input-text" id="cfg-custom-css" rows="8" placeholder="/* Enter custom CSS rules to customize .ai-view and cards */" oninput="applyLiveCustomCss(this.value)" style="font-size:0.78rem; font-family:var(--font-mono, monospace); line-height:1.5;"></textarea>
<span class="form-desc">Injected live in real-time. Overrides apply instantly to all cards & responses.</span>
<div class="snippet-pills">
<span style="font-size:0.72rem; color:var(--text-sub); align-self:center;">Quick Snippets:</span>
<button type="button" class="snippet-pill" onclick="insertCssSnippet('cards_rounded')">+ 24px Round Cards</button>
<button type="button" class="snippet-pill" onclick="insertCssSnippet('glassmorphism')">+ Frosted Glass</button>
<button type="button" class="snippet-pill" onclick="insertCssSnippet('soft_shadows')">+ Subtle Glow</button>
<button type="button" class="snippet-pill" onclick="insertCssSnippet('compact')">+ Compact Density</button>
</div>
</div>
</div>
<!-- SECTION 3: Parameters -->
<div id="modal-sec-params" style="display: none; flex-direction: column; gap: 16px;">
<div style="display:flex; justify-content:space-between; align-items:center; background:var(--bg-secondary); padding:10px 14px; border-radius:var(--radius-sm); border:1px solid var(--border);">
<div>
<div style="font-weight:600; font-size:0.85rem;">Sampling Decoding (Do Sample)</div>
<div style="font-size:0.72rem; color:var(--text-sub);">Enable probabilistic sampling. If disabled, uses greedy decoding (temp=0).</div>
</div>
<input type="checkbox" id="cfg-do-sample" style="width:18px; height:18px; accent-color:var(--accent); cursor:pointer;">
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between;">
<label>Temperature</label>
<span class="slider-val" id="temp-val">1.00</span>
</div>
<div class="slider-row">
<input type="range" id="cfg-temp" min="0" max="2.0" step="0.05" oninput="document.getElementById('temp-val').innerText = parseFloat(this.value).toFixed(2)">
</div>
<span class="form-desc">Higher = more creative; lower = more focused and deterministic. Default: 1.0.</span>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between;">
<label>Top-P (Nucleus Sampling)</label>
<span class="slider-val" id="topp-val">0.95</span>
</div>
<div class="slider-row">
<input type="range" id="cfg-top-p" min="0" max="1.0" step="0.05" oninput="document.getElementById('topp-val').innerText = parseFloat(this.value).toFixed(2)">
</div>
<span class="form-desc">Limits cumulative probability mass of top candidates. Default: 0.95.</span>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between;">
<label>Top-K Sampling</label>
<span class="slider-val" id="topk-val">20</span>
</div>
<div class="slider-row">
<input type="range" id="cfg-top-k" min="0" max="100" step="1" oninput="document.getElementById('topk-val').innerText = this.value === '0' ? 'Off' : this.value">
</div>
<span class="form-desc">Sample only from top K tokens. Set to 0 to disable. Default: 20.</span>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between;">
<label>Min-P Sampling</label>
<span class="slider-val" id="minp-val">0.00</span>
</div>
<div class="slider-row">
<input type="range" id="cfg-min-p" min="0.0" max="1.0" step="0.01" oninput="document.getElementById('minp-val').innerText = parseFloat(this.value).toFixed(2)">
</div>
<span class="form-desc">Minimum probability threshold relative to most likely token. Default: 0.00.</span>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between;">
<label>Presence Penalty (存在惩罚)</label>
<span class="slider-val" id="pres-val">1.50</span>
</div>
<div class="slider-row">
<input type="range" id="cfg-pres-penalty" min="-2.0" max="2.0" step="0.1" oninput="document.getElementById('pres-val').innerText = parseFloat(this.value).toFixed(2)">
</div>
<span class="form-desc">Penalize tokens based on presence in text to encourage new topics. Default: 1.50.</span>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between;">
<label>Repetition Penalty (重复性惩罚)</label>
<span class="slider-val" id="rep-val">1.00</span>
</div>
<div class="slider-row">
<input type="range" id="cfg-rep-penalty" min="1.0" max="2.0" step="0.02" oninput="document.getElementById('rep-val').innerText = parseFloat(this.value).toFixed(2)">
</div>
<span class="form-desc">Penalize recurring tokens to prevent repetitive n-gram loops. Default: 1.00.</span>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between;">
<label>Max Output Tokens (提高至最大 Context 80K)</label>
<span class="slider-val" id="max-val">8192</span>
</div>
<div class="slider-row">
<input type="range" id="cfg-max-tokens" min="512" max="81920" step="512" oninput="document.getElementById('max-val').innerText = this.value">
</div>
<div class="snippet-pills" style="margin-top:6px;">
<span style="font-size:0.72rem; color:var(--text-sub); align-self:center;">Presets:</span>
<button type="button" class="snippet-pill" onclick="setMaxTokensPreset(4096)">4K</button>
<button type="button" class="snippet-pill" onclick="setMaxTokensPreset(8192)">8K</button>
<button type="button" class="snippet-pill" onclick="setMaxTokensPreset(16384)">16K</button>
<button type="button" class="snippet-pill" onclick="setMaxTokensPreset(32768)">32K</button>
<button type="button" class="snippet-pill" onclick="setMaxTokensPreset(65536)">64K</button>
<button type="button" class="snippet-pill" onclick="setMaxTokensPreset(81920)">80K Max</button>
</div>
<span class="form-desc">Maximum generation token limit per response (up to full 80K context window).</span>
</div>
<div class="form-group">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label>System Prompt (留空默认 Mario Zero-Prompt)</label>
<button type="button" class="action-link" style="font-size:0.72rem;" onclick="document.getElementById('cfg-system-prompt').value=''">Clear Prompt</button>
</div>
<textarea class="input-text" id="cfg-system-prompt" rows="4" placeholder="Leave empty to use native Mario card-generating persona" style="font-size:0.75rem; font-family:var(--font-mono, monospace);"></textarea>
<span class="form-desc">Default is empty. Mario natively formats visual HTML cards without manual prompts.</span>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-icon" onclick="closeSettings()">Cancel</button>
<button class="btn-icon" style="background:var(--accent); color:#ffffff;" onclick="saveSettings()">Save Settings</button>
</div>
</div>
</div>
<script>
let appConfig = {};
let isGenerating = false;
let conversationHistory = [];
const THEME_PRESETS = {
default: `/* Default Modern Theme */`,
apple: `/* Apple Clean */
.html-canvas {
border-radius: 20px !important;
background: #fbfbfd !important;
border: 1px solid rgba(0,0,0,0.06) !important;
box-shadow: 0 10px 30px rgba(0,0,0,0.05) !important;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif !important;
}
.card {
border-radius: 16px !important;
border: 1px solid rgba(0,0,0,0.06) !important;
background: #ffffff !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.04) !important;
}
.summary-card {
border-radius: 16px !important;
border-left-width: 6px !important;
}
.callout {
border-radius: 14px !important;
}
.badge {
border-radius: 8px !important;
font-weight: 600 !important;
}`,
notion: `/* Notion Minimal */
.html-canvas {
border-radius: 4px !important;
border: 1px solid #37352f !important;
box-shadow: none !important;
background: #ffffff !important;
}
.card {
border-radius: 4px !important;
border: 1px solid #e3e2e0 !important;
box-shadow: none !important;
}
.summary-card {
border-radius: 4px !important;
border-left-color: #37352f !important;
}
.callout {
border-radius: 4px !important;
border-left-width: 4px !important;
}
.badge {
border-radius: 3px !important;
font-weight: 500 !important;
}`,
cyber: `/* Cyber Neon */
.html-canvas {
background: #090d16 !important;
color: #e2e8f0 !important;
border: 1px solid #00f2ff !important;
box-shadow: 0 0 25px rgba(0, 242, 255, 0.25) !important;
}
.card {
background: #111827 !important;
border: 1px solid #1f2937 !important;
color: #f8fafc !important;
}
.summary-card {
background: #111827 !important;
border-left-color: #00f2ff !important;
}
.modern-table th {
background: #1e293b !important;
color: #00f2ff !important;
}
.badge-accent {
background: rgba(0, 242, 255, 0.2) !important;
color: #00f2ff !important;
border: 1px solid #00f2ff !important;
}`,
editorial: `/* Editorial Serif */
.html-canvas {
background: #fdfbf7 !important;
color: #2c2523 !important;
font-family: "Georgia", "Times New Roman", serif !important;
border: 1px solid #e7dfd5 !important;
}
.card {
background: #ffffff !important;
border: 1px solid #e7dfd5 !important;
box-shadow: 0 2px 4px rgba(44, 37, 35, 0.04) !important;
}
h2, h3, h4 {
font-family: "Georgia", serif !important;
font-weight: 600 !important;
}
.modern-table {
border-top: 2px solid #2c2523 !important;
border-bottom: 2px solid #2c2523 !important;
}`
};
const CSS_SNIPPETS = {
cards_rounded: `\n/* Rounded Cards */\n.card, .html-canvas { border-radius: 24px !important; }\n`,
glassmorphism: `\n/* Frosted Glass */\n.card {\n background: rgba(255, 255, 255, 0.8) !important;\n backdrop-filter: blur(12px) !important;\n border: 1px solid rgba(255, 255, 255, 0.6) !important;\n}\n`,
soft_shadows: `\n/* Subtle Glow */\n.card:hover {\n box-shadow: 0 10px 25px -5px rgba(37, 99, 235, 0.15) !important;\n border-color: #93c5fd !important;\n}\n`,
compact: `\n/* Compact Density */\n.ai-view { gap: 12px !important; }\n.card { padding: 14px 18px !important; }\n`
};
function switchModalNav(tab) {
document.getElementById("nav-tab-conn").className = tab === 'conn' ? "modal-nav-tab active" : "modal-nav-tab";
document.getElementById("nav-tab-theme").className = tab === 'theme' ? "modal-nav-tab active" : "modal-nav-tab";
document.getElementById("nav-tab-params").className = tab === 'params' ? "modal-nav-tab active" : "modal-nav-tab";
document.getElementById("modal-sec-conn").style.display = tab === 'conn' ? 'flex' : 'none';
document.getElementById("modal-sec-theme").style.display = tab === 'theme' ? 'flex' : 'none';
document.getElementById("modal-sec-params").style.display = tab === 'params' ? 'flex' : 'none';
}
function applyLiveCustomCss(cssText) {
let tag = document.getElementById("custom-theme-style");
if (!tag) {
tag = document.createElement("style");
tag.id = "custom-theme-style";
document.head.appendChild(tag);
}
tag.textContent = cssText || "";
}
function selectThemePreset(name) {
appConfig.theme_preset = name;
document.querySelectorAll(".preset-btn").forEach(b => b.classList.remove("active"));
const activeBtn = document.getElementById("btn-theme-" + name);
if (activeBtn) activeBtn.classList.add("active");
const css = THEME_PRESETS[name] || "";
document.getElementById("cfg-custom-css").value = css;
applyLiveCustomCss(css);
}
function insertCssSnippet(key) {
const snip = CSS_SNIPPETS[key] || "";
const tx = document.getElementById("cfg-custom-css");
tx.value = (tx.value + "\n" + snip).trim();
applyLiveCustomCss(tx.value);
}
function resetCustomCss() {
selectThemePreset("default");
}
// Initialize
window.addEventListener("DOMContentLoaded", async () => {
await fetchConfig();
await checkStatus();
setInterval(checkStatus, 5000);
autoResizeTextarea();
});
async function fetchConfig() {
try {
const res = await fetch("/api/config");
appConfig = await res.json();
// 思考强度:localStorage 记住上次选择,否则用 config 里的值(low)
let eff = appConfig.reasoning_effort || "low";
try { eff = localStorage.getItem("reasoning_effort") || eff; } catch (e) {}
appConfig.reasoning_effort = eff;
const effSel = document.getElementById("effort-select");
if (effSel) effSel.value = eff;
const preset = appConfig.theme_preset || "default";
const css = appConfig.custom_css || (preset !== 'default' ? THEME_PRESETS[preset] : "");
applyLiveCustomCss(css);
updateSettingsModalForm();
} catch (e) {
console.error("Failed to load config", e);
}
}
async function checkStatus() {
try {
const res = await fetch("/api/status");
const data = await res.json();
const dot = document.getElementById("status-dot");
const txt = document.getElementById("status-text");
if (data.status === "ready") {
dot.className = "status-dot";
txt.innerText = `${data.model_info} (${data.latency_ms || 0}ms)`;
} else {
dot.className = "status-dot error";
txt.innerText = "Server Offline";
}
} catch (e) {
document.getElementById("status-dot").className = "status-dot error";
document.getElementById("status-text").innerText = "Connection Failed";
}
}
function setEffort(v) {
appConfig.reasoning_effort = v;
try { localStorage.setItem("reasoning_effort", v); } catch (e) {}
}
function toggleThinking() {
appConfig.enable_thinking = !appConfig.enable_thinking;
const sw = document.getElementById("thinking-switch");
const lbl = document.getElementById("thinking-label");
if (appConfig.enable_thinking) {
sw.className = "toggle-switch active";
lbl.innerText = "🧠 CoT Reasoning: ON";
} else {
sw.className = "toggle-switch";
lbl.innerText = "⚡ Direct HTML (No CoT)";
}
}
function switchBackendTab(mode) {
document.getElementById("tab-api").className = mode === 'api' ? "tab-pill active" : "tab-pill";
document.getElementById("tab-direct").className = mode === 'direct' ? "tab-pill active" : "tab-pill";
document.getElementById("section-api").style.display = mode === 'api' ? 'flex' : 'none';
document.getElementById("section-direct").style.display = mode === 'direct' ? 'flex' : 'none';
appConfig.backend_mode = mode;
}
function onPresetChange() {
const sel = document.getElementById("cfg-model-preset").value;
if (sel !== "custom") {
document.getElementById("cfg-model-path").value = sel;
}
}
function openSettings() {
updateSettingsModalForm();
document.getElementById("settings-modal").classList.add("open");
}
function closeSettings() {
document.getElementById("settings-modal").classList.remove("open");
}
function setMaxTokensPreset(val) {
document.getElementById("cfg-max-tokens").value = val;
document.getElementById("max-val").innerText = val;
}
function updateSettingsModalForm() {
document.getElementById("cfg-api-url").value = appConfig.api_url || "http://127.0.0.1:18085";
document.getElementById("cfg-model-path").value = appConfig.model_path || "";
document.getElementById("cfg-ctx").value = appConfig.context_size || 81920;
document.getElementById("cfg-ngl").value = appConfig.gpu_layers || 99;
const doSample = appConfig.do_sample !== false;
document.getElementById("cfg-do-sample").checked = doSample;
const temp = appConfig.temperature ?? 1.0;
document.getElementById("cfg-temp").value = temp;
document.getElementById("temp-val").innerText = parseFloat(temp).toFixed(2);
const topP = appConfig.top_p ?? 0.95;
document.getElementById("cfg-top-p").value = topP;
document.getElementById("topp-val").innerText = parseFloat(topP).toFixed(2);
const topK = appConfig.top_k ?? 20;
document.getElementById("cfg-top-k").value = topK;
document.getElementById("topk-val").innerText = topK <= 0 ? "Off" : topK;
const minP = appConfig.min_p ?? 0.0;
document.getElementById("cfg-min-p").value = minP;
document.getElementById("minp-val").innerText = parseFloat(minP).toFixed(2);
const presPen = appConfig.presence_penalty ?? 1.5;
document.getElementById("cfg-pres-penalty").value = presPen;
document.getElementById("pres-val").innerText = parseFloat(presPen).toFixed(2);
const repPen = appConfig.repetition_penalty ?? 1.0;
document.getElementById("cfg-rep-penalty").value = repPen;
document.getElementById("rep-val").innerText = parseFloat(repPen).toFixed(2);
const maxTok = appConfig.max_tokens ?? 8192;
document.getElementById("cfg-max-tokens").value = maxTok;
document.getElementById("max-val").innerText = maxTok;
document.getElementById("cfg-system-prompt").value = appConfig.system_prompt || "";
const preset = appConfig.theme_preset || "default";
document.querySelectorAll(".preset-btn").forEach(b => b.classList.remove("active"));
const btn = document.getElementById("btn-theme-" + preset);
if (btn) btn.classList.add("active");
document.getElementById("cfg-custom-css").value = appConfig.custom_css || (preset !== 'default' ? THEME_PRESETS[preset] : "");
switchBackendTab(appConfig.backend_mode || 'api');
}
async function saveSettings() {
const payload = {
backend_mode: appConfig.backend_mode,
api_url: document.getElementById("cfg-api-url").value,
do_sample: document.getElementById("cfg-do-sample").checked,
temperature: parseFloat(document.getElementById("cfg-temp").value),
top_p: parseFloat(document.getElementById("cfg-top-p").value),
top_k: parseInt(document.getElementById("cfg-top-k").value),
min_p: parseFloat(document.getElementById("cfg-min-p").value),
presence_penalty: parseFloat(document.getElementById("cfg-pres-penalty").value),
repetition_penalty: parseFloat(document.getElementById("cfg-rep-penalty").value),
max_tokens: parseInt(document.getElementById("cfg-max-tokens").value),
system_prompt: document.getElementById("cfg-system-prompt").value,
enable_thinking: appConfig.enable_thinking,
theme_preset: appConfig.theme_preset || "default",
custom_css: document.getElementById("cfg-custom-css").value
};
await fetch("/api/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
closeSettings();
await fetchConfig();
await checkStatus();
}
async function loadDirectModel() {
const path = document.getElementById("cfg-model-path").value.trim();
const ctx = parseInt(document.getElementById("cfg-ctx").value);
const ngl = parseInt(document.getElementById("cfg-ngl").value);
if (!path) {
alert("Please enter a valid model path.");
return;
}
const btn = event.target;
const origText = btn.innerText;
btn.innerText = "⏳ Loading Model...";
btn.disabled = true;
try {
const res = await fetch("/api/model/load", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model_path: path, context_size: ctx, gpu_layers: ngl })
});
const data = await res.json();
if (res.ok) {
alert(data.message || "Model loaded successfully!");
closeSettings();
await fetchConfig();
await checkStatus();
} else {
alert("Error: " + (data.error || "Failed to load model."));
}
} catch (err) {
alert("Error connecting to server: " + err.message);
} finally {
btn.innerText = origText;
btn.disabled = false;
}
}
function clearChat() {
conversationHistory = [];
const list = document.getElementById("messages-list");
list.innerHTML = `
<div class="welcome-card" id="welcome-card">
<div class="welcome-title">Qwen3.8-27B · Mario-HTML LoRA</div>
<p class="welcome-desc">
Qwen3.8-27B + QLoRA(r16,仅 attention 层)在 830 条马里奥 HTML 数据上 SFT。思维链保留、只改造正文输出,思考强度可在输入框左侧切换。
</p>
<div class="chips-grid">
<div class="prompt-chip" onclick="sendPreset('Solve the linear equation: 2x + 5 = 17 step by step')">📐 2x + 5 = 17 Equation</div>
<div class="prompt-chip" onclick="sendPreset('Explain Binary Search algorithm with complexity and steps')">⚡ Binary Search</div>
<div class="prompt-chip" onclick="sendPreset('Compare 9.11 and 9.9 — which one is larger and why?')">⚖️ 9.11 vs 9.9</div>
<div class="prompt-chip" onclick="sendPreset('Compare Microservices vs Monolith architecture in a modern comparison grid')">🏛️ Microservices vs Monolith</div>
</div>
</div>
`;
}
function sendPreset(text) {
document.getElementById("prompt-input").value = text;
sendMessage();
}
function handleKeyDown(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function autoResizeTextarea() {
const tx = document.getElementById("prompt-input");
tx.addEventListener("input", () => {
tx.style.height = "auto";
tx.style.height = Math.min(tx.scrollHeight, 180) + "px";
});
}
let attachedImageBase64 = null;
function handleImageFileSelect(e) {
const file = e.target.files[0];
if (file) loadImageFile(file);
e.target.value = "";
}
function loadImageFile(file) {
if (!file || !file.type.startsWith("image/")) return;
const reader = new FileReader();
reader.onload = function(evt) {
attachedImageBase64 = evt.target.result;
document.getElementById("attached-img-thumb").src = attachedImageBase64;
document.getElementById("attached-img-name").innerText = `${file.name || 'Image'} (${(file.size / 1024).toFixed(1)} KB)`;
document.getElementById("image-preview-bar").style.display = "flex";
};
reader.readAsDataURL(file);
}
function removeAttachedImage() {
attachedImageBase64 = null;
document.getElementById("image-preview-bar").style.display = "none";
document.getElementById("attached-img-thumb").src = "";
}
// Direct clipboard paste support
window.addEventListener("paste", function(e) {
const items = (e.clipboardData || window.clipboardData).items;
for (let item of items) {
if (item.type.indexOf("image") !== -1) {
const file = item.getAsFile();
loadImageFile(file);
break;
}
}
});
let activeAbortController = null;
function handleSendOrStop() {
if (isGenerating) {
stopGenerating();
} else {
sendMessage();
}
}
function stopGenerating() {
if (activeAbortController) {
try {
activeAbortController.abort();
} catch (_) {}
activeAbortController = null;
}
isGenerating = false;
const sendBtn = document.getElementById("send-btn");
if (sendBtn) {
sendBtn.innerText = "Send ↵";
sendBtn.style.background = "var(--accent)";
}
}
async function sendMessage() {
if (isGenerating) return;
const input = document.getElementById("prompt-input");
const text = input.value.trim();
const currentImg = attachedImageBase64;
if (!text && !currentImg) return;
input.value = "";
input.style.height = "44px";
removeAttachedImage();
// Hide welcome card if present
const welcome = document.getElementById("welcome-card");
if (welcome) welcome.remove();
// Append user bubble
appendUserMessage(text, currentImg);
if (currentImg) {
conversationHistory.push({
role: "user",
content: [
{ type: "text", text: text || "Please analyze and describe this image." },
{ type: "image_url", image_url: { url: currentImg } }
]
});
} else {
conversationHistory.push({ role: "user", content: text });
}
// Create assistant container
const { row, thinkingBody, htmlCanvas, rawSource, statsSpan } = createAssistantRow();
isGenerating = true;
activeAbortController = new AbortController();
const sendBtn = document.getElementById("send-btn");
sendBtn.innerText = "Stop ⏹";
sendBtn.style.background = "#ef4444";
let accumulatedThought = "";
let accumulatedContent = "";
try {
const response = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: activeAbortController.signal,
body: JSON.stringify({
messages: conversationHistory,
enable_thinking: appConfig.enable_thinking,
reasoning_effort: appConfig.reasoning_effort || "low",
do_sample: appConfig.do_sample,
temperature: appConfig.temperature,
top_p: appConfig.top_p,
top_k: appConfig.top_k,
min_p: appConfig.min_p,
presence_penalty: appConfig.presence_penalty,
repetition_penalty: appConfig.repetition_penalty,
max_tokens: appConfig.max_tokens,
system_prompt: appConfig.system_prompt
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let streamBuffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
streamBuffer += decoder.decode(value, { stream: true });
const lines = streamBuffer.split("\n");
// Keep the last segment in streamBuffer (it might be truncated or empty)
streamBuffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data: ")) continue;
const dataStr = trimmed.slice(6).trim();
if (!dataStr) continue;
let packet;
try {
packet = JSON.parse(dataStr);
} catch (jsonErr) {
console.warn("Skipping incomplete/malformed SSE chunk:", jsonErr, dataStr);
continue;
}
if (packet.type === "thought") {
accumulatedThought += packet.content;
thinkingBody.innerText = accumulatedThought;
scrollToBottom();
} else if (packet.type === "content") {
accumulatedContent += packet.content;
if (accumulatedContent.includes("<think>")) {
const tm = accumulatedContent.match(/<think>([\s\S]*?)(?:<\/think>|$)/i);
if (tm && tm[1]) {
thinkingBody.innerText = tm[1].trim();
}
}
renderHtmlView(htmlCanvas, rawSource, accumulatedContent);
scrollToBottom();
} else if (packet.type === "done") {
statsSpan.innerText = `⏱️ ${packet.elapsed}s | ⚡ ${packet.tok_per_sec} tok/s | 📊 ${packet.tokens} tokens`;
conversationHistory.push({ role: "assistant", content: accumulatedContent });
} else if (packet.type === "error") {
htmlCanvas.innerHTML = `<div style="color:#ef4444; padding:12px;"><strong>Error:</strong> ${packet.error}</div>`;
}
}
}
// Process any residual trailing line in streamBuffer
if (streamBuffer.trim().startsWith("data: ")) {
try {
const packet = JSON.parse(streamBuffer.trim().slice(6).trim());
if (packet.type === "done") {
statsSpan.innerText = `⏱️ ${packet.elapsed}s | ⚡ ${packet.tok_per_sec} tok/s | 📊 ${packet.tokens} tokens`;
conversationHistory.push({ role: "assistant", content: accumulatedContent });
}
} catch (_) {}
}
} catch (err) {
if (err.name === "AbortError" || String(err).includes("aborted")) {
statsSpan.innerText = (statsSpan.innerText ? statsSpan.innerText + " | " : "") + "⏹ Stopped by user";
conversationHistory.push({ role: "assistant", content: accumulatedContent });
} else {
htmlCanvas.innerHTML = `<div style="color:#ef4444; padding:12px;"><strong>Network Error:</strong> ${err.message}</div>`;
}
} finally {
isGenerating = false;
activeAbortController = null;
const sendBtn = document.getElementById("send-btn");
if (sendBtn) {
sendBtn.innerText = "Send ↵";
sendBtn.style.background = "var(--accent)";
}
}
}
function appendUserMessage(text, imgSrc) {
const list = document.getElementById("messages-list");
const row = document.createElement("div");
row.className = "message-row message-user";
let imgHtml = "";
if (imgSrc) {
imgHtml = `<div style="margin-bottom:8px;"><img src="${imgSrc}" style="max-width:240px; max-height:180px; border-radius:8px; border:1px solid rgba(0,0,0,0.1); display:block; object-fit:cover;"></div>`;
}
row.innerHTML = `<div class="user-bubble">${imgHtml}${escapeHtml(text)}</div>`;
list.appendChild(row);
scrollToBottom();
}
function createAssistantRow() {
const list = document.getElementById("messages-list");
const row = document.createElement("div");
row.className = "message-row message-assistant";
const showThinking = appConfig.enable_thinking;
row.innerHTML = `
<details class="thinking-box" ${showThinking ? 'open' : ''} style="${showThinking ? '' : 'display:none;'}">
<summary class="thinking-summary">
<span>🧠 Chain of Thought</span>
<span style="margin-left:auto; font-size:0.75rem;">▼ Toggle</span>
</summary>
<div class="thinking-content">Thinking in progress...</div>
</details>
<div class="html-canvas">Generating visual presentation...</div>
<div class="raw-source-view"></div>
<div class="msg-footer">
<div class="msg-stats"><span>Generating...</span></div>
<div class="msg-actions">
<button class="action-btn" onclick="toggleRawView(this)">👁️ View Source</button>
<button class="action-btn" onclick="copyHtml(this)">📋 Copy HTML</button>
</div>
</div>
`;
list.appendChild(row);
scrollToBottom();
const thinkingBody = row.querySelector(".thinking-content");
const htmlCanvas = row.querySelector(".html-canvas");
const rawSource = row.querySelector(".raw-source-view");
const statsSpan = row.querySelector(".msg-stats span");
return { row, thinkingBody, htmlCanvas, rawSource, statsSpan };
}
function renderHtmlView(canvas, rawView, text) {
// 1. Strip any thinking block from HTML canvas view
let clean = text.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
clean = clean.replace(/<think>[\s\S]*$/gi, "").trim();
// 2. Strip code fences if model ever outputs them
if (clean.startsWith("```html")) clean = clean.slice(7);
if (clean.startsWith("```")) clean = clean.slice(3);
if (clean.endsWith("```")) clean = clean.slice(0, -3);
clean = clean.trim();
const startIdx = clean.indexOf('<div class="ai-view">');
if (startIdx !== -1) {
clean = clean.slice(startIdx);
} else if (!clean) {
// If content is still in thinking phase
canvas.innerHTML = '<div style="color:var(--text-sub); font-size:0.85rem; padding:16px 8px; display:flex; align-items:center; gap:8px;"><span>🧠</span><span>Thinking & formatting UI layout...</span></div>';
rawView.innerText = "";
return;
}
rawView.innerText = clean;
canvas.innerHTML = clean;
}
function toggleRawView(btn) {
const row = btn.closest(".message-assistant");
const canvas = row.querySelector(".html-canvas");
const raw = row.querySelector(".raw-source-view");
if (raw.style.display === "block") {
raw.style.display = "none";
canvas.style.display = "block";
btn.innerText = "View Source";
} else {
raw.style.display = "block";
canvas.style.display = "none";
btn.innerText = "View Preview";
}
}
function copyHtml(btn) {
const row = btn.closest(".message-assistant");
const raw = row.querySelector(".raw-source-view").innerText;
navigator.clipboard.writeText(raw);
const orig = btn.innerText;
btn.innerText = "Copied! ✓";
setTimeout(() => btn.innerText = orig, 1500);
}
function scrollToBottom() {
const container = document.getElementById("chat-container");
container.scrollTop = container.scrollHeight;
}
function escapeHtml(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
</script>
</body>
</html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mario-HTML Explorer</title>
<link rel="stylesheet" href="/static/theme.css">
<style id="custom-theme-style"></style>
<style>
:root {
--bg-page: #f8fafc;
--bg-surface: #ffffff;
--bg-surface-elevated: #f1f5f9;
--border-color: #e2e8f0;
--border-focus: #2563eb;
--text-main: #0f172a;
--text-sub: #64748b;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--accent-subtle: #eff6ff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-page);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
}
/* Header */
header {
background-color: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid var(--border-color);
padding: 12px 24px;
display: flex;
align-items: center;
justify-content: space-between;
backdrop-filter: blur(8px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-main);
letter-spacing: -0.02em;
}
.brand-badge {
background: var(--accent-subtle);
color: var(--accent);
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 999px;
font-weight: 600;
border: 1px solid #bfdbfe;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.status-pill {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
padding: 4px 12px;
border-radius: 999px;
background: var(--bg-surface);
border: 1px solid var(--border-color);
color: var(--text-sub);
cursor: pointer;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: all 0.2s;
}
.status-pill:hover { border-color: var(--accent); color: var(--text-main); }
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
.status-dot.error { background: #ef4444; }
.btn-icon {
background: var(--bg-surface);
border: 1px solid var(--border-color);
color: #334155;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;