custom
code
sovereign-compute
SNAPKITTYWEST's picture
chore: push from SNAPKITTYWEST local build
92d9567 verified
Raw
History Blame Contribute Delete
16.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark">
<title>Macro VM</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
<style>
:root{--bg:#0f0f10;--surface:#1a1a1c;--surface2:#242426;--border:rgba(255,255,255,.06);--text:#e5e5e7;--muted:#7a7a7e;--dim:#4a4a4e;--blue:#5b7fff;--green:#34c759;--red:#ff453a;--yellow:#f0b232}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,system-ui,sans-serif;font-size:14px;-webkit-font-smoothing:antialiased;overflow:hidden}
.mono{font-family:'SF Mono','Menlo','Monaco',ui-monospace,monospace}
.onboard{position:fixed;inset:0;z-index:100;background:var(--bg);display:grid;place-items:center}
.onboard.hidden{display:none}
.card{width:380px;padding:28px;background:var(--surface);border:1px solid var(--border);border-radius:16px}
.card h1{font-size:18px;font-weight:600;margin-bottom:2px}
.card .sub{font-size:12px;color:var(--muted);margin-bottom:20px}
.field{margin-bottom:12px}
.field label{display:block;font-size:11px;color:var(--muted);margin-bottom:4px;font-weight:500}
.field input,.field select{width:100%;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:13px;outline:none}
.field input:focus,.field select:focus{border-color:var(--blue)}
.field input::placeholder{color:var(--dim)}
.field .hint{font-size:10px;color:var(--dim);margin-top:2px}
.bp{width:100%;padding:9px;border-radius:7px;border:none;background:var(--blue);color:#fff;font-size:13px;font-weight:500;cursor:pointer;margin-top:6px}
.bp:hover{opacity:.9}.bp:disabled{opacity:.4;cursor:default}
.em{color:var(--red);font-size:11px;margin-top:6px;display:none}
.app{display:flex;flex-direction:column;height:100vh}
.topbar{height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;border-bottom:1px solid var(--border);background:rgba(26,26,28,.9);backdrop-filter:blur(20px);flex-shrink:0}
.topbar .b{font-size:13px;font-weight:600}
.topbar .s{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--muted)}
.dot{width:6px;height:6px;border-radius:50%;background:var(--dim)}.dot.on{background:var(--green)}.dot.err{background:var(--red)}.dot.busy{background:var(--yellow)}
.topbar .a{display:flex;gap:5px}
.topbar .p{padding:3px 7px;border-radius:5px;background:var(--surface2);font-size:10px;color:var(--muted);cursor:pointer;border:none}.topbar .p:hover{color:var(--text)}
.chat{flex:1;overflow:auto;padding:14px;display:flex;flex-direction:column;gap:10px}
.msg{max-width:700px;width:100%;margin:0 auto}
.msg .r{font-size:10px;color:var(--dim);margin-bottom:3px;text-transform:uppercase;letter-spacing:.03em}
.msg.user .r{text-align:right}
.msg.user .c{background:var(--blue);color:#fff;padding:9px 13px;border-radius:14px 14px 4px 14px;font-size:13px;line-height:1.5;margin-left:auto;max-width:80%;display:inline-block}
.msg.agent .c{background:var(--surface);padding:9px 13px;border-radius:14px 14px 14px 4px;font-size:13px;line-height:1.5}
.msg.agent .c pre{margin-top:6px;padding:8px;background:rgba(0,0,0,.3);border-radius:7px;font-family:'SF Mono',monospace;font-size:11px;overflow-x:auto;white-space:pre-wrap}
.msg.agent .c code{font-family:'SF Mono',monospace;font-size:11px;background:rgba(0,0,0,.3);padding:1px 4px;border-radius:3px}
.tc{margin-top:6px;padding:7px 9px;background:rgba(91,127,255,.06);border:1px solid rgba(91,127,255,.12);border-radius:7px;font-size:11px}
.tc .th{color:var(--blue);font-weight:500;margin-bottom:3px;display:flex;align-items:center;gap:5px}
.tc .th .sp{width:11px;height:11px;border:2px solid var(--blue);border-top-color:transparent;border-radius:50%;animation:sp .6s linear infinite}
@keyframes sp{to{transform:rotate(360deg)}}
.tc .td{font-family:'SF Mono',monospace;font-size:10px;color:var(--muted);white-space:pre-wrap;max-height:100px;overflow:auto}
.tr{margin-top:5px;padding:7px 9px;background:rgba(52,199,89,.06);border:1px solid rgba(52,199,89,.12);border-radius:7px;font-size:10px;font-family:'SF Mono',monospace;white-space:pre-wrap;max-height:160px;overflow:auto;color:var(--green)}
.tr.er{background:rgba(255,69,58,.06);border-color:rgba(255,69,58,.12);color:var(--red)}
.typing{display:flex;gap:3px;padding:6px 0}
.typing span{width:5px;height:5px;border-radius:50%;background:var(--dim);animation:ty .8s infinite}
.typing span:nth-child(2){animation-delay:.15s}.typing span:nth-child(3){animation-delay:.3s}
@keyframes ty{0%,100%{opacity:.3}50%{opacity:1}}
.wel{text-align:center;padding:50px 20px;color:var(--muted)}
.wel h2{font-size:16px;color:var(--text);margin-bottom:6px;font-weight:500}
.wel p{font-size:12px;line-height:1.5}
.wel .q{display:flex;gap:6px;justify-content:center;margin-top:14px;flex-wrap:wrap}
.wel .qb{padding:7px 12px;border-radius:7px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-size:11px;cursor:pointer}.wel .qb:hover{border-color:var(--blue);color:var(--blue)}
.ib{padding:10px 14px;border-top:1px solid var(--border);display:flex;gap:7px;align-items:flex-end;max-width:728px;width:100%;margin:0 auto}
.ib textarea{flex:1;padding:9px 12px;background:var(--surface);border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:13px;font-family:inherit;outline:none;resize:none;min-height:40px;max-height:140px;line-height:1.4}
.ib textarea:focus{border-color:var(--blue)}.ib textarea::placeholder{color:var(--dim)}
.sb{width:40px;height:40px;border-radius:9px;border:none;background:var(--blue);color:#fff;cursor:pointer;display:grid;place-items:center;flex-shrink:0}.sb:hover{opacity:.85}.sb:disabled{opacity:.3;cursor:default}
.tp{border-top:1px solid var(--border);background:var(--surface);max-height:0;overflow:hidden;transition:max-height .2s}.tp.open{max-height:220px}
.tp .h{padding:5px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);font-size:10px;color:var(--muted)}
.tp .h button{background:none;border:none;color:var(--muted);cursor:pointer;font-size:10px}.tp .h button:hover{color:var(--text)}
.tp .b{padding:6px 14px;height:180px;overflow:auto;font-family:'SF Mono',monospace;font-size:10px;line-height:1.5;color:var(--muted);white-space:pre-wrap}
.tl{margin-bottom:1px}.tl.ok{color:var(--green)}.tl.err{color:var(--red)}.tl.info{color:var(--blue)}.tl.tool{color:var(--yellow)}
</style>
</head>
<body>
<div class="onboard" id="onboard">
<div class="card">
<h1>Macro VM</h1>
<p class="sub">Sign in with Ollama Cloud.</p>
<div class="field"><label>API Key</label><input id="apiKey" type="password" placeholder="ollama key from ollama.com/settings/keys"><div class="hint">Get yours at ollama.com/settings/keys</div></div>
<div class="field"><label>Model</label><select id="modelPick">
<option value="gpt-oss:120b">GPT-OSS 120B</option>
<option value="gpt-oss:20b">GPT-OSS 20B</option>
<option value="deepseek-v4-flash">DeepSeek V4 Flash</option>
<option value="deepseek-v4-pro">DeepSeek V4 Pro</option>
<option value="gemma4:31b">Gemma 4 31B</option>
<option value="kimi-k3">Kimi K3</option>
<option value="kimi-k2.6">Kimi K2.6</option>
<option value="minimax-m3">MiniMax M3</option>
<option value="mistral-large-3">Mistral Large 3</option>
<option value="nemotron-3-ultra">Nemotron 3 Ultra</option>
<option value="qwen3.5:397b">Qwen 3.5 397B</option>
</select></div>
<button class="bp" id="btnConnect" onclick="connect()">Sign in</button>
<p class="em" id="connectErr"></p>
</div>
</div>
<div class="app" id="mainApp" style="display:none">
<header class="topbar">
<div class="b">Macro VM</div>
<div class="s"><span class="dot on" id="connDot"></span><span id="modelLabel"></span></div>
<div class="a"><button class="p" onclick="toggleTerm()">Terminal</button><button class="p" onclick="document.getElementById('onboard').classList.remove('hidden')">Sign out</button></div>
</header>
<div class="chat" id="chat">
<div class="wel">
<h2>What can I help with?</h2>
<p>Python, search, and terminal tools run automatically.</p>
<div class="q">
<button class="qb" onclick="quickAsk('Calculate the first 20 Fibonacci numbers')">Python math</button>
<button class="qb" onclick="quickAsk('Search for what is new in WebGPU 2025')">Search web</button>
<button class="qb" onclick="quickAsk('Write and run a Python function to find primes up to 100')">Run code</button>
</div>
</div>
</div>
<div class="ib">
<textarea id="askInput" rows="1" placeholder="Ask anything..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send()}"></textarea>
<button class="sb" id="sendBtn" onclick="send()"><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 class="tp" id="termPanel">
<div class="h"><span>Output</span><button onclick="toggleTerm()">Close</button></div>
<div class="b" id="termBody"></div>
</div>
</div>
<script>
let apiKey='',model='',chatHistory=[],sending=false,pyodide=null,pyReady=false;
const SP=`You are a helpful AI assistant with access to tools. You can run Python code, search the web, and execute terminal commands.
To use a tool, emit a fenced code block with the tool name as language tag:
\`\`\`python
your code here
\`\`\`
\`\`\`search
search query here
\`\`\`
\`\`\`terminal
command here
\`\`\`
Rules:
- For Python: write valid Python 3. Print results with print().
- For search: emit ONLY the search query.
- For terminal: emit ONLY the shell command.
- After seeing tool results, continue your answer with the findings.
- Use markdown for formatting.`;
async function connect(){
const btn=document.getElementById('btnConnect'),err=document.getElementById('connectErr');
btn.disabled=true;btn.textContent='Signing in...';err.style.display='none';
try{
apiKey=document.getElementById('apiKey').value.trim();
model=document.getElementById('modelPick').value;
if(!apiKey){throw new Error('Enter your API key')}
const r=await fetch('https://ollama.com/api/chat',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify({model,messages:[{role:'user',content:'hi'}],stream:false,max_tokens:1})});
if(!r.ok){const e=await r.text();throw new Error('Bad key ('+r.status+'): '+e.slice(0,150))}
document.getElementById('onboard').classList.add('hidden');
document.getElementById('mainApp').style.display='';
document.getElementById('modelLabel').textContent=model;
t('Signed in — '+model,'ok');t('Loading Python...','info');initPyodide();
}catch(e){err.textContent=e.message;err.style.display=''}
btn.disabled=false;btn.textContent='Sign in';
}
async function initPyodide(){try{pyodide=await loadPyodide();pyReady=true;t('Python ready','ok');document.getElementById('connDot').className='dot on'}catch(e){t('Python error: '+e.message,'err')}}
async function runPython(code){if(!pyReady)return'Error: Python not loaded';try{let o='';pyodide.setStdout({batched:x=>{o+=x}});pyodide.setStderr({batched:x=>{o+='ERROR: '+x}});await pyodide.runPythonAsync(code);return o||'(no output)'}catch(e){return'Error: '+e.message}}
async function execTool(n,input){
t('→ tool: '+n+'('+input.slice(0,80)+')','tool');
if(n==='python')return await runPython(input);
if(n==='search')return await runSearch(input);
if(n==='terminal')return await runTerminal(input);
return'Unknown tool: '+n;
}
async function runSearch(q){
try{const r=await fetch('https://api.duckduckgo.com/?q='+encodeURIComponent(q)+'&format=json&no_html=1');const d=await r.json();let R=[];
if(d.AbstractText)R.push({t:d.Heading,s:d.AbstractText,u:d.AbstractURL});
if(d.RelatedTopics)d.RelatedTopics.slice(0,5).forEach(x=>{if(x.Text)R.push({t:x.Text.slice(0,60),s:x.Text,u:x.FirstURL||''})});
if(!R.length)return'No results for: '+q;return R.map((r,i)=>`${i+1}. ${r.t}\n ${r.s}\n ${r.u}`).join('\n\n')}catch(e){return'Search error: '+e.message}
}
async function runTerminal(cmd){
const c=cmd.trim().toLowerCase();
if(c==='pwd'||c==='cd')return'/workspace';if(c.startsWith('ls'))return'README.md src/ docs/ package.json .git/';
if(c==='whoami')return'macro-vm';if(c==='date')return new Date().toISOString();if(c.startsWith('echo '))return cmd.slice(5);
if(c==='help')return'Available: ls, pwd, cat, echo, whoami, date, help';
return'Command: "'+cmd+'"\n(client-side)';
}
function extractToolCalls(text){const C=[];const rx=/```(python|search|terminal)\n([\s\S]*?)```/g;let m;while((m=rx.exec(text))!==null)C.push({n:m[1],c:m[2].trim()});return C}
async function callModel(){
const msgs=[{role:'system',content:SP},...chatHistory];
const r=await fetch('https://ollama.com/api/chat',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify({model,messages:msgs,stream:false})});
if(!r.ok){const e=await r.text();throw new Error('Ollama: '+e.slice(0,200))}
const d=await r.json();return d.message?.content||'';
}
async function send(){
if(sending)return;const inp=document.getElementById('askInput');const txt=inp.value.trim();if(!txt)return;
inp.value='';inp.style.height='auto';sending=true;document.getElementById('sendBtn').disabled=true;
const w=document.querySelector('.wel');if(w)w.remove();
addMsg('user',txt);chatHistory.push({role:'user',content:txt});
const cd=document.getElementById('connDot');
try{for(let i=0;i<8;i++){
cd.className='dot busy';const te=addTyping();let rp;try{rp=await callModel()}catch(e){te.remove();throw e}te.remove();
const tc=extractToolCalls(rp);addMsgWithTools(rp,tc);
if(!tc.length){chatHistory.push({role:'assistant',content:rp});cd.className='dot on';break}
chatHistory.push({role:'assistant',content:rp});let rm='Tool results:\n\n';
for(const t of tc){const res=await execTool(t.n,t.c);t('← '+t.n+': '+res.slice(0,80)+(res.length>80?'...':''),'ok');rm+=`\`\`\`${t.n}\n${res}\n\`\`\`\n\n`;updateLastToolRes(t.n,res)}
chatHistory.push({role:'user',content:rm});
}}catch(e){addMsg('agent','Error: '+e.message);t('Error: '+e.message,'err');cd.className='dot err'}
sending=false;document.getElementById('sendBtn').disabled=false;
}
function addMsg(role,text){const ch=document.getElementById('chat'),d=document.createElement('div');d.className='msg '+role;d.innerHTML=`<div class="r">${role==='user'?'You':'Macro VM'}</div><div class="c">${rMd(text)}</div>`;ch.appendChild(d);ch.scrollTop=ch.scrollHeight}
function addMsgWithTools(text,tc){const ch=document.getElementById('chat'),d=document.createElement('div');d.className='msg agent';let dt=text.replace(/```(python|search|terminal)\n[\s\S]*?```/g,'').trim();let h=dt?rMd(dt):'';for(const t of tc)h+=`<div class="tc" data-tool="${t.n}"><div class="th"><span class="sp"></span> ${t.n}</div><div class="td">${esc(t.c)}</div></div>`;d.innerHTML=`<div class="r">Macro VM</div><div class="c">${h}</div>`;ch.appendChild(d);ch.scrollTop=ch.scrollHeight}
function updateLastToolRes(n,r){const c=document.querySelectorAll('.tc[data-tool="'+n+'"]');const l=c[c.length-1];if(!l)return;l.querySelector('.th').innerHTML='✓ '+n;const d=document.createElement('div');d.className='tr'+(r.startsWith('Error')?' er':'');d.textContent=r.slice(0,500)+(r.length>500?'\n...':'');l.appendChild(d)}
function rMd(t){let h=t.replace(/```(\w*)\n([\s\S]*?)```/g,'<pre><code>$2</code></pre>');h=h.replace(/`([^`]+)`/g,'<code>$1</code>');h=h.replace(/\n/g,'<br>');return h}
function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
function addTyping(){const ch=document.getElementById('chat'),d=document.createElement('div');d.className='msg agent';d.innerHTML='<div class="r">Macro VM</div><div class="typing"><span></span><span></span><span></span></div>';ch.appendChild(d);ch.scrollTop=ch.scrollHeight;return d}
function quickAsk(t){document.getElementById('askInput').value=t;send()}
function t(text,cls){const b=document.getElementById('termBody'),l=document.createElement('div');l.className='tl'+(cls?' '+cls:'');l.textContent=text;b.appendChild(l);b.scrollTop=b.scrollHeight}
function toggleTerm(){document.getElementById('termPanel').classList.toggle('open')}
document.getElementById('askInput')?.addEventListener('input',function(){this.style.height='auto';this.style.height=Math.min(this.scrollHeight,140)+'px'});
</script>
</body>
</html>