Echo-IX v1.0 — AI chat powered by Inference-X. BSL-1.1. Signature 935.

This commit is contained in:
Salka Elmadani 2026-02-25 01:50:55 +00:00
commit a9a464e2a5
6 changed files with 464 additions and 0 deletions

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
Business Source License 1.1
Licensor: Salka Elmadani, Holding SALKA ELMADANI SA (Zug)
Licensed Work: Echo-IX
Change Date: 2030-02-12
Change License: Apache License 2.0
Additional Use Grant: Revenue < $1M USD = free use.
Full text: https://mariadb.com/bsl11/

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# Echo-IX
AI chat interface powered by [Inference-X](https://inference-x.com). 228 KB engine. BSL-1.1.
## Run
```bash
IX_HOST=127.0.0.1 IX_PORT=8081 node server.js
```
## API
- `POST /api/echo/chat` — Chat completion
- `POST /api/echo/stream` — SSE streaming
- `GET /api/echo/models` — Available models
- `GET /api/echo/hardware` — Backend status
- `GET /api/health` — Service health
## License
BSL-1.1 -> Apache 2.0 on 2030-02-12. Signature 935.

7
deploy.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -e
echo "=== Echo-IX Deploy ==="
pm2 delete echo-ix 2>/dev/null || true
IX_HOST=${IX_HOST:-127.0.0.1} IX_PORT=${IX_PORT:-8081} pm2 start server.js --name echo-ix
pm2 save
echo "Echo-IX running"

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"name": "echo-ix",
"version": "1.0.0",
"description": "Echo - AI chat by Inference-X",
"main": "server.js",
"scripts": {"start": "node server.js"},
"license": "BUSL-1.1",
"private": true
}

389
public/index.html Normal file
View File

