File size: 16,820 Bytes
92d9567 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | <!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,'&').replace(/</g,'<').replace(/>/g,'>')}
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>
|