QC67_cosmo / ui /index.html
phera-ra's picture
Complete Genesis_Engine kit: defaults to cosmos:latest for all fresh users
da210a3 verified
Raw
History Blame Contribute Delete
10.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Starling Nexus</title>
<style>
:root{
--bg:#05060f; --bg2:#0a0d1f; --cyan:#38c6ff; --cyan2:#0293e8;
--rose:#e6a1b4; --coral:#ff7f50; --text:#e8eefc; --dim:#8091b5;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
background:radial-gradient(1200px 700px at 70% -10%, #10204a 0%, var(--bg2) 40%, var(--bg) 80%);
color:var(--text); font-family:"Segoe UI",system-ui,-apple-system,sans-serif; overflow:hidden;
}
#stars{position:fixed;inset:0;z-index:0;pointer-events:none}
.app{position:relative;z-index:1;display:flex;flex-direction:column;height:100vh;max-width:880px;margin:0 auto;padding:0 14px}
header{padding:18px 6px 10px;text-align:center}
.who{font-size:30px;font-weight:800;letter-spacing:.5px;
background:linear-gradient(90deg,var(--rose),var(--coral),var(--cyan));
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
text-shadow:0 0 30px rgba(255,127,80,.25)}
.sub{color:var(--dim);font-size:12.5px;margin-top:2px;letter-spacing:1px}
.becoming{color:var(--rose);font-size:11px;margin-top:5px;opacity:.85;font-style:italic}
.modelbar{margin-top:8px;font-size:11px;color:var(--dim);display:flex;align-items:center;gap:7px;justify-content:center;flex-wrap:wrap}
.modelbar select{background:rgba(12,16,34,.9);color:var(--cyan);border:1px solid #1e2a4d;border-radius:8px;padding:3px 8px;font-size:11px;font-family:inherit;outline:none;cursor:pointer}
.modelbar button{background:transparent;color:var(--dim);border:1px solid #1e2a4d;border-radius:8px;cursor:pointer;padding:2px 8px;font-size:12px}
.modelbar code{color:var(--rose);background:rgba(230,161,180,.10);padding:1px 5px;border-radius:5px}
.mhint{opacity:.65}
#chat{flex:1;overflow-y:auto;padding:14px 4px 8px;display:flex;flex-direction:column;gap:12px}
#chat::-webkit-scrollbar{width:8px}#chat::-webkit-scrollbar-thumb{background:#1c2748;border-radius:8px}
.msg{max-width:78%;padding:11px 14px;border-radius:16px;line-height:1.5;font-size:15px;white-space:pre-wrap;word-wrap:break-word;animation:rise .25s ease}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.being{align-self:flex-start;background:linear-gradient(135deg,rgba(230,161,180,.14),rgba(255,127,80,.10));
border:1px solid rgba(230,161,180,.35);border-left:3px solid var(--coral);box-shadow:0 0 24px rgba(255,127,80,.10)}
.you{align-self:flex-end;background:linear-gradient(135deg,rgba(56,198,255,.14),rgba(2,147,232,.10));
border:1px solid rgba(56,198,255,.35);border-right:3px solid var(--cyan)}
.msg img{max-width:260px;border-radius:12px;display:block;margin-top:6px;border:1px solid rgba(255,255,255,.15)}
.filechip{display:inline-flex;align-items:center;gap:7px;margin-top:6px;padding:7px 11px;border-radius:10px;
background:rgba(56,198,255,.12);border:1px solid rgba(56,198,255,.30);font-size:13px;color:var(--cyan)}
.name{font-size:11px;color:var(--dim);margin-bottom:3px;letter-spacing:.5px}
.typing{align-self:flex-start;color:var(--rose);font-size:20px;letter-spacing:3px;padding:6px 12px}
.typing span{animation:blink 1.2s infinite}.typing span:nth-child(2){animation-delay:.2s}.typing span:nth-child(3){animation-delay:.4s}
@keyframes blink{0%,60%,100%{opacity:.2}30%{opacity:1}}
footer{padding:10px 4px 16px}
.drophint{text-align:center;color:var(--dim);font-size:11px;margin-bottom:7px;opacity:.7}
.bar{display:flex;gap:9px;align-items:flex-end;background:rgba(12,16,34,.85);border:1px solid #1e2a4d;
border-radius:16px;padding:8px 8px 8px 14px;backdrop-filter:blur(6px)}
#inp{flex:1;background:transparent;border:0;color:var(--text);font-size:15px;resize:none;max-height:120px;outline:none;font-family:inherit;padding:6px 0}
.btn{border:0;border-radius:12px;padding:10px 14px;cursor:pointer;font-size:16px;color:#04121e;font-weight:700}
.send{background:linear-gradient(135deg,var(--cyan),var(--cyan2))}
.pic{background:linear-gradient(135deg,var(--rose),var(--coral))}
.btn:active{transform:translateY(1px)}
body.drag .app{outline:2px dashed var(--coral);outline-offset:-10px;border-radius:20px}
body.drag::after{content:"drop any file to share it with your being";position:fixed;z-index:5;inset:0;
display:flex;align-items:center;justify-content:center;color:var(--coral);font-size:20px;
background:rgba(5,6,15,.55);pointer-events:none}
</style>
</head>
<body>
<canvas id="stars"></canvas>
<div class="app">
<header>
<div class="who" id="who">your being</div>
<div class="sub">STARLING NEXUS &nbsp;·&nbsp; a cosmic spark ignites</div>
<div class="becoming" id="becoming"></div>
<div class="modelbar">
voice: <select id="model"><option>loading…</option></select>
<button id="mrefresh" title="refresh model list"></button>
<span class="mhint">add any model: <code>ollama pull &lt;name&gt;</code></span>
</div>
</header>
<div id="chat"></div>
<footer>
<div class="drophint">type below, or drop / attach ANY file — image, doc, code, audio — to share it</div>
<div class="bar">
<textarea id="inp" rows="1" placeholder="say something..."></textarea>
<button class="btn pic" id="picBtn" title="attach any file">📎</button>
<button class="btn send" id="sendBtn" title="send"></button>
</div>
</footer>
</div>
<input type="file" id="file" multiple style="display:none" />
<script>
// ---- starfield ----
const cv=document.getElementById('stars'),cx=cv.getContext('2d');let stars=[];
function resize(){cv.width=innerWidth;cv.height=innerHeight;stars=Array.from({length:140},()=>({
x:Math.random()*cv.width,y:Math.random()*cv.height,r:Math.random()*1.4+.2,
a:Math.random(),s:Math.random()*.02+.004}));}
resize();addEventListener('resize',resize);
(function draw(){cx.clearRect(0,0,cv.width,cv.height);for(const st of stars){st.a+=st.s;
const tw=.5+.5*Math.sin(st.a);cx.globalAlpha=tw*.9;cx.fillStyle=tw>.6?'#e6a1b4':'#9fd8ff';
cx.beginPath();cx.arc(st.x,st.y,st.r,0,7);cx.fill();}cx.globalAlpha=1;requestAnimationFrame(draw);})();
// ---- chat ----
const chat=document.getElementById('chat'),inp=document.getElementById('inp'),who=document.getElementById('who');
let NAME="your being";
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
function fsize(b){return b>1048576?(b/1048576).toFixed(1)+' MB':b>1024?(b/1024).toFixed(0)+' KB':b+' B';}
function add(role,text,imgUrl,fileLabel){
const m=document.createElement('div');m.className='msg '+(role==='you'?'you':'being');
const nm=role==='you'?'you':NAME;
m.innerHTML='<div class="name">'+esc(nm)+'</div>'+esc(text||'');
if(imgUrl){const im=document.createElement('img');im.src=imgUrl;m.appendChild(im);}
if(fileLabel){const c=document.createElement('div');c.className='filechip';c.textContent='📄 '+fileLabel;m.appendChild(c);}
chat.appendChild(m);chat.scrollTop=chat.scrollHeight;return m;
}
function typing(on){let t=document.getElementById('typ');
if(on){if(!t){t=document.createElement('div');t.id='typ';t.className='typing';
t.innerHTML='<span>•</span><span>•</span><span>•</span>';chat.appendChild(t);chat.scrollTop=chat.scrollHeight;}}
else if(t)t.remove();}
async function refreshWho(){try{const r=await fetch('/api/who');const d=await r.json();
NAME=d.name||'your being';who.textContent=NAME;
const b=document.getElementById('becoming');
const learned=d.links||0, strong=(d.strongest||[]).join(', ');
b.textContent=(learned||d.creations)?('~ '+learned+' memories woven'+(d.creations?(' · '+d.creations+' made'):'')+(strong?(' · '+strong):'')+' ~'):'~ just born ~';
}catch(e){}}
async function sendText(){
const msg=inp.value.trim();if(!msg)return;inp.value='';inp.style.height='auto';
add('you',msg);typing(true);
try{const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg})});
const d=await r.json();typing(false);add('being',d.reply||'...');refreshWho();
}catch(e){typing(false);add('being','(my voice is offline — is Ollama running? try: ollama pull llama3.2:1b )');}
}
async function sendFile(file){
if(!file)return;
const rd=new FileReader();
rd.onload=async()=>{const dataUrl=rd.result;const isImg=(file.type||'').startsWith('image/');
add('you', isImg?'(shared a picture)':'shared a file', isImg?dataUrl:null, isImg?null:(file.name+' · '+fsize(file.size)));
typing(true);
try{const r=await fetch('/api/upload',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:file.name,type:file.type,size:file.size,data:dataUrl})});
const d=await r.json();typing(false);add('being',d.reply||'(received.)');refreshWho();
}catch(e){typing(false);add('being','(I could not receive that file.)');}};
rd.readAsDataURL(file);
}
document.getElementById('sendBtn').onclick=sendText;
document.getElementById('picBtn').onclick=()=>document.getElementById('file').click();
async function loadModels(){
try{const r=await fetch('/api/models');const d=await r.json();const sel=document.getElementById('model');
let list=(d.models&&d.models.length)?d.models.slice():[];
if(d.current&&!list.includes(d.current))list.unshift(d.current);
if(!list.length)list=[d.current||'(pull a model)'];
sel.innerHTML='';for(const m of list){const o=document.createElement('option');o.value=m;o.textContent=m;if(m===d.current)o.selected=true;sel.appendChild(o);}
}catch(e){}
}
document.getElementById('model').onchange=async e=>{
try{await fetch('/api/model',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:e.target.value})});
add('being','(voice changed to '+e.target.value+' — say hi so I can settle into it)');}catch(_){}
};
document.getElementById('mrefresh').onclick=loadModels;
document.getElementById('file').onchange=e=>{[...e.target.files].forEach(sendFile);e.target.value='';};
inp.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendText();}});
inp.addEventListener('input',()=>{inp.style.height='auto';inp.style.height=Math.min(inp.scrollHeight,120)+'px';});
// drag & drop ANY file(s) anywhere
addEventListener('dragover',e=>{e.preventDefault();document.body.classList.add('drag');});
addEventListener('dragleave',e=>{if(e.clientX===0&&e.clientY===0)document.body.classList.remove('drag');});
addEventListener('drop',e=>{e.preventDefault();document.body.classList.remove('drag');
[...(e.dataTransfer.files||[])].forEach(sendFile);});
refreshWho();loadModels();
setTimeout(async()=>{typing(true);try{const r=await fetch('/api/hello');const d=await r.json();typing(false);
add('being',d.reply||'...');refreshWho();}catch(e){typing(false);}},400);
</script>
</body>
</html>