Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>RP-AI</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/contrib/auto-render.min.js"></script> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0d0d0d; | |
| --surface: #1a1a1a; | |
| --border: #2a2a2a; | |
| --text: #e8e8e8; | |
| --muted: #888888; | |
| --accent: #f97316; | |
| --accent-dim: rgba(249,115,22,0.15); | |
| --input-bg: #1e1e1e; | |
| } | |
| body { | |
| font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| height: 100vh; | |
| height: 100dvh; | |
| overflow: hidden; | |
| display: flex; | |
| } | |
| /* ── Scrollbar ── */ | |
| ::-webkit-scrollbar { width: 5px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #555; } | |
| /* ── Sidebar ── */ | |
| #sidebar { | |
| width: 280px; | |
| min-width: 280px; | |
| background: var(--surface); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| height: 100dvh; | |
| transition: transform 0.25s ease; | |
| z-index: 40; | |
| } | |
| #sidebar.collapsed { transform: translateX(-100%); position: absolute; height: 100%; } | |
| .sidebar-header { | |
| padding: 16px 16px 12px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .sidebar-scroll { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 8px 0; | |
| } | |
| .size-group-label { | |
| padding: 8px 16px 4px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--muted); | |
| } | |
| .model-item { | |
| padding: 8px 16px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 13px; | |
| color: #ccc; | |
| transition: background 0.15s, color 0.15s; | |
| border-left: 3px solid transparent; | |
| } | |
| .model-item:hover { background: rgba(255,255,255,0.04); color: #fff; } | |
| .model-item.active { | |
| background: var(--accent-dim); | |
| color: var(--accent); | |
| border-left-color: var(--accent); | |
| font-weight: 600; | |
| } | |
| .model-item .family-badge { | |
| font-size: 9px; | |
| font-weight: 700; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.04em; | |
| flex-shrink: 0; | |
| } | |
| .family-lfm { background: rgba(59,130,246,0.15); color: #60a5fa; } | |
| .family-qwen { background: rgba(168,85,247,0.15); color: #c084fc; } | |
| .family-gemma { background: rgba(34,197,94,0.15); color: #4ade80; } | |
| .family-granite { background: rgba(249,115,22,0.15); color: #fb923c; } | |
| /* ── Main area ── */ | |
| #main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| min-width: 0; | |
| position: relative; | |
| } | |
| /* ── Top bar ── */ | |
| #topbar { | |
| height: 52px; | |
| min-height: 52px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| padding: 0 16px; | |
| gap: 12px; | |
| background: var(--bg); | |
| } | |
| .topbar-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 14px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--muted); | |
| letter-spacing: 0.04em; | |
| user-select: none; | |
| } | |
| .topbar-btn:hover { border-color: #444; color: #ccc; } | |
| .topbar-btn.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); } | |
| .topbar-btn.active svg { color: var(--accent); } | |
| #model-pill { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 5px 12px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| font-size: 12px; | |
| color: #ccc; | |
| max-width: 260px; | |
| overflow: hidden; | |
| } | |
| #model-pill .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; } | |
| #model-pill .size { color: var(--accent); font-weight: 700; flex-shrink: 0; } | |
| #status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 5px 10px; | |
| border-radius: 20px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.04em; | |
| } | |
| .status-idle { background: rgba(255,255,255,0.06); color: var(--muted); } | |
| .status-loading { background: rgba(250,204,21,0.12); color: #facc15; } | |
| .status-ready { background: rgba(34,197,94,0.12); color: #4ade80; } | |
| .status-switching { background: rgba(249,115,22,0.12); color: #fb923c; } | |
| .spin { display: inline-block; width: 12px; height: 12px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: spin 0.6s linear infinite; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* ── Chat area ── */ | |
| #chat-scroll { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0 16px 180px 16px; | |
| scroll-behavior: smooth; | |
| } | |
| #chat-container { max-width: 780px; margin: 0 auto; padding-top: 24px; } | |
| /* ── Welcome screen ── */ | |
| .welcome { text-align: center; padding-top: 12vh; } | |
| .welcome h1 { font-size: 28px; font-weight: 800; color: #fff; margin-bottom: 6px; } | |
| .welcome p { color: var(--muted); font-size: 14px; margin-bottom: 28px; } | |
| .suggestions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; max-width: 560px; margin: 0 auto; } | |
| .suggestion-chip { | |
| padding: 8px 16px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| font-size: 13px; | |
| color: #aaa; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .suggestion-chip:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); } | |
| /* ── Messages ── */ | |
| .msg-row { display: flex; gap: 12px; margin-bottom: 20px; } | |
| .msg-row.user { flex-direction: row-reverse; } | |
| .msg-avatar { | |
| width: 30px; height: 30px; border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 13px; font-weight: 700; flex-shrink: 0; margin-top: 2px; | |
| } | |
| .msg-avatar.bot { background: var(--accent); color: #000; } | |
| .msg-avatar.user { background: #333; color: #fff; } | |
| .msg-body { max-width: 85%; min-width: 60px; } | |
| .msg-content { | |
| padding: 12px 16px; | |
| border-radius: 16px; | |
| font-size: 14.5px; | |
| line-height: 1.7; | |
| word-break: break-word; | |
| } | |
| .msg-row.assistant .msg-content { background: var(--surface); border: 1px solid var(--border); color: var(--text); } | |
| .msg-row.user .msg-content { background: var(--accent); color: #000; font-weight: 500; } | |
| /* ── Thinking block ── */ | |
| .thinking-block { | |
| background: rgba(249,115,22,0.06); | |
| border: 1px solid rgba(249,115,22,0.15); | |
| border-radius: 10px; | |
| padding: 10px 14px; | |
| margin-bottom: 10px; | |
| font-size: 12.5px; | |
| color: #999; | |
| line-height: 1.6; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .thinking-label { | |
| font-size: 10px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| color: var(--accent); | |
| margin-bottom: 4px; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| /* ── Sources card ── */ | |
| .sources-card { | |
| background: rgba(59,130,246,0.06); | |
| border: 1px solid rgba(59,130,246,0.15); | |
| border-radius: 10px; | |
| padding: 10px 14px; | |
| margin-bottom: 10px; | |
| font-size: 12px; | |
| } | |
| .sources-head { display: flex; align-items: center; gap: 6px; color: #60a5fa; font-weight: 600; margin-bottom: 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; } | |
| .source-item { display: flex; gap: 8px; padding: 3px 0; color: #aaa; } | |
| .source-item .idx { color: #60a5fa; font-weight: 700; flex-shrink: 0; } | |
| .source-item a { color: #93c5fd; text-decoration: none; } | |
| .source-item a:hover { text-decoration: underline; } | |
| .source-item .domain { color: #666; font-size: 11px; } | |
| .searching-indicator { display: flex; align-items: center; gap: 8px; color: #60a5fa; font-size: 12px; padding: 8px 0; } | |
| /* ── Typing dots ── */ | |
| .typing-dots { display: flex; gap: 4px; padding: 8px 4px; } | |
| .typing-dots span { width: 6px; height: 6px; background: var(--muted); border-radius: 50%; animation: bounce-dot 1.2s ease-in-out infinite; } | |
| .typing-dots span:nth-child(2) { animation-delay: 0.15s; } | |
| .typing-dots span:nth-child(3) { animation-delay: 0.3s; } | |
| @keyframes bounce-dot { 0%,80%,100% { opacity:0.3; transform:scale(0.8); } 40% { opacity:1; transform:scale(1); } } | |
| /* ── Input bar ── */ | |
| #input-area { | |
| position: absolute; | |
| bottom: 0; left: 0; right: 0; | |
| padding: 12px 16px 20px; | |
| background: linear-gradient(transparent, var(--bg) 30%); | |
| pointer-events: none; | |
| } | |
| #input-wrap { | |
| max-width: 780px; | |
| margin: 0 auto; | |
| pointer-events: auto; | |
| background: var(--input-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| padding: 8px 8px 8px 20px; | |
| display: flex; | |
| align-items: flex-end; | |
| transition: border-color 0.2s; | |
| } | |
| #input-wrap:focus-within { border-color: var(--accent); } | |
| #user-input { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| color: var(--text); | |
| font-size: 14.5px; | |
| font-family: inherit; | |
| resize: none; | |
| max-height: 150px; | |
| line-height: 1.5; | |
| padding: 6px 0; | |
| } | |
| #user-input::placeholder { color: #555; } | |
| #send-btn { | |
| width: 38px; height: 38px; | |
| border-radius: 50%; | |
| border: none; | |
| background: var(--accent); | |
| color: #000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| flex-shrink: 0; | |
| margin-bottom: 2px; | |
| transition: opacity 0.15s, transform 0.15s; | |
| } | |
| #send-btn:hover { opacity: 0.85; transform: scale(1.05); } | |
| #send-btn:disabled { opacity: 0.3; cursor: default; transform: none; } | |
| /* ── Settings panel (slide-over) ── */ | |
| #settings-overlay { | |
| position: fixed; inset: 0; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 50; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.2s; | |
| } | |
| #settings-overlay.open { opacity: 1; pointer-events: auto; } | |
| #settings-panel { | |
| position: fixed; top: 0; right: 0; bottom: 0; | |
| width: 320px; max-width: 90vw; | |
| background: var(--surface); | |
| border-left: 1px solid var(--border); | |
| z-index: 51; | |
| padding: 24px; | |
| transform: translateX(100%); | |
| transition: transform 0.25s ease; | |
| overflow-y: auto; | |
| } | |
| #settings-panel.open { transform: translateX(0); } | |
| .setting-group { margin-bottom: 20px; } | |
| .setting-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 8px; } | |
| .setting-textarea { | |
| width: 100%; | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| color: var(--text); | |
| font-family: inherit; | |
| font-size: 13px; | |
| padding: 10px 12px; | |
| resize: vertical; | |
| min-height: 80px; | |
| outline: none; | |
| } | |
| .setting-textarea:focus { border-color: var(--accent); } | |
| input[type="range"] { | |
| -webkit-appearance: none; appearance: none; | |
| width: 100%; height: 4px; border-radius: 2px; | |
| background: var(--border); outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; appearance: none; | |
| width: 14px; height: 14px; border-radius: 50%; | |
| background: var(--accent); cursor: pointer; | |
| border: 2px solid var(--bg); | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 14px; height: 14px; border-radius: 50%; | |
| background: var(--accent); cursor: pointer; | |
| border: 2px solid var(--bg); | |
| } | |
| /* ── Markdown prose in messages ── */ | |
| .msg-content p { margin-bottom: 0.6em; } | |
| .msg-content strong { color: #fff; font-weight: 600; } | |
| .msg-content code { background: rgba(255,255,255,0.08); border-radius: 4px; padding: 0.15em 0.4em; font-size: 0.875em; color: var(--accent); } | |
| .msg-content pre { background: #111 ; border: 1px solid var(--border); border-radius: 10px; padding: 14px; overflow-x: auto; margin: 10px 0; } | |
| .msg-content pre code { background: none; padding: 0; color: #e8e8e8; font-size: 0.8125em; } | |
| .msg-content a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } | |
| .msg-content a:hover { color: #fb923c; } | |
| .msg-content ul, .msg-content ol { padding-left: 1.4em; margin: 0.4em 0; } | |
| .msg-content li { margin: 0.25em 0; } | |
| .msg-content blockquote { border-left: 3px solid var(--accent); padding-left: 1em; margin: 0.6em 0; color: var(--muted); } | |
| .msg-content h1,.msg-content h2,.msg-content h3,.msg-content h4 { color: #fff; font-weight: 700; margin-top: 1.1em; margin-bottom: 0.4em; } | |
| .msg-content h1 { font-size: 1.4em; } .msg-content h2 { font-size: 1.25em; } .msg-content h3 { font-size: 1.12em; } | |
| .msg-content hr { border-color: var(--border); margin: 1em 0; } | |
| .msg-content table { width: 100%; border-collapse: collapse; margin: 0.6em 0; } | |
| .msg-content th, .msg-content td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; } | |
| .msg-content th { background: rgba(255,255,255,0.05); font-weight: 600; color: #fff; } | |
| /* ── Mobile ── */ | |
| #menu-toggle { display: none; } | |
| @media (max-width: 768px) { | |
| #sidebar { position: fixed; left: 0; top: 0; height: 100%; height: 100dvh; } | |
| #sidebar.collapsed { transform: translateX(-100%); } | |
| #menu-toggle { display: flex; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Sidebar --> | |
| <aside id="sidebar" class="collapsed"> | |
| <div class="sidebar-header"> | |
| <span style="font-weight:800; font-size:16px; color:#fff;">⚡ RP-AI</span> | |
| <button onclick="toggleSidebar()" style="background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;"> | |
| <i data-lucide="panel-left-close" style="width:18px;height:18px;"></i> | |
| </button> | |
| </div> | |
| <div class="sidebar-scroll" id="model-list"></div> | |
| <div style="padding:12px 16px; border-top:1px solid var(--border);"> | |
| <button onclick="clearHistory()" style="width:100%;padding:10px;border-radius:10px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2);color:#f87171;font-size:13px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px;"> | |
| <i data-lucide="trash-2" style="width:14px;height:14px;"></i> Clear Chat | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Main --> | |
| <div id="main"> | |
| <!-- Top bar --> | |
| <div id="topbar"> | |
| <button id="menu-toggle" onclick="toggleSidebar()" style="background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;"> | |
| <i data-lucide="menu" style="width:20px;height:20px;"></i> | |
| </button> | |
| <div id="model-pill" title="Click sidebar to switch model"> | |
| <span class="name" id="pill-name">LFM2.5 1.2B Claude 4.6 Opus</span> | |
| <span class="size" id="pill-size">1.2B</span> | |
| </div> | |
| <div style="flex:1;"></div> | |
| <div id="status-badge" class="status-idle" title="Model status"> | |
| <span id="status-dot"></span> | |
| <span id="status-text">IDLE</span> | |
| </div> | |
| <button class="topbar-btn" id="think-btn" onclick="toggleThinking()"> | |
| <i data-lucide="brain" style="width:14px;height:14px;"></i> | |
| <span id="think-label">THINK</span> | |
| </button> | |
| <button class="topbar-btn" id="web-btn" onclick="toggleWeb()"> | |
| <i data-lucide="globe" style="width:14px;height:14px;"></i> | |
| <span id="web-label">SEARCH</span> | |
| </button> | |
| <button class="topbar-btn" onclick="openSettings()"> | |
| <i data-lucide="settings-2" style="width:14px;height:14px;"></i> | |
| </button> | |
| </div> | |
| <!-- Chat --> | |
| <div id="chat-scroll"> | |
| <div id="chat-container"> | |
| <div class="welcome" id="welcome-screen"> | |
| <h1>What can I help with?</h1> | |
| <p id="welcome-sub">RP-AI — 28 small models from DavidAU</p> | |
| <div class="suggestions"> | |
| <div class="suggestion-chip" onclick="useSuggestion(this)">Explain quantum computing simply</div> | |
| <div class="suggestion-chip" onclick="useSuggestion(this)">Write a Python web scraper</div> | |
| <div class="suggestion-chip" onclick="useSuggestion(this)">Compare React vs Vue vs Svelte</div> | |
| <div class="suggestion-chip" onclick="useSuggestion(this)">Create a sci-fi short story</div> | |
| <div class="suggestion-chip" onclick="useSuggestion(this)">Explain transformer architecture</div> | |
| <div class="suggestion-chip" onclick="useSuggestion(this)">Help me debug my code</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input --> | |
| <div id="input-area"> | |
| <div id="input-wrap"> | |
| <textarea id="user-input" placeholder="Message RP-AI..." rows="1"></textarea> | |
| <button id="send-btn" onclick="sendMessage()"> | |
| <i data-lucide="arrow-up" style="width:18px;height:18px;"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings overlay --> | |
| <div id="settings-overlay" onclick="closeSettings()"></div> | |
| <div id="settings-panel"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;"> | |
| <span style="font-weight:700;font-size:16px;color:#fff;">Settings</span> | |
| <button onclick="closeSettings()" style="background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;"> | |
| <i data-lucide="x" style="width:18px;height:18px;"></i> | |
| </button> | |
| </div> | |
| <div class="setting-group"> | |
| <div class="setting-label">System Prompt</div> | |
| <textarea id="system-prompt" class="setting-textarea" placeholder="Custom system prompt..."></textarea> | |
| </div> | |
| <div class="setting-group"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;"> | |
| <span class="setting-label" style="margin-bottom:0;">Temperature</span> | |
| <span style="font-size:13px;font-weight:700;color:var(--accent);" id="temp-val">0.9</span> | |
| </div> | |
| <input type="range" id="temp-slider" min="0" max="1" step="0.05" value="0.9" oninput="document.getElementById('temp-val').textContent=this.value"> | |
| </div> | |
| <div class="setting-group"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;"> | |
| <span class="setting-label" style="margin-bottom:0;">Top-p</span> | |
| <span style="font-size:13px;font-weight:700;color:var(--accent);" id="p-val">0.95</span> | |
| </div> | |
| <input type="range" id="p-slider" min="0" max="1" step="0.01" value="0.95" oninput="document.getElementById('p-val').textContent=this.value"> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| lucide.createIcons(); | |
| // ── Model definitions ── | |
| const MODELS = [ | |
| // 1.2B | |
| { id: "DavidAU/LFM2.5-1.2B-Thinking-Claude-4.6-Opus-Heretic-Uncensored-DISTILL", name: "LFM2.5 Claude 4.6 Opus", size: "1.2B", family: "LFM" }, | |
| { id: "DavidAU/LFM2.5-1.2B-Thinking-Gemini-Pro-1000-Heretic-Uncensored-DISTILL", name: "LFM2.5 Gemini Pro 1000", size: "1.2B", family: "LFM" }, | |
| { id: "DavidAU/LFM2.5-1.2B-Thinking-SuperMinds-7x-Heretic-Uncensored-DISTILL", name: "LFM2.5 SuperMinds 7x", size: "1.2B", family: "LFM" }, | |
| // 2B | |
| { id: "DavidAU/Qwen3.5-2B-Claude-4.6-OS-Auto-Variable-HERETIC-UNCENSORED-THINKING", name: "Qwen 3.5 Claude 4.6 OS", size: "2B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-2B-Polaris-HighIQ-Thinking-Compact", name: "Qwen 3.5 Polaris HighIQ", size: "2B", family: "Qwen" }, | |
| { id: "DavidAU/gemma-4-E2B-it-The-DECKARD-Expresso-ONE-Universe-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 DECKARD Expresso", size: "2B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E2B-it-The-DECKARD-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 DECKARD", size: "2B", family: "Gemma" }, | |
| // 4B | |
| { id: "DavidAU/Qwen3.5-4B-Claude-4.6-HighIQ-THINKING", name: "Qwen 3.5 Claude 4.6 HighIQ", size: "4B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-4B-Claude-4.6-OS-Auto-Variable-HERETIC-UNCENSORED-THINKING", name: "Qwen 3.5 Claude 4.6 OS", size: "4B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-4B-Deckard-HERETIC-UNCENSORED-Thinking", name: "Qwen 3.5 DECKARD", size: "4B", family: "Qwen" }, | |
| { id: "DavidAU/gemma-3-4b-it-vl-Heretic-4-Horsemen-Uncensored", name: "Gemma 3 Heretic 4 Horsemen", size: "4B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E4B-it-Claude-Opus-4.5-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 Claude Opus 4.5", size: "4B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E4B-it-GLM-4.7-Flash-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 GLM 4.7 Flash", size: "4B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E4B-it-The-DECKARD-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 DECKARD", size: "4B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E4B-it-The-DECKARD-V2-Strong-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 DECKARD V2", size: "4B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E4B-it-The-DECKARD-V3-Expresso-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 DECKARD V3", size: "4B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E4B-it-The-DECKARD-Expresso-Universe-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 DECKARD Expresso", size: "4B", family: "Gemma" }, | |
| { id: "DavidAU/gemma-4-E4B-it-The-DECKARD-Claude-Opus-Expresso-Universe-HERETIC-UNCENSORED-Thinking", name: "Gemma 4 DECKARD Claude Opus", size: "4B", family: "Gemma" }, | |
| // 8B | |
| { id: "DavidAU/granite-4.1-8b-Claude-Opus-4.6-Thinking-MAX", name: "Granite 4.1 Claude Opus 4.6 MAX", size: "8B", family: "Granite" }, | |
| { id: "DavidAU/granite-4.1-8b-Brainstone2-Thinking", name: "Granite 4.1 Brainstone2", size: "8B", family: "Granite" }, | |
| { id: "DavidAU/granite-4.1-8b-FlintStones-V1", name: "Granite 4.1 FlintStones V1", size: "8B", family: "Granite" }, | |
| { id: "DavidAU/LFM2-8B-A1B-SpeedDemon-GLM-4.7-Flash-Thinking-Instruct-Hybrid", name: "LFM2 8B SpeedDemon GLM", size: "8B", family: "LFM" }, | |
| // 9B | |
| { id: "DavidAU/Qwen3.5-9B-Claude-4.6-HighIQ-THINKING", name: "Qwen 3.5 Claude 4.6 HighIQ", size: "9B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-9B-Claude-4.6-OS-Auto-Variable-HERETIC-UNCENSORED-THINKING", name: "Qwen 3.5 Claude 4.6 OS", size: "9B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-9B-Deckard-HERETIC-UNCENSORED-Thinking", name: "Qwen 3.5 DECKARD", size: "9B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-9B-Claude-4.6-HighIQ-INSTRUCT", name: "Qwen 3.5 Claude 4.6 Instruct", size: "9B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.6-9B-Heretic-Uncensored-Thinking-Sweet-Madness", name: "Qwen 3.6 Sweet Madness", size: "9B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-9B-Star-Trek-TNG-DS9-Heretic-Uncensored-Thinking", name: "Qwen 3.5 Star Trek TNG/DS9", size: "9B", family: "Qwen" }, | |
| { id: "DavidAU/Qwen3.5-9B-GBO-Fire-HERETIC-UNCENSORED-THINKING-X8", name: "Qwen 3.5 GBO Fire X8", size: "9B", family: "Qwen" }, | |
| ]; | |
| // ── State ── | |
| let client = null; | |
| let chatHistory = []; | |
| let currentJob = null; | |
| let selectedModel = MODELS[0]; | |
| let thinkingEnabled = true; | |
| let webEnabled = false; | |
| const THINK_CLOSE = '</think>'; | |
| // ── DOM refs ── | |
| const chatContainer = document.getElementById('chat-container'); | |
| const chatScroll = document.getElementById('chat-scroll'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const welcomeScreen = document.getElementById('welcome-screen'); | |
| // ── Build model list in sidebar ── | |
| function buildModelList() { | |
| const list = document.getElementById('model-list'); | |
| let currentSize = ''; | |
| let html = ''; | |
| MODELS.forEach((m, i) => { | |
| if (m.size !== currentSize) { | |
| currentSize = m.size; | |
| html += `<div class="size-group-label">${m.size} Parameters</div>`; | |
| } | |
| const familyClass = 'family-' + m.family.toLowerCase(); | |
| const active = i === 0 ? ' active' : ''; | |
| html += `<div class="model-item${active}" data-index="${i}" onclick="window._selectModel(${i})"> | |
| <span class="family-badge ${familyClass}">${m.family}</span> | |
| <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${m.name}</span> | |
| </div>`; | |
| }); | |
| list.innerHTML = html; | |
| } | |
| buildModelList(); | |
| window._selectModel = function(idx) { | |
| const m = MODELS[idx]; | |
| if (m.id === selectedModel.id) return; | |
| selectedModel = m; | |
| document.getElementById('pill-name').textContent = m.name; | |
| document.getElementById('pill-size').textContent = m.size; | |
| document.querySelectorAll('.model-item').forEach(el => el.classList.remove('active')); | |
| document.querySelector(`.model-item[data-index="${idx}"]`).classList.add('active'); | |
| // Tell backend to switch model | |
| setStatus('switching', 'SWITCHING'); | |
| if (client) { | |
| client.predict('/switch_model', { model_id: m.id }).catch(e => console.error('Model switch error', e)); | |
| } | |
| // Clear chat on model switch | |
| clearHistory(true); | |
| // On mobile, close sidebar | |
| if (window.innerWidth <= 768) toggleSidebar(); | |
| }; | |
| // ── Sidebar toggle ── | |
| window.toggleSidebar = function() { | |
| document.getElementById('sidebar').classList.toggle('collapsed'); | |
| }; | |
| // ── Status badge ── | |
| function setStatus(state, text) { | |
| const badge = document.getElementById('status-badge'); | |
| const dot = document.getElementById('status-dot'); | |
| const label = document.getElementById('status-text'); | |
| badge.className = 'status-' + state; | |
| label.textContent = text; | |
| if (state === 'loading' || state === 'switching') { | |
| dot.innerHTML = '<span class="spin"></span>'; | |
| } else { | |
| dot.innerHTML = ''; | |
| } | |
| } | |
| // ── Toggles ── | |
| window.toggleThinking = function() { | |
| thinkingEnabled = !thinkingEnabled; | |
| document.getElementById('think-btn').classList.toggle('active', thinkingEnabled); | |
| }; | |
| window.toggleWeb = function() { | |
| webEnabled = !webEnabled; | |
| document.getElementById('web-btn').classList.toggle('active', webEnabled); | |
| }; | |
| window.openSettings = function() { | |
| document.getElementById('settings-overlay').classList.add('open'); | |
| document.getElementById('settings-panel').classList.add('open'); | |
| }; | |
| window.closeSettings = function() { | |
| document.getElementById('settings-overlay').classList.remove('open'); | |
| document.getElementById('settings-panel').classList.remove('open'); | |
| }; | |
| // Init toggles | |
| document.getElementById('think-btn').classList.add('active'); | |
| // ── Utility ── | |
| function escapeHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } | |
| function renderMath(el) { | |
| if (window.renderMathInElement) { | |
| renderMathInElement(el, { | |
| delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], | |
| throwOnError: false | |
| }); | |
| } | |
| } | |
| function domainFromUrl(u) { try { return new URL(u).hostname.replace(/^www\./,''); } catch(_) { return u; } } | |
| function buildWebContext(query, results) { | |
| if (!results || results.length === 0) return ''; | |
| const lines = [`[Web search results for: ${query}]`]; | |
| results.forEach((r, i) => { | |
| lines.push(`[${i+1}] ${r.title}`); | |
| lines.push(`URL: ${r.url}`); | |
| if (r.snippet) lines.push(`Snippet: ${r.snippet}`); | |
| lines.push(''); | |
| }); | |
| lines.push('Use the above web search results to inform your answer. When you rely on a result, cite it as [1], [2], etc. If the results do not answer the question, say so and answer from your own knowledge.'); | |
| return lines.join('\n'); | |
| } | |
| function splitThinking(fullText) { | |
| const text = fullText.replace(/<\|im_end\|>/g, '').replace(/<\|im_start\|>/g, ''); | |
| if (!thinkingEnabled) return { thinking: '', answer: text.trim() }; | |
| const pos = text.indexOf(THINK_CLOSE); | |
| if (pos === -1) return { thinking: text.trim(), answer: '' }; | |
| return { thinking: text.slice(0, pos).replace(/<think>/g, '').trim(), answer: text.slice(pos + THINK_CLOSE.length).trim() }; | |
| } | |
| function renderSourcesCard(query, results) { | |
| const items = results.map((r, i) => | |
| `<div class="source-item"><span class="idx">[${i+1}]</span><span><a href="${escapeHtml(r.url)}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a><span class="domain"> — ${escapeHtml(domainFromUrl(r.url))}</span></span></div>` | |
| ).join(''); | |
| return `<div class="sources-card"><div class="sources-head"><i data-lucide="globe" style="width:12px;height:12px;"></i><span>Searched: "${escapeHtml(query)}"</span></div>${items}</div>`; | |
| } | |
| // ── Messages ── | |
| function appendMessage(role, text = '') { | |
| if (welcomeScreen) welcomeScreen.style.display = 'none'; | |
| const div = document.createElement('div'); | |
| div.className = `msg-row ${role}`; | |
| const avatarClass = role === 'user' ? 'user' : 'bot'; | |
| const avatarText = role === 'user' ? 'U' : '⚡'; | |
| div.innerHTML = ` | |
| <div class="msg-avatar ${avatarClass}">${avatarText}</div> | |
| <div class="msg-body"> | |
| <div class="msg-content"> | |
| <div class="sources-container"></div> | |
| <div class="thinking-container"></div> | |
| <div class="text-container">${role === 'user' ? escapeHtml(text) : ''}</div> | |
| </div> | |
| </div>`; | |
| chatContainer.appendChild(div); | |
| chatScroll.scrollTo({ top: chatScroll.scrollHeight, behavior: 'smooth' }); | |
| return div; | |
| } | |
| function updateBotMessage(div, fullText) { | |
| const thinkContainer = div.querySelector('.thinking-container'); | |
| const textContainer = div.querySelector('.text-container'); | |
| const { thinking, answer } = splitThinking(fullText); | |
| if (thinking) { | |
| thinkContainer.innerHTML = `<div class="thinking-block"><div class="thinking-label"><i data-lucide="brain" style="width:11px;height:11px;"></i> Thinking</div>${marked.parse(thinking)}</div>`; | |
| lucide.createIcons({ nodes: [thinkContainer] }); | |
| } | |
| if (answer) { | |
| textContainer.innerHTML = marked.parse(answer); | |
| renderMath(textContainer); | |
| } else if (thinkingEnabled && thinking) { | |
| textContainer.innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div>'; | |
| } else { | |
| textContainer.innerHTML = ''; | |
| } | |
| chatScroll.scrollTo({ top: chatScroll.scrollHeight, behavior: 'smooth' }); | |
| return answer; | |
| } | |
| // ── Send ── | |
| window.sendMessage = async function() { | |
| const text = userInput.value.trim(); | |
| if (!text || !client) return; | |
| userInput.value = ''; | |
| userInput.style.height = 'auto'; | |
| appendMessage('user', text); | |
| sendBtn.disabled = true; | |
| const botDiv = appendMessage('assistant'); | |
| const sourcesContainer = botDiv.querySelector('.sources-container'); | |
| const textContainer = botDiv.querySelector('.text-container'); | |
| textContainer.innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div>'; | |
| let isStopped = false; | |
| sendBtn.onclick = () => { | |
| if (currentJob) { currentJob.cancel(); isStopped = true; resetSendBtn(); } | |
| }; | |
| // Web search phase | |
| let webContext = ''; | |
| if (webEnabled) { | |
| sourcesContainer.innerHTML = `<div class="searching-indicator"><span class="spin"></span><span>Searching for "${escapeHtml(text)}"...</span></div>`; | |
| try { | |
| const searchResp = await client.predict('/search', { query: text, num_results: 5 }); | |
| const results = (searchResp && searchResp.data && searchResp.data[0]) || []; | |
| if (results.length > 0) { | |
| sourcesContainer.innerHTML = renderSourcesCard(text, results); | |
| lucide.createIcons({ nodes: [sourcesContainer] }); | |
| webContext = buildWebContext(text, results); | |
| } else { | |
| sourcesContainer.innerHTML = '<div style="font-size:12px;color:#666;font-style:italic;margin-bottom:8px;">No web results found.</div>'; | |
| } | |
| } catch (err) { | |
| console.error('Search error', err); | |
| sourcesContainer.innerHTML = '<div style="font-size:12px;color:#f59e0b;font-style:italic;margin-bottom:8px;">Search failed.</div>'; | |
| } | |
| if (isStopped) { resetSendBtn(); return; } | |
| textContainer.innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div>'; | |
| } | |
| // Generation phase | |
| try { | |
| currentJob = client.submit('/predict', { | |
| message: text, | |
| history: chatHistory, | |
| thinking_mode: thinkingEnabled, | |
| temperature: parseFloat(document.getElementById('temp-slider').value), | |
| top_p: parseFloat(document.getElementById('p-slider').value), | |
| system_prompt: document.getElementById('system-prompt').value, | |
| web_context: webContext, | |
| }); | |
| let finalAnswer = ''; | |
| for await (const msg of currentJob) { | |
| if (isStopped) break; | |
| if (msg.type === 'data' && msg.data) { | |
| finalAnswer = updateBotMessage(botDiv, msg.data[0]); | |
| } else if (msg.type === 'status' && msg.stage === 'complete') { | |
| break; | |
| } else if (msg.type === 'status' && msg.stage === 'error') { | |
| throw new Error(msg.message || 'Generation failed'); | |
| } | |
| } | |
| if (!isStopped && finalAnswer) { | |
| chatHistory.push([text, finalAnswer]); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| if (!isStopped) { | |
| textContainer.innerHTML = '<p style="color:#f87171;">Error — please try again.</p>'; | |
| } | |
| } finally { | |
| resetSendBtn(); | |
| currentJob = null; | |
| } | |
| }; | |
| function resetSendBtn() { | |
| sendBtn.disabled = false; | |
| sendBtn.onclick = sendMessage; | |
| } | |
| window.clearHistory = function(silent) { | |
| chatHistory = []; | |
| chatContainer.innerHTML = ''; | |
| if (!silent) { | |
| welcomeScreen ? welcomeScreen.style.display = '' : null; | |
| if (welcomeScreen) chatContainer.appendChild(welcomeScreen); | |
| } | |
| closeSettings(); | |
| }; | |
| window.useSuggestion = function(el) { | |
| userInput.value = el.textContent; | |
| sendMessage(); | |
| }; | |
| // ── Input handling ── | |
| userInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } | |
| }); | |
| userInput.addEventListener('input', () => { | |
| userInput.style.height = 'auto'; | |
| userInput.style.height = userInput.scrollHeight + 'px'; | |
| }); | |
| // ── Init Gradio client ── | |
| async function init() { | |
| try { | |
| client = await Client.connect(window.location.origin, { events: ["data", "status"] }); | |
| pollStatus(); | |
| } catch (err) { | |
| console.error("Gradio connect error", err); | |
| setStatus('idle', 'OFFLINE'); | |
| } | |
| } | |
| let statusHandle = null; | |
| async function pollStatus() { | |
| if (!client) return; | |
| try { | |
| const resp = await client.predict('/status', {}); | |
| const s = (resp && resp.data && resp.data[0]) || {}; | |
| if (s.model_loaded) { | |
| setStatus('ready', (s.device || 'CPU').toUpperCase() + ' READY'); | |
| } else if (s.load_in_progress) { | |
| setStatus('loading', 'LOADING'); | |
| statusHandle = setTimeout(pollStatus, 3000); | |
| } else { | |
| setStatus('idle', 'IDLE'); | |
| } | |
| } catch (err) { | |
| statusHandle = setTimeout(pollStatus, 5000); | |
| } | |
| } | |
| init(); | |
| </script> | |
| </body> | |
| </html> |