Better output from the same model. Fused computation, adaptive precision, surgical expert loading. 305 KB, 19 backends, zero dependencies. https://inference-x.com
368 lines
16 KiB
HTML
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>
|