RP-AI / index.html
R-Kentaren's picture
Upload index.html with huggingface_hub
fc942bc verified
Raw
History Blame Contribute Delete
42 kB
<!DOCTYPE html>
<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 !important; 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;">&#9889; 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 &mdash; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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' : '&#9889;';
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>