wesjos's picture
update: wesjos/Spark-x2.6-4B-HTML
203de2a verified
Raw History Blame Contribute Delete
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 &amp; 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 &amp; 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 &amp; Engine</div> <div class="modal-nav-tab" id="nav-tab-theme" onclick="switchModalNav('theme')">🎨 Styling &amp; 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 &amp; 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 &amp; 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 &amp; 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); } </script> </body> </html>
<!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;