inference-x/web/chat.html
Salka Elmadani ec36668cf5 Inference-X v1.0 — Universal AI Inference Engine
Better output from the same model. Fused computation, adaptive precision,
surgical expert loading. 305 KB, 19 backends, zero dependencies.

https://inference-x.com
2026-02-23 07:10:47 +00:00

368 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IX Web — Inference-X</title>
<style>
:root {
--bg: #0a0a0f; --bg2: #12121a; --bg3: #1a1a26;
--tx: #e8e8f0; --tx2: #8888a0; --tx3: #555568;
--ac: #6b8afd; --ac2: #4a6ae0; --ac3: #3a5ad0;
--gn: #4ade80; --yl: #fbbf24; --rd: #f87171;
--bd: #2a2a3a; --r: 8px; --mono: 'SF Mono', 'Fira Code', monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--tx); height: 100vh; display: flex; flex-direction: column; }
/* ─── Header ─── */
.header { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; background: var(--bg2); border-bottom: 1px solid var(--bd); flex-shrink: 0; }
.logo { display: flex; align-items: center; gap: 10px; }
.logo svg { width: 28px; height: 28px; }
.logo-text { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em; }
.logo-sub { font-size: .7rem; color: var(--tx2); font-family: var(--mono); }
.controls { display: flex; align-items: center; gap: 12px; }
.model-select { background: var(--bg3); border: 1px solid var(--bd); color: var(--tx); padding: 6px 10px; border-radius: var(--r); font-size: .8rem; font-family: var(--mono); cursor: pointer; max-width: 200px; }
.model-select:focus { outline: none; border-color: var(--ac); }
.hw-badge { display: flex; align-items: center; gap: 6px; font-size: .7rem; font-family: var(--mono); color: var(--tx2); background: var(--bg3); padding: 4px 10px; border-radius: 20px; }
.hw-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--gn); }
.hw-dot.offline { background: var(--rd); }
/* ─── Chat area ─── */
.chat { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.msg { max-width: 780px; width: 100%; margin: 0 auto; display: flex; gap: 12px; }
.msg.user { flex-direction: row-reverse; }
.msg-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: .8rem; font-weight: 600; flex-shrink: 0; }
.msg.user .msg-avatar { background: var(--ac); color: #fff; }
.msg.assistant .msg-avatar { background: var(--bg3); color: var(--ac); border: 1px solid var(--bd); }
.msg-body { background: var(--bg2); border: 1px solid var(--bd); border-radius: 12px; padding: 12px 16px; line-height: 1.6; font-size: .9rem; max-width: 85%; }
.msg.user .msg-body { background: var(--ac3); border-color: var(--ac2); color: #fff; }
.msg-body pre { background: var(--bg); border: 1px solid var(--bd); border-radius: 6px; padding: 10px; overflow-x: auto; font-family: var(--mono); font-size: .8rem; margin: 8px 0; }
.msg-body code { font-family: var(--mono); font-size: .85em; background: rgba(107,138,253,.15); padding: 2px 5px; border-radius: 3px; }
.msg-body pre code { background: none; padding: 0; }
.msg-body p { margin-bottom: 8px; }
.msg-body p:last-child { margin-bottom: 0; }
.msg-meta { font-size: .7rem; color: var(--tx3); font-family: var(--mono); margin-top: 6px; }
/* ─── Typing indicator ─── */
.typing { display: none; max-width: 780px; width: 100%; margin: 0 auto; }
.typing.active { display: flex; }
.typing-dots { display: flex; gap: 4px; padding: 12px 16px; background: var(--bg2); border: 1px solid var(--bd); border-radius: 12px; margin-left: 44px; }
.typing-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--tx3); animation: dot 1.4s infinite; }
.typing-dots span:nth-child(2) { animation-delay: .2s; }
.typing-dots span:nth-child(3) { animation-delay: .4s; }
@keyframes dot { 0%,60%,100% { opacity: .3; transform: scale(1); } 30% { opacity: 1; transform: scale(1.2); } }
/* ─── Welcome ─── */
.welcome { flex: 1; display: flex; align-items: center; justify-content: center; }
.welcome-inner { text-align: center; max-width: 500px; padding: 40px 20px; }
.welcome-title { font-size: 1.8rem; font-weight: 700; margin-bottom: 12px; background: linear-gradient(135deg, var(--ac), var(--gn)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.welcome-desc { color: var(--tx2); line-height: 1.6; margin-bottom: 24px; }
.welcome-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; text-align: left; }
.welcome-card { background: var(--bg2); border: 1px solid var(--bd); border-radius: var(--r); padding: 14px; cursor: pointer; transition: border-color .2s; }
.welcome-card:hover { border-color: var(--ac); }
.welcome-card-title { font-size: .8rem; font-weight: 600; margin-bottom: 4px; }
.welcome-card-desc { font-size: .75rem; color: var(--tx2); }
/* ─── Input ─── */
.input-area { padding: 16px 20px; background: var(--bg2); border-top: 1px solid var(--bd); flex-shrink: 0; }
.input-wrap { max-width: 780px; margin: 0 auto; display: flex; gap: 10px; align-items: flex-end; }
.input-box { flex: 1; position: relative; }
.input-box textarea { width: 100%; background: var(--bg3); border: 1px solid var(--bd); border-radius: 12px; color: var(--tx); padding: 12px 16px; font-size: .9rem; font-family: inherit; resize: none; min-height: 48px; max-height: 200px; line-height: 1.5; }
.input-box textarea:focus { outline: none; border-color: var(--ac); }
.input-box textarea::placeholder { color: var(--tx3); }
.send-btn { background: var(--ac); color: #fff; border: none; border-radius: 12px; width: 48px; height: 48px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background .2s; flex-shrink: 0; }
.send-btn:hover { background: var(--ac2); }
.send-btn:disabled { opacity: .4; cursor: not-allowed; }
.send-btn svg { width: 20px; height: 20px; }
.footer-text { text-align: center; font-size: .65rem; color: var(--tx3); margin-top: 8px; font-family: var(--mono); }
/* ─── Settings panel ─── */
.settings-btn { background: none; border: 1px solid var(--bd); color: var(--tx2); width: 32px; height: 32px; border-radius: var(--r); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.settings-btn:hover { border-color: var(--ac); color: var(--ac); }
.settings { display: none; position: fixed; top: 0; right: 0; width: 320px; height: 100%; background: var(--bg2); border-left: 1px solid var(--bd); padding: 20px; z-index: 100; overflow-y: auto; }
.settings.open { display: block; }
.settings h3 { font-size: .9rem; margin-bottom: 16px; }
.settings label { display: block; font-size: .75rem; color: var(--tx2); margin-bottom: 4px; font-family: var(--mono); }
.settings input[type=range] { width: 100%; margin-bottom: 16px; accent-color: var(--ac); }
.settings .val { float: right; color: var(--ac); }
.settings-close { background: none; border: none; color: var(--tx2); cursor: pointer; float: right; font-size: 1.2rem; }
/* ─── Responsive ─── */
@media (max-width: 600px) {
.hw-badge { display: none; }
.welcome-grid { grid-template-columns: 1fr; }
.model-select { max-width: 140px; }
.msg-body { max-width: 92%; }
}
</style>
</head>
<body>
<div class="header">
<div class="logo">
<svg viewBox="0 0 100 100" fill="none"><circle cx="50" cy="50" r="45" stroke="#6b8afd" stroke-width="3"/><circle cx="50" cy="50" r="20" fill="#6b8afd" opacity=".3"/><circle cx="50" cy="50" r="8" fill="#6b8afd"/></svg>
<div>
<div class="logo-text">IX Web</div>
<div class="logo-sub">Inference-X</div>
</div>
</div>
<div class="controls">
<select id="modelSelect" class="model-select" onchange="onModelChange()">
<option value="auto">auto (smallest)</option>
</select>
<div class="hw-badge" id="hwBadge">
<div class="hw-dot" id="hwDot"></div>
<span id="hwText">connecting...</span>
</div>
<button class="settings-btn" onclick="toggleSettings()" title="Settings"></button>
</div>
</div>
<div class="chat" id="chat">
<div class="welcome" id="welcome">
<div class="welcome-inner">
<div class="welcome-title">Your AI. Your hardware.</div>
<div class="welcome-desc">IX Web runs AI models locally with Inference-X. No cloud, no API keys, no data leaving your machine.</div>
<div class="welcome-grid">
<div class="welcome-card" onclick="quickSend('Explain quantum computing in simple terms')">
<div class="welcome-card-title">Explain</div>
<div class="welcome-card-desc">Quantum computing in simple terms</div>
</div>
<div class="welcome-card" onclick="quickSend('Write a Python function to sort a list')">
<div class="welcome-card-title">Code</div>
<div class="welcome-card-desc">Python sort function</div>
</div>
<div class="welcome-card" onclick="quickSend('What are the benefits of open source AI?')">
<div class="welcome-card-title">Discuss</div>
<div class="welcome-card-desc">Open source AI benefits</div>
</div>
<div class="welcome-card" onclick="quickSend('Translate to French: The future belongs to those who build it')">
<div class="welcome-card-title">Translate</div>
<div class="welcome-card-desc">English → French</div>
</div>
</div>
</div>
</div>
<div class="typing" id="typing">
<div class="typing-dots"><span></span><span></span><span></span></div>
</div>
</div>
<div class="input-area">
<div class="input-wrap">
<div class="input-box">
<textarea id="input" placeholder="Send a message..." rows="1" onkeydown="handleKey(event)" oninput="autoGrow(this)"></textarea>
</div>
<button class="send-btn" id="sendBtn" onclick="send()" title="Send">
<svg 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 class="footer-text">
Inference-X · <span id="modelName">auto</span> · <span id="hwSummary">0 models</span> · Open source under BSL-1.1
</div>
</div>
<div class="settings" id="settings">
<button class="settings-close" onclick="toggleSettings()"></button>
<h3>Settings</h3>
<label>Temperature <span class="val" id="tempVal">0.7</span></label>
<input type="range" id="tempSlider" min="0" max="2" step="0.1" value="0.7" oninput="document.getElementById('tempVal').textContent=this.value">
<label>Max tokens <span class="val" id="tokVal">512</span></label>
<input type="range" id="tokSlider" min="32" max="4096" step="32" value="512" oninput="document.getElementById('tokVal').textContent=this.value">
<label>Top-p <span class="val" id="topPVal">0.9</span></label>
<input type="range" id="topPSlider" min="0" max="1" step="0.05" value="0.9" oninput="document.getElementById('topPVal').textContent=this.value">
<label style="margin-top:20px; font-size:.7rem; color:var(--tx3);">API endpoint</label>
<input type="text" id="apiUrl" value="" style="width:100%;background:var(--bg3);border:1px solid var(--bd);color:var(--tx);padding:8px;border-radius:var(--r);font-family:var(--mono);font-size:.75rem;margin-top:4px;" placeholder="auto-detect">
</div>
<script>
var API = ''; // auto-detect: same origin
var busy = false;
var messages = [];
function getApi() {
var custom = document.getElementById('apiUrl').value.trim();
return custom || '';
}
function api(method, path, body) {
var base = getApi();
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
return fetch(base + path, opts).then(function(r) { return r.json(); });
}
// ─── Init ───
function init() {
loadModels();
loadHardware();
document.getElementById('input').focus();
}
function loadModels() {
api('GET', '/v1/models').then(function(r) {
var sel = document.getElementById('modelSelect');
var models = (r && r.data) || [];
models.sort(function(a, b) { return a.size_gb - b.size_gb; });
models.forEach(function(m) {
if (!m.ready) return;
var opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.id + ' (' + m.size_gb + ' GB)';
sel.appendChild(opt);
});
var count = models.filter(function(m) { return m.ready; }).length;
document.getElementById('hwSummary').textContent = count + ' models';
}).catch(function() {
document.getElementById('hwSummary').textContent = 'offline';
});
}
function loadHardware() {
api('GET', '/health').then(function(r) {
if (r && r.status === 'ok') {
document.getElementById('hwDot').className = 'hw-dot';
document.getElementById('hwText').textContent = r.ram_gb + 'GB · ' + r.cores + ' cores';
}
}).catch(function() {
document.getElementById('hwDot').className = 'hw-dot offline';
document.getElementById('hwText').textContent = 'offline';
});
}
function onModelChange() {
var sel = document.getElementById('modelSelect');
document.getElementById('modelName').textContent = sel.value;
}
function toggleSettings() {
document.getElementById('settings').classList.toggle('open');
}
// ─── Chat ───
function addMessage(role, content, meta) {
var welcome = document.getElementById('welcome');
if (welcome) welcome.style.display = 'none';
var chat = document.getElementById('chat');
var typing = document.getElementById('typing');
var div = document.createElement('div');
div.className = 'msg ' + role;
var avatar = document.createElement('div');
avatar.className = 'msg-avatar';
avatar.textContent = role === 'user' ? 'Y' : 'IX';
var body = document.createElement('div');
body.className = 'msg-body';
body.innerHTML = formatContent(content);
div.appendChild(avatar);
div.appendChild(body);
if (meta) {
var metaDiv = document.createElement('div');
metaDiv.className = 'msg-meta';
metaDiv.textContent = meta;
body.appendChild(metaDiv);
}
chat.insertBefore(div, typing);
chat.scrollTop = chat.scrollHeight;
}
function formatContent(text) {
// Basic markdown: code blocks, inline code, bold, italic, paragraphs
text = text.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// Paragraphs
var parts = text.split('\n\n');
if (parts.length > 1) {
text = parts.map(function(p) {
if (p.trim().startsWith('<pre>')) return p;
return '<p>' + p.replace(/\n/g, '<br>') + '</p>';
}).join('');
} else {
text = text.replace(/\n/g, '<br>');
}
return text;
}
function quickSend(text) {
document.getElementById('input').value = text;
send();
}
function send() {
if (busy) return;
var input = document.getElementById('input');
var text = input.value.trim();
if (!text) return;
input.value = '';
input.style.height = 'auto';
busy = true;
document.getElementById('sendBtn').disabled = true;
addMessage('user', text);
messages.push({ role: 'user', content: text });
// Show typing
var typing = document.getElementById('typing');
typing.classList.add('active');
document.getElementById('chat').scrollTop = document.getElementById('chat').scrollHeight;
var model = document.getElementById('modelSelect').value;
var temp = parseFloat(document.getElementById('tempSlider').value);
var maxTok = parseInt(document.getElementById('tokSlider').value);
var topP = parseFloat(document.getElementById('topPSlider').value);
api('POST', '/v1/chat/completions', {
model: model,
messages: messages,
temperature: temp,
max_tokens: maxTok,
top_p: topP
}).then(function(r) {
typing.classList.remove('active');
if (r.error) {
addMessage('assistant', 'Error: ' + r.error);
} else {
var content = r.choices[0].message.content;
var ix = r.ix || {};
var meta = r.model + ' · ' + (ix.elapsed || '?') + 's · ' + (ix.tokens_per_second || '?') + ' tok/s';
addMessage('assistant', content, meta);
messages.push({ role: 'assistant', content: content });
}
}).catch(function(e) {
typing.classList.remove('active');
addMessage('assistant', 'Connection error: ' + e.message + '. Is the IX Web server running?');
}).finally(function() {
busy = false;
document.getElementById('sendBtn').disabled = false;
document.getElementById('input').focus();
});
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
}
function autoGrow(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
}
init();
</script>
</body>
</html>