@ -0,0 +1,389 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Echo - Inference-X</title>
<meta name="description" content="Echo AI chat. 228KB engine. Your hardware, your data.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#x25CE;</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--font:'Outfit','Noto Sans Arabic',sans-serif;--mono:'JetBrains Mono',monospace;--r:6px;--tr:.25s ease}
[data-theme="dark"]{--bg:#0b0b12;--bg2:#111119;--bg3:#1a1a26;--sf:#1c1c2b;--bd:#2a2a3d;--bd2:#353550;--tx:#e8e8f0;--txd:#9090a8;--txm:#5a5a72;--ac:#e05a3a;--ac2:#4d9fff;--ac3:#d4a843;--gn:#4ddd7d;--cy:#4dd8d0;--rd:#e05a3a;--zo:.04;--cb:rgba(17,17,25,.85);--gw:rgba(224,90,58,.08);--sh:0 4px 24px rgba(0,0,0,.4)}
[data-theme="light"]{--bg:#f5f0e8;--bg2:#ede7db;--bg3:#e5dfd2;--sf:#fff;--bd:#d4cec0;--bd2:#c4baa8;--tx:#1a1612;--txd:#5a5248;--txm:#8a8070;--ac:#c04828;--ac2:#2a6db8;--ac3:#b08830;--gn:#2a8a4a;--cy:#1a8a82;--rd:#c04828;--zo:.06;--cb:rgba(255,255,255,.9);--gw:rgba(192,72,40,.05);--sh:0 4px 24px rgba(0,0,0,.08)}
body{font-family:var(--font);background:var(--bg);color:var(--tx);height:100vh;overflow:hidden}
a{color:var(--ac2);text-decoration:none}
.nav{height:48px;display:flex;align-items:center;padding:0 1.2rem;border-bottom:1px solid var(--bd);background:var(--bg2);justify-content:space-between}
.nav-left{display:flex;align-items:center;gap:.8rem}
.nav-logo{font-family:var(--mono);font-weight:700;font-size:.9rem;color:var(--tx);text-decoration:none;display:flex;align-items:center;gap:.4rem}
.nav-logo span{color:var(--cy)}
.nav-right{display:flex;align-items:center;gap:.6rem}
.nav-link{font-size:.75rem;color:var(--txd);padding:.3rem .6rem;border-radius:var(--r);transition:all .2s;text-decoration:none}
.nav-link:hover{color:var(--tx);background:var(--bg3)}
.theme-btn{background:none;border:1px solid var(--bd);color:var(--txm);width:28px;height:28px;border-radius:50%;cursor:pointer;font-size:.7rem}
.page{display:none;height:calc(100vh - 48px)}.page.active{display:block}
@keyframes fi{from{opacity:0}to{opacity:1}}
/* ═══ ECHO — Industrial Chat Interface ═══ */
.echo-wrap{display:flex;flex-direction:column;height:calc(100vh - 52px);background:var(--bg)}
.echo-top{padding:.5rem 1.2rem;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:.6rem;background:var(--bg2);flex-wrap:wrap;min-height:42px}
.echo-brand{display:flex;align-items:center;gap:.4rem}
.echo-dot{width:8px;height:8px;border-radius:50%;background:var(--gn);animation:pulse 2s infinite;flex-shrink:0}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.echo-name{font-family:var(--mono);font-weight:700;font-size:.88rem;color:var(--tx);letter-spacing:.5px}
.echo-ver{font-family:var(--mono);font-size:.55rem;color:var(--txm);background:var(--bg3);padding:1px 5px;border-radius:3px;letter-spacing:.5px}
.echo-model-wrap{position:relative}
.echo-sel{background:var(--bg);border:1px solid var(--bd);color:var(--tx);padding:4px 24px 4px 8px;border-radius:var(--r);font-size:.72rem;font-family:var(--mono);cursor:pointer;max-width:240px;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%239090a8'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;transition:border-color .2s}
.echo-sel:hover,.echo-sel:focus{border-color:var(--ac);outline:none}
.echo-sel option{background:var(--bg2);color:var(--tx);font-family:var(--mono)}
.echo-sel option:disabled{color:var(--txm);font-style:italic}
.echo-hw{display:flex;gap:.5rem;margin-left:auto;font-family:var(--mono);font-size:.6rem;color:var(--txm);align-items:center;flex-wrap:wrap}
.echo-hw b{color:var(--txd);font-weight:600}
.echo-hw .on{color:var(--gn)}.echo-hw .off{color:var(--rd)}
.hw-sep{color:var(--bd);font-size:.5rem}
/* Messages area */
.echo-msgs{flex:1;overflow-y:auto;padding:0;scroll-behavior:smooth;position:relative}
/* Welcome state */
.echo-welcome{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100%;padding:2rem;text-align:center;animation:fadeUp .5s ease}
@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
.echo-welcome-icon{font-size:2.5rem;margin-bottom:.5rem;opacity:.4}
.echo-welcome h2{font-family:var(--mono);font-size:1.4rem;font-weight:700;color:var(--tx);margin-bottom:.3rem}
.echo-welcome p{color:var(--txm);font-size:.82rem;max-width:400px;margin-bottom:1.5rem}
.echo-suggestions{display:flex;flex-direction:column;gap:.4rem;max-width:400px;width:100%}
.echo-sug{background:var(--bg2);border:1px solid var(--bd);border-radius:8px;padding:.55rem .9rem;font-size:.78rem;color:var(--txd);text-align:left;cursor:pointer;font-family:var(--font);transition:all .2s;line-height:1.4}
.echo-sug:hover{border-color:var(--ac);color:var(--tx);background:var(--bg3);transform:translateX(4px)}
.echo-notice{margin-top:1.5rem;padding:.5rem .9rem;border-radius:var(--r);background:color-mix(in srgb,var(--ac3) 8%,transparent);border:1px solid color-mix(in srgb,var(--ac3) 20%,transparent);font-size:.68rem;color:var(--ac3);font-family:var(--mono);max-width:400px;line-height:1.5}
/* Chat messages */
.echo-msg{max-width:780px;margin:0 auto;padding:0 1.5rem;animation:msgIn .3s ease}
@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.echo-msg+.echo-msg{margin-top:.3rem}
.echo-msg.user{padding-top:.8rem}
.echo-msg.ai{padding-bottom:.4rem}
/* User message */
.echo-msg.user .bub{background:var(--bg3);border:1px solid var(--bd);border-radius:12px 12px 4px 12px;padding:.6rem 1rem;font-size:.86rem;line-height:1.65;display:inline-block;max-width:80%;float:right;color:var(--tx);word-wrap:break-word}
.echo-msg.user::after{content:'';display:block;clear:both}
/* AI message */
.echo-msg.ai .bub{padding:.6rem 0;font-size:.86rem;line-height:1.75;max-width:100%;color:var(--tx);word-wrap:break-word}
/* Meta info */
.echo-msg .meta{font-size:.58rem;color:var(--txm);margin-top:.15rem;font-family:var(--mono);clear:both;display:flex;align-items:center;gap:.6rem}
.echo-msg .meta .tps{color:var(--cy)}
.echo-msg .meta .model-tag{color:var(--ac3)}
/* Thinking/loading state */
.echo-thinking{display:flex;align-items:center;gap:.5rem;padding:.6rem 0;color:var(--txm);font-size:.78rem;font-family:var(--mono)}
.echo-thinking-dots{display:flex;gap:3px}
.echo-thinking-dots span{width:5px;height:5px;background:var(--ac);border-radius:50%;animation:thinkBounce .8s infinite alternate}
.echo-thinking-dots span:nth-child(2){animation-delay:.15s}
.echo-thinking-dots span:nth-child(3){animation-delay:.3s}
@keyframes thinkBounce{to{opacity:.15;transform:scale(.6)}}
.echo-thinking-text{animation:thinkFade 2s infinite}
@keyframes thinkFade{0%,100%{opacity:.5}50%{opacity:1}}
/* Markdown in chat */
.echo-msg .bub p{margin:.3rem 0}
.echo-msg .bub strong{font-weight:700;color:var(--tx)}
.echo-msg .bub em{font-style:italic;color:var(--txd)}
.echo-msg .bub code{font-family:var(--mono);background:var(--bg3);padding:1px 6px;border-radius:3px;font-size:.8rem;border:1px solid var(--bd)}
.echo-msg .bub pre{background:var(--bg2);border:1px solid var(--bd);border-radius:8px;padding:.8rem 1rem;overflow-x:auto;margin:.6rem 0;font-family:var(--mono);font-size:.76rem;line-height:1.6;direction:ltr;text-align:left;position:relative}
.echo-msg .bub pre code{background:none;padding:0;border:none;font-size:inherit}
.echo-msg .bub ul,.echo-msg .bub ol{padding-left:1.2rem;margin:.4rem 0}
.echo-msg .bub li{margin:.15rem 0;line-height:1.6}
.echo-msg .bub a{color:var(--ac2);text-decoration:underline;text-underline-offset:2px}
.echo-msg .bub blockquote{border-left:3px solid var(--ac3);padding:.3rem 0 .3rem .8rem;color:var(--txd);margin:.5rem 0;font-style:italic}
.echo-msg .bub h2,.echo-msg .bub h3,.echo-msg .bub h4{margin:.6rem 0 .2rem;font-weight:700;color:var(--tx)}
.echo-msg .bub h2{font-size:.95rem}.echo-msg .bub h3{font-size:.88rem}.echo-msg .bub h4{font-size:.84rem}
/* Input area */
.echo-input{padding:.6rem 1.2rem .4rem;border-top:1px solid var(--bd);background:var(--bg)}
.echo-bar{max-width:780px;margin:0 auto;display:flex;gap:.5rem;align-items:end;position:relative}
.echo-bar textarea{flex:1;padding:.65rem 1rem;background:var(--bg2);border:1px solid var(--bd);border-radius:12px;color:var(--tx);font-size:.86rem;font-family:var(--font);resize:none;max-height:150px;line-height:1.5;transition:border-color .2s}
.echo-bar textarea:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 3px color-mix(in srgb,var(--ac) 10%,transparent)}
.echo-bar textarea::placeholder{color:var(--txm)}
.echo-send{width:36px;height:36px;border-radius:50%;background:var(--ac);border:none;color:white;font-size:1rem;cursor:pointer;transition:all .15s;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700}
.echo-send:hover{transform:scale(1.05);filter:brightness(1.1)}
.echo-send:disabled{opacity:.2;cursor:not-allowed;transform:none}
.echo-stop{padding:.3rem .7rem;border-radius:20px;background:transparent;border:1px solid var(--rd);color:var(--rd);font-size:.68rem;cursor:pointer;font-family:var(--mono);display:none;transition:all .15s}
.echo-stop.show{display:inline-block}
.echo-stop:hover{background:var(--rd);color:white}
.echo-footer{max-width:780px;margin:.3rem auto 0;display:flex;gap:.5rem;justify-content:center;font-size:.55rem;color:var(--txm);font-family:var(--mono)}
/* Responsive */
@media(max-width:768px){
.echo-hw{display:none}
.echo-bar textarea{font-size:.82rem}
.echo-msg{padding:0 .8rem}
.echo-msg.user .bub{max-width:90%}
.echo-suggestions{padding:0 1rem}
.echo-welcome{padding:1.5rem 1rem}
}
/* ═══ MODELS STORE ═══ */
</style>
</head>
<body>
<nav class="nav">
<div class="nav-left">
<a href="/" class="nav-logo">&#x25CE; <span>Echo</span></a>
<span style="font-size:.5rem;color:var(--txm);font-family:var(--mono)">by Inference-X</span>
</div>
<div class="nav-right">
<a href="https://inference-x.com" class="nav-link" target="_blank">inference-x.com</a>
<a href="https://inference-x.com/docs" class="nav-link" target="_blank">Docs</a>
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">&#x25D0;</button>
</div>
</nav>
<div class="page" id="page-echo">
<div class="echo-wrap">
<div class="echo-top">
<div class="echo-brand"><span class="echo-dot"></span><span class="echo-name">Echo</span><span class="echo-ver">v8</span></div>
<div class="echo-model-wrap">
<select class="echo-sel" id="echoModel" onchange="onModelSwitch()"><option value="">Loading...</option></select>
</div>
<button class="echo-stop" id="echoStop" onclick="stopGen()">■ Stop</button>
<div class="echo-hw" id="echoHwBar">
<span id="hwDot" class="on">● online</span>
<span class="hw-sep">|</span>
<span id="hwActive" style="color:var(--cy)"></span>
<span class="hw-sep">|</span>
<span><b id="hwRAM">-</b> RAM</span>
<span><b id="hwCPU">-</b> vCPU</span>
<span><b id="hwN">-</b> loaded</span>
</div>
</div>
<div class="echo-msgs" id="echoMsgs">
<div class="echo-welcome">
<div class="echo-welcome-icon"></div>
<h2>Echo</h2>
<p>228 KB inference engine. Your prompt, your hardware, your data.</p>
<div class="echo-suggestions">
<button class="echo-sug" onclick="useSug(this)">Explain transformer attention in simple terms</button>
<button class="echo-sug" onclick="useSug(this)">Write a Python quicksort with comments</button>
<button class="echo-sug" onclick="useSug(this)">What makes a good API design?</button>
</div>
<div class="echo-notice">Running on Montagne (64GB/16vCPU). First response may take 10-20s while the model loads. Streaming enabled.</div>
</div>
</div>
<div class="echo-input">
<div class="echo-bar">
<textarea id="echoIn" rows="1" placeholder="Message Echo..." onkeydown="echoKey(event)" oninput="autoGrow(this)"></textarea>
<button class="echo-send" id="echoSend" onclick="sendEcho()" title="Send (Enter)"></button>
</div>
<div class="echo-footer">
<span>Inference-X engine 228KB</span>
<span></span>
<span>BSL-1.1</span>
<span></span>
<span id="echoFooterModel">auto</span>
</div>
</div>
</div>
</div>
<script>
function toggleTheme(){var t=document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark';document.documentElement.setAttribute('data-theme',t);try{localStorage.setItem('theme',t)}catch(e){}}
try{var st=localStorage.getItem('theme');if(st)document.documentElement.setAttribute('data-theme',st)}catch(e){}
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function md(s){if(!s)return'';return s
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/```(\w*)\n([\s\S]*?)```/g,(m,lang,code)=>'<pre><code>'+code.trim()+'</code></pre>')
.replace(/`([^`]+)`/g,'<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
.replace(/\*(.+?)\*/g,'<em>$1</em>')
.replace(/^### (.+)$/gm,'<h4 style="margin:.5rem 0 .2rem;font-size:.85rem">$1</h4>')
.replace(/^## (.+)$/gm,'<h3 style="margin:.5rem 0 .2rem;font-size:.9rem">$1</h3>')
.replace(/^# (.+)$/gm,'<h2 style="margin:.5rem 0 .2rem">$1</h2>')
.replace(/^> (.+)$/gm,'<blockquote>$1</blockquote>')
.replace(/^- (.+)$/gm,'<li>$1</li>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" target="_blank">$1</a>')
.replace(/\n/g,'<br>')}
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
/* ═══ ECHO JS — Rewritten ═══ */
/* ECHO JS — Rewritten */
let echoHist=[],echoCtrl=null,hwInterval=null,echoStarted=false;
function loadModels_echo(){
api('GET','/api/echo/models').then(r=>{
const sel=document.getElementById('echoModel');if(!sel)return;sel.innerHTML='';
const auto=document.createElement('option');auto.value='auto';auto.textContent='auto (recommended)';sel.appendChild(auto);
if(r.ok){
const models=(r.data.data||r.data.models||[]).sort((a,b)=>(a.size_gb||0)-(b.size_gb||0));
models.forEach(m=>{
const o=document.createElement('option');o.value=m.id;
const sz=m.size_gb?' \u00B7 '+m.size_gb+'GB':'';
const st=m.ready===false?' (not loaded)':'';
o.textContent=m.id+sz+st;
if(m.ready===false){o.disabled=true;o.style.color='var(--txm)'}
sel.appendChild(o);
});
}
updateFooterModel();
}).catch(()=>{});
}
var S={token:null};
function api(m,p,b){const o={method:m,headers:{'Content-Type':'application/json'}}
let echoHist=[],echoCtrl=null,hwInterval=null,echoStarted=false;
function loadModels_echo(){
api('GET','/api/echo/models').then(r=>{
const sel=document.getElementById('echoModel');if(!sel)return;sel.innerHTML='';
const auto=document.createElement('option');auto.value='auto';auto.textContent='auto (recommended)';sel.appendChild(auto);
if(r.ok){
const models=(r.data.data||r.data.models||[]).sort((a,b)=>(a.size_gb||0)-(b.size_gb||0));
models.forEach(m=>{
const o=document.createElement('option');o.value=m.id;
const sz=m.size_gb?' \u00B7 '+m.size_gb+'GB':'';
const st=m.ready===false?' (not loaded)':'';
o.textContent=m.id+sz+st;
if(m.ready===false){o.disabled=true;o.style.color='var(--txm)'}
sel.appendChild(o);
});
}
updateFooterModel();
}).catch(()=>{});
}
function updateFooterModel(){
const sel=document.getElementById('echoModel');
const ft=document.getElementById('echoFooterModel');
if(sel&&ft)ft.textContent=sel.value||'auto';
}
function updateHW(){
api('GET','/api/echo/hardware').then(r=>{
if(!r.ok)return;const d=r.data;
const dot=document.getElementById('hwDot');
if(dot){dot.textContent=d.status==='online'?'\u25CF online':'\u25CF offline';dot.className=d.status==='online'?'on':'off'}
const g=id=>document.getElementById(id);
if(g('hwRAM'))g('hwRAM').textContent=(d.ram_gb||0)+'GB';
if(g('hwCPU'))g('hwCPU').textContent=(d.cores||0);
if(g('hwN'))g('hwN').textContent=(d.models||0);
if(g('hwActive'))g('hwActive').textContent=d.active_model?'\u25B6 '+d.active_model:'';
}).catch(()=>{});
}
function onModelSwitch(){
updateFooterModel();
if(echoStarted){
const sel=document.getElementById('echoModel');
addMsg('system','<span style="color:var(--cy);font-size:.78rem">Switched to <b>'+esc(sel.value||'auto')+'</b></span>');
echoHist=[];
}
}
function useSug(el){document.getElementById('echoIn').value=el.textContent;sendEcho()}
function autoGrow(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,150)+'px'}
function echoKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendEcho()}}
function addMsg(role,html,meta){
if(!echoStarted){var w=document.querySelector('.echo-welcome');if(w)w.style.display='none';echoStarted=true}
var msgs=document.getElementById('echoMsgs');
var d=document.createElement('div');
d.className=role==='system'?'echo-msg ai':'echo-msg '+role;
d.innerHTML='<div class="bub">'+html+'</div>';
if(meta){var md2=document.createElement('div');md2.className='meta';md2.innerHTML=meta;d.appendChild(md2)}
msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;return d;
}
function sendEcho(){
var inp=document.getElementById('echoIn'),msg=inp.value.trim();if(!msg)return;
inp.value='';inp.style.height='auto';
var model=document.getElementById('echoModel').value;
addMsg('user',esc(msg),'<span>'+new Date().toLocaleTimeString()+'</span>');
echoHist.push({role:'user',content:msg});
var aiDiv=addMsg('ai',
'<div class="echo-thinking">'+
'<div class="echo-thinking-dots"><span></span><span></span><span></span></div>'+
'<span class="echo-thinking-text" id="echoThinkText">Routing...</span>'+
'</div>'
);
var bub=aiDiv.querySelector('.bub');
var sendBtn=document.getElementById('echoSend');
var stopBtn=document.getElementById('echoStop');
sendBtn.disabled=true;stopBtn.classList.add('show');
var fullText='',t0=Date.now(),firstToken=false;
var thinkText=document.getElementById('echoThinkText');
var thinkTimer=setInterval(function(){
var s=Math.floor((Date.now()-t0)/1000);
if(!firstToken&&thinkText){
if(s<3)thinkText.textContent='Routing to model...';
else if(s<10)thinkText.textContent='Generating ('+s+'s)...';
else thinkText.textContent='Still working ('+s+'s)...';
}
},1000);
echoCtrl=new AbortController();
fetch('/api/echo/stream',{
method:'POST',
headers:{'Content-Type':'application/json',...(S.token?{Authorization:'Bearer '+S.token}:{})},
body:JSON.stringify({messages:echoHist,model:model||'auto',max_tokens:2048,temperature:0.7}),
signal:echoCtrl.signal
}).then(function(res){
if(!res.ok)throw new Error('not-sse');
var reader=res.body.getReader(),dec=new TextDecoder(),buf='',rModel='';
function read(){
reader.read().then(function(r2){
if(r2.done){finish(rModel);return}
buf+=dec.decode(r2.value,{stream:true});
var lines=buf.split('\n');buf=lines.pop();
lines.forEach(function(line){
if(!line.startsWith('data: '))return;
var data=line.slice(6);
if(data==='[DONE]'){finish(rModel);return}
try{
var j=JSON.parse(data);
if(j.model)rModel=j.model;
var delta=j.choices&&j.choices[0]&&j.choices[0].delta;
if(delta&&delta.content){
if(!firstToken){firstToken=true;clearInterval(thinkTimer);bub.innerHTML=''}
fullText+=delta.content;
bub.innerHTML=md(fullText);
document.getElementById('echoMsgs').scrollTop=99999;
}
}catch(e){}
});
read();
}).catch(function(e){if(e.name!=='AbortError')finish(rModel)});
}
read();
}).catch(function(e){
if(e.name==='AbortError')return;
clearInterval(thinkTimer);
api('POST','/api/echo/chat',{messages:echoHist,model:model||'auto',max_tokens:2048}).then(function(r){
if(r.ok){fullText=r.data.choices[0].message.content;bub.innerHTML=md(fullText)}
else{bub.innerHTML='<span style="color:var(--txm)">Backend is loading. Try again in a moment.</span>'}
finish(r.ok?r.data.model:'');
}).catch(function(){
bub.innerHTML='<span style="color:var(--txm)">Connection error. Backend may be restarting.</span>';
finish('');
});
});
function finish(rModel){
clearInterval(thinkTimer);
var elapsed=((Date.now()-t0)/1000).toFixed(1);
if(fullText){echoHist.push({role:'assistant',content:fullText});bub.innerHTML=md(fullText)}
var metaDiv=document.createElement('div');metaDiv.className='meta';
metaDiv.innerHTML='<span class="model-tag">'+(rModel||model||'auto')+'</span><span>'+elapsed+'s</span><span>Montagne</span>';
aiDiv.appendChild(metaDiv);
sendBtn.disabled=false;stopBtn.classList.remove('show');echoCtrl=null;
document.getElementById('echoMsgs').scrollTop=99999;updateHW();
}
}
function stopGen(){if(echoCtrl){echoCtrl.abort();echoCtrl=null}}
document.getElementById('page-echo').classList.add('active');
loadModels_echo();updateHW();setInterval(updateHW,15000);
</script>
</body>
</html>

29
server.js Normal file
View File

@ -0,0 +1,29 @@
var http=require('http'),fs=require('fs'),path=require('path');
var PORT=parseInt(process.env.PORT||'3090');
var IX_HOST=process.env.IX_HOST||'127.0.0.1';
var IX_PORT=parseInt(process.env.IX_PORT||'8081');
var DEFAULT_MODEL='qwen2.5-3b-instruct-q4_k_m';
var rates={};
function rateOk(k,m,w){var n=Date.now();if(!rates[k])rates[k]=[];rates[k]=rates[k].filter(function(t){return n-t<w});if(rates[k].length>=m)return false;rates[k].push(n);return true}
function ixProxy(p,b,to,cb){var d=JSON.stringify(b);var o={hostname:IX_HOST,port:IX_PORT,path:p,method:'POST',timeout:to,headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(d)}};var r=http.request(o,function(res){var c=[];res.on('data',function(x){c.push(x)});res.on('end',function(){try{cb(null,JSON.parse(Buffer.concat(c).toString()))}catch(e){cb(new Error('Parse'))}})});r.on('error',function(e){cb(e)});r.on('timeout',function(){r.destroy();cb(new Error('Timeout'))});r.write(d);r.end()}
function ixGet(p,cb){http.get({hostname:IX_HOST,port:IX_PORT,path:p,timeout:5000},function(res){var c=[];res.on('data',function(x){c.push(x)});res.on('end',function(){try{cb(null,JSON.parse(Buffer.concat(c).toString()))}catch(e){cb(new Error('Parse'))}})}).on('error',function(e){cb(e)})}
function json(res,d,s){res.writeHead(s||200,{'Content-Type':'application/json'});res.end(JSON.stringify(d))}
function readBody(req,cb){var c=[];req.on('data',function(x){c.push(x)});req.on('end',function(){try{cb(JSON.parse(Buffer.concat(c).toString()))}catch(e){cb({})}})}
var srv=http.createServer(function(req,res){
res.setHeader('Access-Control-Allow-Origin','*');
res.setHeader('Access-Control-Allow-Methods','GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers','Content-Type,Authorization');
if(req.method==='OPTIONS'){res.writeHead(204);res.end();return}
res.setHeader('X-Frame-Options','DENY');
res.setHeader('X-Content-Type-Options','nosniff');
var url=req.url.split('?')[0];
if(url==='/api/health')return json(res,{status:'ok',service:'echo-ix',version:'1.0',signature:'935'});
if(url==='/api/echo/models'){ixGet('/v1/models',function(e,d){if(!e&&d&&d.data)return json(res,{data:d.data,source:'live'});json(res,{data:[],source:'offline'})});return}
if(url==='/api/echo/hardware'){ixGet('/v1/models',function(e,d){if(e)return json(res,{status:'offline',ram_gb:64,cores:16,models:0});json(res,{status:'online',ram_gb:64,cores:16,models:d.data?d.data.length:0,active_model:DEFAULT_MODEL})});return}
if(url==='/api/echo/chat'&&req.method==='POST'){var ip=req.socket.remoteAddress;if(!rateOk('e:'+ip,30,6e4))return json(res,{error:'Rate limit'},429);readBody(req,function(b){var msgs=b.messages||[];if(!msgs.length)return json(res,{error:'Messages required'},400);var t0=Date.now();var m=b.model==='auto'||!b.model?DEFAULT_MODEL:b.model;ixProxy('/v1/chat/completions',{model:m,messages:msgs,max_tokens:Math.min(b.max_tokens||512,2048),temperature:b.temperature||0.7},120000,function(e,d){if(e)return json(res,{choices:[{message:{role:'assistant',content:'Backend loading.'},finish_reason:'stop'}],ix:{backend:'offline'}});d.ix={backend:m,latency_ms:Date.now()-t0};json(res,d)})});return}
if(url==='/api/echo/stream'&&req.method==='POST'){var ip=req.socket.remoteAddress;if(!rateOk('e:'+ip,30,6e4))return json(res,{error:'Rate limit'},429);readBody(req,function(b){var msgs=b.messages||[];if(!msgs.length)return json(res,{error:'Messages required'},400);res.writeHead(200,{'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive','X-Accel-Buffering':'no'});var m=b.model==='auto'||!b.model?DEFAULT_MODEL:b.model;var d=JSON.stringify({model:m,messages:msgs,max_tokens:Math.min(b.max_tokens||512,2048),temperature:b.temperature||0.7,stream:true});var o={hostname:IX_HOST,port:IX_PORT,path:'/v1/chat/completions',method:'POST',timeout:120000,headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(d)}};var p=http.request(o,function(pR){pR.on('data',function(c){res.write(c)});pR.on('end',function(){res.end()})});p.on('error',function(){res.end()});p.write(d);p.end();req.on('close',function(){p.destroy()})});return}
if(url==='/'||url==='/echo')url='/index.html';
var ext=path.extname(url);var mime={'.html':'text/html','.css':'text/css','.js':'application/javascript','.png':'image/png','.svg':'image/svg+xml'};
fs.readFile(path.join(__dirname,'public',url),function(e,d){if(e){fs.readFile(path.join(__dirname,'public','index.html'),function(e2,d2){if(e2){res.writeHead(404);res.end('Not found');return}res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'});res.end(d2)});return}res.writeHead(200,{'Content-Type':(mime[ext]||'application/octet-stream')+';charset=utf-8'});res.end(d)});
});
srv.listen(PORT,function(){console.log('Echo-IX :'+PORT+' -> '+IX_HOST+':'+IX_PORT+' | sig 935')});