| <!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"> |
| |
| <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> |
|
|
| |
| <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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| } |
| 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> |
|
|