qwen2
ExocoreV1 / templates /index.html
Helenbaclayon Bacalso
Add airllm integration and macOS-style redesign
2d2475e
Raw
History Blame Contribute Delete
32 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ExocoreV1</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', sans-serif; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); height: 100vh; display: flex; flex-direction: column; overflow: hidden; color: #e0e0e0; }
.mac-menu { background: rgba(30,30,50,0.85); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-bottom: 1px solid rgba(255,255,255,0.08); padding: 6px 16px; display: flex; align-items: center; gap: 18px; flex-shrink: 0; user-select: none; }
.mac-apple { font-size: 1.05rem; color: #c4b5fd; font-weight: 600; }
.mac-menu-item { font-size: 0.78rem; color: rgba(255,255,255,0.6); padding: 4px 10px; border-radius: 4px; cursor: default; }
.mac-menu-item:hover { background: rgba(255,255,255,0.08); color: #fff; }
.mac-menu-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.mac-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
.mac-dot.green { background: #30d158; }
.mac-dot.yellow { background: #ffd60a; }
.mac-clock { font-size: 0.75rem; color: rgba(255,255,255,0.5); }
.desktop { flex: 1; display: flex; align-items: center; justify-content: center; padding: 20px; position: relative; }
.window { background: rgba(30,30,50,0.75); backdrop-filter: blur(30px); -webkit-backdrop-filter: blur(30px); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; width: 100%; max-width: 1100px; height: 100%; display: flex; flex-direction: column; box-shadow: 0 24px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05); overflow: hidden; }
.window-titlebar { display: flex; align-items: center; padding: 10px 16px; border-bottom: 1px solid rgba(255,255,255,0.06); flex-shrink: 0; }
.window-dots { display: flex; gap: 8px; margin-right: 16px; }
.window-dot { width: 12px; height: 12px; border-radius: 50%; }
.window-dot.r { background: #ff5f57; }
.window-dot.y { background: #febc2e; }
.window-dot.g { background: #28c840; }
.window-title { font-size: 0.8rem; color: rgba(255,255,255,0.5); font-weight: 500; flex: 1; text-align: center; }
.window-body { flex: 1; display: flex; overflow: hidden; }
.sidebar { width: 240px; border-right: 1px solid rgba(255,255,255,0.06); padding: 12px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; flex-shrink: 0; }
.sidebar-label { font-size: 0.65rem; color: rgba(255,255,255,0.3); text-transform: uppercase; letter-spacing: 1px; font-weight: 600; margin-bottom: 4px; }
.model-card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 10px 12px; cursor: pointer; transition: all .15s; margin-bottom: 4px; }
.model-card:hover { background: rgba(255,255,255,0.07); border-color: rgba(196,181,253,0.3); }
.model-card.selected { background: rgba(196,181,253,0.08); border-color: rgba(196,181,253,0.4); }
.model-card .mn { font-size: 0.8rem; color: rgba(255,255,255,0.85); font-weight: 500; }
.model-card .mt { font-size: 0.7rem; color: rgba(255,255,255,0.35); margin-top: 2px; }
.pb { font-size: 0.6rem; background: rgba(196,181,253,0.15); border: 1px solid rgba(196,181,253,0.25); color: #c4b5fd; padding: 1px 6px; border-radius: 10px; margin-left: 4px; }
.status-bar { display: flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 6px 10px; font-size: 0.75rem; color: rgba(255,255,255,0.4); }
.status-bar .sdot { width: 6px; height: 6px; border-radius: 50%; }
.status-bar .sdot.loading { background: #ffd60a; animation: pulse 1s infinite; }
.status-bar .sdot.ready { background: #30d158; }
.status-bar .sdot.off { background: rgba(255,255,255,0.15); }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:.3} }
.param-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; }
.param-row label { font-size: 0.7rem; color: rgba(255,255,255,0.4); }
.param-row input { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.8); border-radius: 5px; padding: 4px 8px; font-size: 0.75rem; width: 80px; }
.param-row input:focus { outline: none; border-color: rgba(196,181,253,0.5); }
.chat-area { flex: 1; display: flex; flex-direction: column; }
.messages { flex: 1; overflow-y: auto; padding: 20px 24px; display: flex; flex-direction: column; gap: 14px; }
.welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 8px; }
.welcome h2 { font-size: 1.5rem; background: linear-gradient(135deg, #c4b5fd, #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; }
.welcome p { color: rgba(255,255,255,0.3); font-size: 0.85rem; }
.msg { max-width: 85%; }
.msg.user { align-self: flex-end; }
.msg.assistant { align-self: flex-start; }
.msg-bubble { padding: 12px 16px; border-radius: 12px; font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; }
.msg.user .msg-bubble { background: rgba(196,181,253,0.15); border: 1px solid rgba(196,181,253,0.2); color: #e2e8f0; border-bottom-right-radius: 4px; }
.msg.assistant .msg-bubble { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); color: rgba(255,255,255,0.85); border-bottom-left-radius: 4px; }
.think-block { background: rgba(10,20,40,0.5); border: 1px solid rgba(59,130,246,0.2); border-radius: 10px; padding: 10px 14px; margin-bottom: 6px; }
.think-header { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; margin-bottom: 4px; }
.think-header .th-label { font-size: 0.7rem; color: rgba(255,255,255,0.3); text-transform: uppercase; letter-spacing: 0.5px; }
.think-badge { font-size: 0.6rem; background: rgba(59,130,246,0.1); border: 1px solid rgba(59,130,246,0.2); color: #60a5fa; padding: 1px 6px; border-radius: 10px; }
.think-toggle { margin-left: auto; font-size: 0.65rem; color: rgba(255,255,255,0.3); }
.think-content { font-size: 0.78rem; color: rgba(255,255,255,0.45); white-space: pre-wrap; line-height: 1.5; overflow: hidden; transition: max-height 0.3s ease; }
.think-content.collapsed { max-height: 0 !important; }
.bottom-bar { border-top: 1px solid rgba(255,255,255,0.06); padding: 12px 16px; flex-shrink: 0; }
.mode-row { display: flex; align-items: center; gap: 5px; margin-bottom: 8px; flex-wrap: wrap; }
.mode-btn { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.4); padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 0.75rem; transition: all .15s; }
.mode-btn.active { background: rgba(196,181,253,0.1); border-color: rgba(196,181,253,0.35); color: #c4b5fd; }
.mode-btn.deep.active { background: rgba(59,130,246,0.1); border-color: rgba(59,130,246,0.35); color: #60a5fa; }
.mode-btn:hover:not(.active) { background: rgba(255,255,255,0.07); color: rgba(255,255,255,0.6); }
.tool-btn { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.4); padding: 5px 12px; border-radius: 6px; cursor: pointer; font-size: 0.75rem; margin-left: auto; display: flex; align-items: center; gap: 4px; }
.tool-btn:hover { background: rgba(255,255,255,0.07); color: rgba(255,255,255,0.6); }
.tool-btn.active { background: rgba(59,130,246,0.1); border-color: rgba(59,130,246,0.3); color: #60a5fa; }
.input-row { display: flex; gap: 8px; align-items: flex-end; }
.msg-input { flex: 1; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.85); border-radius: 8px; padding: 10px 14px; font-size: 0.85rem; resize: none; max-height: 120px; font-family: inherit; line-height: 1.4; }
.msg-input:focus { outline: none; border-color: rgba(196,181,253,0.4); }
.msg-input::placeholder { color: rgba(255,255,255,0.2); }
.send-btn { background: linear-gradient(135deg, #7c3aed, #6d28d9); color: white; border: none; border-radius: 8px; padding: 10px 20px; cursor: pointer; font-size: 0.85rem; font-weight: 500; height: 42px; transition: opacity .15s; }
.send-btn:hover { opacity: 0.85; }
.send-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.ctx-row { padding: 4px 16px 0; display: none; }
.ctx-badge { display: inline-flex; align-items: center; gap: 4px; background: rgba(59,130,246,0.1); border: 1px solid rgba(59,130,246,0.2); border-radius: 20px; padding: 3px 10px; font-size: 0.7rem; color: #60a5fa; }
.ctx-badge button { background: none; border: none; color: #60a5fa; cursor: pointer; font-size: 0.8rem; padding: 0 0 0 4px; line-height: 1; }
.search-panel { border-top: 1px solid rgba(255,255,255,0.06); padding: 8px 16px; display: none; align-items: center; gap: 8px; }
.search-panel.open { display: flex; }
.search-input { flex: 1; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.85); border-radius: 6px; padding: 7px 12px; font-size: 0.8rem; font-family: inherit; }
.search-input:focus { outline: none; border-color: rgba(59,130,246,0.4); }
.search-results { padding: 0 16px; display: none; max-height: 200px; overflow-y: auto; }
.search-results.open { display: block; padding: 8px 16px; }
.sr-item { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 8px 11px; margin-bottom: 6px; }
.sr-item .sr-title { font-size: 0.78rem; color: #93c5fd; font-weight: 500; }
.sr-item .sr-snip { font-size: 0.72rem; color: rgba(255,255,255,0.35); margin-top: 2px; line-height: 1.4; }
.sr-item .sr-url { font-size: 0.65rem; color: rgba(255,255,255,0.2); margin-top: 2px; }
.sr-item .sr-actions { display: flex; gap: 5px; margin-top: 6px; }
.sr-btn { font-size: 0.65rem; padding: 3px 8px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.4); cursor: pointer; }
.sr-btn:hover { border-color: rgba(59,130,246,0.3); color: #60a5fa; }
.sr-msg { font-size: 0.72rem; color: rgba(255,255,255,0.3); padding: 6px 0; }
.gemini-area { border-top: 1px solid rgba(255,255,255,0.06); padding: 8px 12px; }
.gemini-row { display: flex; align-items: center; justify-content: space-between; font-size: 0.72rem; color: rgba(255,255,255,0.35); }
.gemini-row button { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.4); padding: 3px 10px; font-size: 0.7rem; border-radius: 5px; cursor: pointer; }
.gemini-row button:hover { background: rgba(255,255,255,0.07); }
.gdot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.15); display: inline-block; margin-right: 4px; }
.gdot.on { background: #30d158; }
.dock { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); background: rgba(30,30,50,0.7); backdrop-filter: blur(40px); -webkit-backdrop-filter: blur(40px); border: 1px solid rgba(255,255,255,0.06); border-radius: 16px; padding: 6px 14px; display: none; gap: 10px; align-items: flex-end; }
.dock-item { display: flex; flex-direction: column; align-items: center; gap: 3px; cursor: pointer; padding: 4px 8px; border-radius: 8px; transition: all .15s; }
.dock-item:hover { background: rgba(255,255,255,0.06); transform: scale(1.05); }
.dock-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1.1rem; }
.dock-label { font-size: 0.55rem; color: rgba(255,255,255,0.4); }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; }
::-webkit-scrollbar-track { background: transparent; }
</style>
</head>
<body>
<div class="mac-menu">
<span class="mac-apple"></span>
<span class="mac-menu-item">ExocoreV1</span>
<span class="mac-menu-item">File</span>
<span class="mac-menu-item">Edit</span>
<span class="mac-menu-item">View</span>
<span class="mac-menu-item">Window</span>
<span class="mac-menu-item" onclick="document.getElementById('settingsDialog').classList.toggle('show')">Settings</span>
<div class="mac-menu-right">
<span class="mac-dot green"></span>
<span class="mac-dot yellow"></span>
<span class="mac-clock" id="clock"></span>
</div>
</div>
<div class="desktop">
<div class="window">
<div class="window-titlebar">
<div class="window-dots">
<div class="window-dot r" onclick="clearChat()" title="Close"></div>
<div class="window-dot y" onclick="document.getElementById('msgInput').focus()" title="Minimize"></div>
<div class="window-dot g" title="Maximize"></div>
</div>
<div class="window-title">ExocoreV1 — Chat</div>
</div>
<div class="window-body">
<!-- SIDEBAR -->
<div class="sidebar">
<div>
<div class="sidebar-label">Models</div>
<div id="modelList"><p style="color:rgba(255,255,255,0.2);font-size:0.75rem;">Loading…</p></div>
</div>
<div>
<div class="sidebar-label">Status</div>
<div class="status-bar"><span class="sdot loading" id="statusDot"></span><span id="statusText">Auto-loading…</span></div>
</div>
<div id="loadBtnWrap" style="display:none;">
<button class="mode-btn" id="loadBtn" onclick="loadSelectedModel()" style="width:100%;text-align:center;padding:6px;">Load Selected</button>
</div>
<div>
<div class="sidebar-label">Parameters</div>
<div class="param-row"><label>Max Tokens</label><input type="number" id="maxTokens" value="512" min="64" max="4096"></div>
<div class="param-row"><label>Temperature</label><input type="number" id="temperature" value="0.7" step="0.05" min="0.1" max="2"></div>
</div>
<div id="detailsSection" style="display:none;">
<div class="sidebar-label">Model Details</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;" id="detailsGrid"></div>
</div>
<div class="gemini-area">
<div class="gemini-row">
<span><span class="gdot" id="geminiDot"></span><span id="geminiStatus">Gemini</span></span>
<button onclick="initGemini()">Init</button>
</div>
</div>
</div>
<!-- CHAT -->
<div class="chat-area">
<div class="messages" id="messages">
<div class="welcome" id="welcome">
<h2>ExocoreV1</h2>
<p id="welcomeSub">Auto-loading model…</p>
</div>
</div>
<div class="ctx-row" id="ctxRow">
<span class="ctx-badge">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<span id="ctxLabel">Research context active</span>
<button onclick="clearContext()">×</button>
</span>
</div>
<div class="search-panel" id="searchPanel">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#60a5fa" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input type="text" class="search-input" id="searchInput" placeholder="Search the web…" onkeydown="if(event.key==='Enter')runSearch()">
<button onclick="runSearch()" style="background:rgba(59,130,246,0.15);border:1px solid rgba(59,130,246,0.25);color:#60a5fa;padding:6px 12px;border-radius:6px;font-size:0.75rem;cursor:pointer;">Search</button>
<button onclick="closeSearch()" style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:rgba(255,255,255,0.4);padding:6px 10px;border-radius:6px;font-size:0.75rem;cursor:pointer;"></button>
</div>
<div class="search-results" id="searchResults"></div>
<div class="bottom-bar">
<div class="mode-row">
<button class="mode-btn active" id="mFast" onclick="setMode('fast')">Fast</button>
<button class="mode-btn" id="mThink" onclick="setMode('thinking')">Thinking</button>
<button class="mode-btn deep" id="mDeep" onclick="setMode('deep')">Deep Think</button>
<button class="mode-btn" onclick="clearChat()">Clear</button>
<button class="tool-btn" id="researchBtn" onclick="toggleSearch()">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
Research
</button>
</div>
<div class="input-row">
<textarea class="msg-input" id="msgInput" rows="1"
placeholder="Message…"
onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/></svg></button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let selectedModel = null;
let currentMode = 'fast';
let searchContext = null;
let isGenerating = false;
let loadedModel = null;
function updateClock() {
const n = new Date();
document.getElementById('clock').textContent = n.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
}
setInterval(updateClock, 10000);
updateClock();
function escHtml(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
}
function setMode(m) {
currentMode = m;
document.getElementById('mFast').classList.toggle('active', m === 'fast');
document.getElementById('mThink').classList.toggle('active', m === 'thinking');
document.getElementById('mDeep').classList.toggle('active', m === 'deep');
}
function toggleSearch() {
const p = document.getElementById('searchPanel');
const b = document.getElementById('researchBtn');
const open = p.classList.contains('open');
if (open) { closeSearch(); }
else { p.classList.add('open'); b.classList.add('active'); document.getElementById('searchInput').focus(); }
}
function closeSearch() {
document.getElementById('searchPanel').classList.remove('open');
document.getElementById('searchResults').classList.remove('open');
document.getElementById('researchBtn').classList.remove('active');
}
function clearContext() {
searchContext = null;
document.getElementById('ctxRow').style.display = 'none';
}
function setContext(text) {
if (searchContext) searchContext += '\n\n' + text;
else searchContext = text;
document.getElementById('ctxRow').style.display = 'block';
document.getElementById('ctxLabel').textContent = 'Research context (' + searchContext.length + ' chars)';
}
async function runSearch() {
const q = document.getElementById('searchInput').value.trim();
if (!q) return;
const sr = document.getElementById('searchResults');
sr.innerHTML = '<div class="sr-msg">Searching…</div>';
sr.classList.add('open');
try {
const r = await fetch('/api/search', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ query: q })
});
const d = await r.json();
if (d.error) { sr.innerHTML = `<div class="sr-msg" style="color:#ef4444;">Error: ${escHtml(d.error)}</div>`; return; }
if (!d.results || !d.results.length) { sr.innerHTML = '<div class="sr-msg">No results found.</div>'; return; }
sr.innerHTML = '';
d.results.forEach(res => {
const el = document.createElement('div');
el.className = 'sr-item';
el.innerHTML = `
<div class="sr-title">${escHtml(res.title)}</div>
<div class="sr-snip">${escHtml(res.snippet)}</div>
<div class="sr-url">${escHtml(res.url)}</div>
<div class="sr-actions">
<button class="sr-btn" onclick="addResultToCtx(${JSON.stringify(JSON.stringify(res))})">+ Context</button>
${res.url ? `<button class="sr-btn" onclick="readLink(${JSON.stringify(res.url)})">Read</button>` : ''}
${res.url ? `<button class="sr-btn" onclick="readMultiLinks(${JSON.stringify([res.url])})">Read+Chat</button>` : ''}
</div>`;
sr.appendChild(el);
});
} catch(e) { sr.innerHTML = `<div class="sr-msg" style="color:#ef4444;">Error: ${escHtml(e.message)}</div>`; }
}
function addResultToCtx(encoded) {
const res = JSON.parse(encoded);
setContext(`[${res.title}] ${res.snippet}${res.url}`);
}
async function readLink(url) {
const sr = document.getElementById('searchResults');
const msg = document.createElement('div');
msg.className = 'sr-msg';
msg.textContent = 'Reading ' + url + '…';
sr.insertBefore(msg, sr.firstChild);
try {
const r = await fetch('/api/read-link', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ url })
});
const d = await r.json();
msg.remove();
if (d.pages && d.pages[0] && !d.pages[0].error) {
const pg = d.pages[0];
setContext(`[${pg.title || url}]\n${pg.text}`);
} else {
alert('Read error: ' + (d.pages?.[0]?.error || 'Unknown'));
}
} catch(e) { msg.remove(); alert('Error: ' + e.message); }
}
async function readMultiLinks(urls) {
const sr = document.getElementById('searchResults');
const msg = document.createElement('div');
msg.className = 'sr-msg';
msg.textContent = 'Reading ' + urls.length + ' page(s)…';
sr.insertBefore(msg, sr.firstChild);
try {
const r = await fetch('/api/read-link', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ urls })
});
const d = await r.json();
msg.remove();
if (d.pages) {
d.pages.forEach(pg => {
if (!pg.error) setContext(`[${pg.title || pg.url}]\n${pg.text}`);
});
}
} catch(e) { msg.remove(); alert('Error: ' + e.message); }
}
async function loadModels() {
try {
const r = await fetch('/api/models');
const d = await r.json();
const list = document.getElementById('modelList');
list.innerHTML = '';
if (!d.models || !d.models.length) {
list.innerHTML = '<p style="color:rgba(255,255,255,0.2);font-size:0.75rem;">No models found.</p>';
return;
}
d.models.forEach(m => {
const name = m.name || m;
const type = m.type || 'pt';
const isProt = name.toLowerCase() === 'exocore.pt';
const div = document.createElement('div');
div.className = 'model-card' + (selectedModel === name ? ' selected' : '');
div.onclick = () => selectModel(name, type);
div.innerHTML = `<div class="mn">${escHtml(name)}${isProt ? '<span class="pb">Protected</span>' : ''}</div>
<div class="mt">${type === 'exocore' ? 'ExocoreV1' : type}</div>`;
list.appendChild(div);
});
} catch(e) {}
}
function selectModel(name) {
selectedModel = name;
document.querySelectorAll('.model-card').forEach(el => {
const nm = el.querySelector('.mn').childNodes[0].nodeValue.trim();
el.classList.toggle('selected', nm === name);
});
document.getElementById('loadBtnWrap').style.display = '';
}
async function loadSelectedModel() {
if (!selectedModel) return;
const btn = document.getElementById('loadBtn');
btn.disabled = true; btn.textContent = 'Loading…';
try {
const r = await fetch('/api/load', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ model: selectedModel, type: 'exocore' })
});
const d = await r.json();
if (d.error) alert(d.error);
else { loadedModel = selectedModel; updateStatusReady('ExocoreV1'); loadModelDetails(); }
} catch(e) { alert(e.message); }
btn.disabled = false; btn.textContent = 'Load Selected';
}
function updateStatusReady(name) {
document.getElementById('statusDot').className = 'sdot ready';
document.getElementById('statusText').textContent = name || 'Ready';
const w = document.getElementById('welcome');
if (w) { w.innerHTML = '<h2 style="background:linear-gradient(135deg,#c4b5fd,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">ExocoreV1</h2><p style="color:rgba(255,255,255,0.3);">Model loaded · Start chatting</p>'; }
}
async function loadModelDetails() {
try {
const r = await fetch('/api/model-details');
const d = await r.json();
if (d.error || d.loading) return;
document.getElementById('detailsSection').style.display = '';
const cells = [
{ v: (d.params_m||0)+'M', k:'Parameters' },
{ v: d.num_layers||d.n_layer||'—', k:'Layers' },
{ v: d.hidden_size||d.n_embd||'—', k:'Hidden' },
{ v: d.num_heads||'—', k:'Attn Heads' },
{ v: d.num_kv_heads||'—', k:'KV Heads' },
{ v: d.vocab_size ? Math.round(d.vocab_size/1000)+'K' : '—', k:'Vocab' },
];
document.getElementById('detailsGrid').innerHTML = cells.map(c =>
`<div style="background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.05);border-radius:5px;padding:4px 6px;">
<div style="font-size:0.78rem;color:#c4b5fd;font-weight:600;">${c.v}</div>
<div style="font-size:0.6rem;color:rgba(255,255,255,0.3);margin-top:1px;">${c.k}</div>
</div>`).join('');
} catch(e) {}
}
async function checkAutoLoad() {
try {
const r = await fetch('/api/autoload-status');
const d = await r.json();
if (d.loading) {
document.getElementById('statusDot').className = 'sdot loading';
document.getElementById('statusText').textContent = 'Loading ExocoreV1…';
document.getElementById('welcomeSub').textContent = 'ExocoreV1 is loading, please wait…';
setTimeout(checkAutoLoad, 2000);
} else if (d.done && d.model) {
loadedModel = d.model;
updateStatusReady('ExocoreV1');
loadModelDetails();
} else if (d.error) {
document.getElementById('statusDot').className = 'sdot off';
document.getElementById('statusText').textContent = 'Load failed';
document.getElementById('welcomeSub').textContent = 'Select and load a model from the sidebar.';
} else {
document.getElementById('statusDot').className = 'sdot off';
document.getElementById('statusText').textContent = 'No model loaded';
document.getElementById('welcomeSub').textContent = 'Select and load a model from the sidebar.';
}
} catch(e) { setTimeout(checkAutoLoad, 3000); }
}
function toggleThink(header) {
const content = header.nextElementSibling;
const collapsed = content.classList.toggle('collapsed');
header.querySelector('.think-toggle').textContent = collapsed ? '▶' : '▼';
}
function appendThinkBlock(parent, thinkText, mode) {
const label = mode === 'deep' ? 'Deep Think (Multi-Pass)' : 'Thinking';
const tb = document.createElement('div');
tb.className = 'think-block';
const maxH = Math.min(thinkText.length * 0.5, 300) + 'px';
tb.innerHTML = `<div class="think-header" onclick="toggleThink(this)">
<span class="th-label">${label}</span>
<span class="think-badge">⟳ Exocore</span>
<span class="think-toggle">▼</span>
</div>
<div class="think-content" style="max-height:${maxH};">
<div style="color:rgba(255,255,255,0.45);white-space:pre-wrap;">${escHtml(thinkText)}</div>
</div>`;
parent.appendChild(tb);
}
function addUserMessage(text) {
const w = document.getElementById('welcome');
if (w) w.remove();
const div = document.createElement('div');
div.className = 'msg user';
div.innerHTML = `<div class="msg-bubble">${escHtml(text)}</div>`;
document.getElementById('messages').appendChild(div);
div.scrollIntoView({ behavior:'smooth', block:'end' });
}
function addAssistantMessage() {
const div = document.createElement('div');
div.className = 'msg assistant';
const bubble = document.createElement('div');
bubble.className = 'msg-bubble';
div.appendChild(bubble);
document.getElementById('messages').appendChild(div);
div.scrollIntoView({ behavior:'smooth', block:'end' });
return { container: div, bubble };
}
async function sendMessage() {
if (isGenerating) return;
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text) return;
isGenerating = true;
document.getElementById('sendBtn').disabled = true;
input.value = ''; input.style.height = 'auto';
addUserMessage(text);
const { container, bubble } = addAssistantMessage();
bubble.textContent = '…';
let fullText = '';
let thinkShown = false;
try {
const resp = await fetch('/api/chat', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
message: text,
max_tokens: parseInt(document.getElementById('maxTokens').value),
temperature: parseFloat(document.getElementById('temperature').value),
mode: currentMode,
search_context: searchContext,
})
});
if (!resp.ok) {
const err = await resp.json().catch(()=>({}));
bubble.textContent = 'Error: ' + (err.error || resp.statusText);
return;
}
bubble.textContent = '';
const reader = resp.body.getReader();
const dec = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split('\n\n');
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const obj = JSON.parse(line.slice(6));
if (obj.error) { bubble.textContent = 'Error: ' + obj.error; break; }
if (obj.think_block && !thinkShown) {
thinkShown = true;
appendThinkBlock(container, obj.think_block, currentMode);
}
if (obj.token) {
fullText += obj.token;
bubble.textContent = fullText;
bubble.scrollIntoView({ behavior:'smooth', block:'end' });
}
} catch(e) {}
}
}
if (!fullText && !thinkShown) bubble.textContent = '(no response)';
if (searchContext) clearContext();
} catch(e) {
bubble.textContent = 'Error: ' + e.message;
} finally {
isGenerating = false;
document.getElementById('sendBtn').disabled = false;
}
}
async function clearChat() {
await fetch('/api/clear', { method:'POST' });
document.getElementById('messages').innerHTML =
`<div class="welcome" id="welcome"><h2 style="background:linear-gradient(135deg,#c4b5fd,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">ExocoreV1</h2><p style="color:rgba(255,255,255,0.3);">Chat cleared.</p></div>`;
clearContext();
}
async function initGemini() {
const r = await fetch('/api/gemini/init', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
const d = await r.json();
if (d.success) {
document.getElementById('geminiDot').className = 'gdot on';
document.getElementById('geminiStatus').textContent = 'Gemini Ready';
} else {
document.getElementById('geminiStatus').textContent = d.error || 'Failed';
}
}
async function checkGemini() {
try {
const r = await fetch('/api/gemini/status');
const d = await r.json();
if (d.initialized) {
document.getElementById('geminiDot').className = 'gdot on';
document.getElementById('geminiStatus').textContent = 'Gemini Ready';
}
} catch(e) {}
}
loadModels();
checkAutoLoad();
checkGemini();
</script>
</body>
</html>