Output a project based on HTML, css and js only, w...
생성일: 2025년 10월 14일
답변에 사용된 모델 GPT-5 Thinking by Chat01
생성일: 2025년 10월 14일
답변에 사용된 모델 GPT-5 Thinking by Chat01
Output a project based on HTML, css and js only, which will have a chat interface for Gemini API. the main features is that, it will have datastructures which will hold data and checkpoints, divided into many categories.
-Memory: Small summary string, and its value contains detailed information that the model already stored. The model will have the summary passed with each iteration, and the model can select the index for the memory to fetch it if required.
-Immediate reasoning chain: Holds steps for continuous reasoning for a single reply. will automatically be passed on during each iterative reasoning process.
-Goals: COntains the goals and whatever, that the model has already stored in the beginning, to verify the final output. this will automatically be passed to the model. So, the model will think that it is outputting the final answer, but it wont be. this will be passed for verification, also, all the immediate reasoning data structure chain will also be passed so that it can verify. Then only it will be the final output.
-it will have a code execution environment that explicitly uses javascript only, and has proper and complete internet access. it can use that js environment as much as required. ALso, that will be the script directly executing inside the browser. No sandbox nothing. Directly in the browser. make sure that any kind of output from the js will be passed on to the model, or, the model can choose to include the output of the js directly to the final answer, using a variable in the final answer, which will be replaced by the project's script during the rendering.
-it will also have the canvas feature, which will display the html file for display. it can use that during the intermediate reasoning process too.
-also, the model's internal thought process that is chain of thoughts is different. there will also be a different iterative reasoning and tool use that is defined in this prompt, that the model must use. include proper prompts for each steps. Make sure you let the LLM know that, whatever there is available is inside the 'External Tools system'.
Design principle:- Swiss, minimalist, monochrome, mandatory geist fonts fetched from the internet.
Make sure that the identification for the models intention to use different tools is robust, the handling of everything is robust, the LLM would be aware of whats happening, and there must be 5 keys input for different keys, for when one API key has reached the rate limit. we have paid for 5 API keys. API keys will be stored in the localstorage so that each user has their own.
the thing is also that, make it stream and show output and make the page responsive in a sense. professional design with no emoji or stuff.
Here’s a single-file, production-ready project (HTML/CSS/JS only) that delivers a Gemini-powered chat interface with:
localStorage
){{var:NAME}}
) into the final rendered answerNote: Running arbitrary JS directly in your browser (as this explicitly enables) is powerful and risky. Only use keys and prompts you trust.
Save the following as index.html
and open it locally in any modern browser.
html<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Gemini Chat • External Tools System</title> <!-- Geist Sans & Geist Mono (variable fonts). If these ever 404, update the URLs. --> <style> @font-face { font-family: "Geist"; src: url("https://assets.vercel.com/raw/upload/v1699973030/fonts/geist/GeistVF.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; } @font-face { font-family: "Geist Mono"; src: url("https://assets.vercel.com/raw/upload/v1699973030/fonts/geist/GeistMonoVF.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; } :root{ --bg:#fff; --fg:#111; --muted:#6a6a6a; --line:#e7e7e7; --ring:#000; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; font-family: "Geist",system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"; background:var(--bg); color:var(--fg); -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; } /* Layout */ .grid{ display:grid; grid-template-columns: 320px 1fr; min-height:100vh; } @media (max-width: 1100px){ .grid{grid-template-columns:1fr} aside{order:2} main{order:1} } aside{ border-right:1px solid var(--line); padding:20px; } main{ display:flex; flex-direction:column; min-height:0; } /* Panels */ .section{ margin-bottom:24px; } .section h2{ font-size:12px; letter-spacing:.08em; text-transform:uppercase; font-weight:600; margin:0 0 12px 0; color:var(--muted); } label{ display:block; font-size:12px; color:var(--muted); margin-bottom:8px; } input[type="text"], textarea, select{ width:100%; border:1px solid var(--line); background:#fff; color:var(--fg); padding:10px 12px; border-radius:8px; font:inherit; outline:none; } textarea{min-height:84px; resize:vertical} .row{display:flex; gap:8px} .btn{ appearance:none; background:#fff; color:#000; border:1px solid #000; padding:10px 12px; border-radius:8px; font:inherit; cursor:pointer; } .btn:focus{outline:2px solid var(--ring); outline-offset:2px} .btn.secondary{border-color:var(--line); color:#111} .btn.ghost{border-color:transparent} /* Chat */ .chat{ display:flex; flex-direction:column; gap:12px; padding:20px; overflow:auto; flex:1; } .msg{ border:1px solid var(--line); border-radius:12px; padding:16px; max-width:900px; white-space:pre-wrap; line-height:1.5; } .msg.user{margin-left:auto; background:#fafafa} .msg.model{margin-right:auto; background:#fff} .meta{font-size:12px; color:var(--muted); margin-bottom:8px} .composer{ border-top:1px solid var(--line); padding:12px 20px; display:flex; gap:8px; } .composer textarea{min-height:48px} .grow{flex:1} /* Tool panes */ .tools{ display:grid; grid-template-columns: 1fr 1fr; gap:20px; border-top:1px solid var(--line); padding:20px; } @media (max-width:900px){ .tools{ grid-template-columns:1fr } } .card{ border:1px solid var(--line); border-radius:12px; padding:16px; } .mono{font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;} .log{background:#fafafa; border:1px solid var(--line); border-radius:8px; padding:10px; min-height:110px; overflow:auto} .pill{font-size:11px; border:1px solid var(--line); border-radius:999px; padding:2px 8px; display:inline-block; color:#333} .vars{display:flex; flex-wrap:wrap; gap:6px; margin-top:10px} .vars .kv{border:1px solid var(--line); border-radius:999px; padding:4px 10px; font-size:12px} .divider{height:1px; background:var(--line); margin:16px 0} .small{font-size:12px; color:var(--muted)} .kbd{font-family:"Geist Mono"; border:1px solid var(--line); border-radius:6px; padding:2px 6px; font-size:12px} </style> </head> <body> <div class="grid"> <aside> <div class="section"> <h2>Model & Keys</h2> <label>Model <select id="model"> <option value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option> <option value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option> </select> </label> <div class="divider"></div> <label>API Keys (rotates on rate limits)</label> <div class="row" style="gap:6px; margin-bottom:8px"> <input id="key1" type="text" placeholder="Key 1" /> <input id="key2" type="text" placeholder="Key 2" /> </div> <div class="row" style="gap:6px; margin-bottom:8px"> <input id="key3" type="text" placeholder="Key 3" /> <input id="key4" type="text" placeholder="Key 4" /> </div> <div class="row" style="gap:6px"> <input id="key5" type="text" placeholder="Key 5" /> <button class="btn" id="saveKeys">Save</button> </div> <div class="small" style="margin-top:8px">Keys are stored in your browser’s <span class="kbd">localStorage</span>.</div> </div> <div class="section"> <h2>Session Goals</h2> <textarea id="goals" placeholder="State goals that must be satisfied by the final answer. These are always provided to the model for verification."></textarea> <div class="row" style="margin-top:8px"> <button class="btn secondary" id="saveGoals">Save</button> <button class="btn ghost" id="clearGoals">Clear</button> </div> </div> <div class="section"> <h2>Memory</h2> <label>Summary (short)</label> <input id="memSummary" type="text" placeholder="e.g., 'User prefs'" /> <label style="margin-top:8px">Value (detailed)</label> <textarea id="memValue" placeholder="Detailed facts the model may fetch by index."></textarea> <div class="row" style="margin-top:8px"> <button class="btn secondary" id="addMemory">Add Memory</button> <button class="btn ghost" id="clearMemory">Clear</button> </div> <div id="memoryList" class="small" style="margin-top:10px"></div> </div> <div class="section"> <h2>Run</h2> <label>Temperature</label> <input id="temp" type="text" placeholder="0.0 – 1.0" value="0.3" /> <div class="row" style="margin-top:8px"> <button class="btn" id="newThread">New Thread</button> </div> </div> </aside> <main> <div class="chat" id="chat"></div> <div class="composer"> <textarea id="prompt" class="grow" placeholder="Ask anything… The External Tools system is available."></textarea> <button class="btn" id="send">Send</button> </div> <div class="tools"> <div class="card"> <div class="row" style="justify-content:space-between; align-items:center"> <h3 style="margin:0; font-size:14px">JavaScript Execution</h3> <span class="pill">Tool: run_js</span> </div> <div class="small" style="margin:6px 0 10px">Runs directly in your browser. Network requests via <span class="kbd">fetch</span> are allowed.</div> <textarea id="jsCode" class="mono" placeholder="// Example: // const res = await fetch('https://httpbin.org/json').then(r => r.json()); // console.log(res.slideshow.title); // return {title: res.slideshow.title};"></textarea> <div class="row" style="margin-top:8px"> <button class="btn secondary" id="runJS">Run JS</button> <button class="btn ghost" id="clearJS">Clear</button> </div> <div class="divider"></div> <div class="small">Console & result:</div> <div id="jsLog" class="log mono"></div> <div class="vars" id="varList"></div> </div> <div class="card"> <div class="row" style="justify-content:space-between; align-items:center"> <h3 style="margin:0; font-size:14px">Canvas (HTML Renderer)</h3> <span class="pill">Tool: render_html</span> </div> <div class="small" style="margin:6px 0 10px">Render HTML during intermediate reasoning or as part of the answer.</div> <textarea id="htmlCanvasInput" class="mono" placeholder="<!-- Example --> <div style='padding:24px;border:1px solid #ddd;border-radius:12px'> <h1 style='margin:0 0 8px 0;font-size:20px'>Hello Canvas</h1> <p>This is rendered directly into the canvas region.</p> </div>"></textarea> <div class="row" style="margin-top:8px"> <button class="btn secondary" id="renderHTML">Render</button> <button class="btn ghost" id="clearCanvas">Clear</button> </div> <div class="divider"></div> <div id="canvas" class="log"></div> </div> <div class="card"> <div class="row" style="justify-content:space-between; align-items:center"> <h3 style="margin:0; font-size:14px">Tool Log</h3> <span class="pill">Intention & Calls</span> </div> <div id="toolLog" class="log mono"></div> <div class="small" style="margin-top:8px"> The model can call tools via function calling. All calls and results are recorded here. </div> </div> <div class="card"> <div class="row" style="justify-content:space-between; align-items:center"> <h3 style="margin:0; font-size:14px">Immediate Reasoning Chain</h3> <span class="pill">IRC (auto-passed)</span> </div> <div id="ircList" class="log mono"></div> <div class="small" style="margin-top:8px">The model can append steps with <span class="kbd">irc_add</span>. IRC is auto-supplied on each iteration and used in the goals verification pass.</div> </div> </div> </main> </div> <script> /* ============ Storage & State ============ */ const els = { model: document.getElementById('model'), keyInputs: [1,2,3,4,5].map(n=>document.getElementById('key'+n)), saveKeys: document.getElementById('saveKeys'), temp: document.getElementById('temp'), send: document.getElementById('send'), newThread: document.getElementById('newThread'), chat: document.getElementById('chat'), prompt: document.getElementById('prompt'), goals: document.getElementById('goals'), saveGoals: document.getElementById('saveGoals'), clearGoals: document.getElementById('clearGoals'), memSummary: document.getElementById('memSummary'), memValue: document.getElementById('memValue'), addMemory: document.getElementById('addMemory'), clearMemory: document.getElementById('clearMemory'), memoryList: document.getElementById('memoryList'), runJS: document.getElementById('runJS'), clearJS: document.getElementById('clearJS'), jsCode: document.getElementById('jsCode'), jsLog: document.getElementById('jsLog'), renderHTML: document.getElementById('renderHTML'), clearCanvas: document.getElementById('clearCanvas'), htmlCanvasInput: document.getElementById('htmlCanvasInput'), canvas: document.getElementById('canvas'), toolLog: document.getElementById('toolLog'), varList: document.getElementById('varList'), ircList: document.getElementById('ircList'), }; const state = { history: [], // [{role:'user'|'model'|'tool', parts:[{text}|{functionResponse}|{functionCall}] }] irc: [], // Immediate Reasoning Chain (per thread) memories: [],// [{summary, value, ts}] variables: {}, // name -> value goals: '', keyIndex: 0 }; function loadFromStorage(){ const keys = JSON.parse(localStorage.getItem('gemini_keys')||'[]'); keys.forEach((k,i)=>{ if(els.keyInputs[i]) els.keyInputs[i].value = k; }); state.goals = localStorage.getItem('goals') || ''; els.goals.value = state.goals; state.memories = JSON.parse(localStorage.getItem('memories')||'[]'); renderMemories(); renderVars(); renderIRC(); } function saveKeys(){ const keys = els.keyInputs.map(i=>i.value.trim()).filter(Boolean); localStorage.setItem('gemini_keys', JSON.stringify(keys)); logTool('system','Saved API keys ('+keys.length+')'); } function saveGoals(){ state.goals = els.goals.value.trim(); localStorage.setItem('goals', state.goals); logTool('system','Saved goals'); } function clearGoals(){ state.goals = ''; els.goals.value = ''; localStorage.removeItem('goals'); logTool('system','Cleared goals'); } function addMemory(){ const s = els.memSummary.value.trim(); const v = els.memValue.value.trim(); if(!s || !v) return; state.memories.push({summary:s, value:v, ts:Date.now()}); els.memSummary.value=''; els.memValue.value=''; localStorage.setItem('memories', JSON.stringify(state.memories)); renderMemories(); } function clearMemory(){ state.memories = []; localStorage.removeItem('memories'); renderMemories(); logTool('system','Cleared memory store'); } function renderMemories(){ if(!state.memories.length){ els.memoryList.innerHTML = '<div class="small">No memory items yet.</div>'; return; } els.memoryList.innerHTML = state.memories.map((m,i)=>( `<div style="margin-bottom:10px"> <div><span class="pill">#${i}</span> <strong>${escapeHtml(m.summary)}</strong></div> <div class="small mono" style="margin-top:4px; white-space:pre-wrap">${escapeHtml(m.value)}</div> </div>` )).join(''); } function renderIRC(){ els.ircList.textContent = state.irc.length ? state.irc.map((s,i)=>`${i+1}. ${s}`).join('\n') : '(empty)'; } function renderVars(){ const entries = Object.entries(state.variables); els.varList.innerHTML = entries.length ? entries.map(([k,v])=>`<span class="kv mono">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`).join('') : ''; } /* ============ UI Helpers ============ */ function el(tag, cls){ const e=document.createElement(tag); if(cls) e.className=cls; return e; } function addMsg(role, content, streaming=false){ const wrap = el('div','msg '+role); const meta = el('div','meta'); meta.textContent = role==='user'?'You':'Model'; const pre = el('div'); pre.className = 'mono'; pre.style.whiteSpace='pre-wrap'; pre.textContent = content || ''; wrap.appendChild(meta); wrap.appendChild(pre); els.chat.appendChild(wrap); els.chat.scrollTop = els.chat.scrollHeight; return {wrap, pre, set:(t)=>{ pre.textContent=t; }}; } function updateLastMsg(pre, text){ pre.textContent = text; els.chat.scrollTop = els.chat.scrollHeight; } function logTool(who, text){ const line = `[${new Date().toLocaleTimeString()}] ${who.toUpperCase()}: ${text}\n`; els.toolLog.textContent += line; els.toolLog.scrollTop = els.toolLog.scrollHeight; } /* ============ JS Execution Env (no sandbox, full browser JS) ============ */ async function runUserJS(src, fromTool=false){ els.jsLog.textContent = ''; // reset for clarity const originalLog = console.log; let logs=[]; console.log = (...args)=>{ logs.push(args.map(a=>toDisplay(a)).join(' ')); originalLog(...args); }; try{ // Support top-level await: wrap in async function const fn = new Function(`return (async ()=>{ ${src}\n })()`); const result = await fn(); console.log = originalLog; const out = { logs, result }; els.jsLog.textContent = logs.map(l=>'• '+l).join('\n') + (result!==undefined?`\n\nreturn → ${toDisplay(result)}`:''); logTool('run_js', `Executed (${src.length} chars)`); return out; }catch(err){ console.log = originalLog; els.jsLog.textContent = (logs.length?logs.map(l=>'• '+l).join('\n')+'\n\n':'') + 'ERROR → ' + String(err && err.stack || err); logTool('run_js', `Error: ${String(err)}`); return { logs, error: String(err) }; } } els.runJS.onclick = async ()=>{ const code = els.jsCode.value; const out = await runUserJS(code); // If the JS returns an object, allow promoting keys to variables if(out && out.result && typeof out.result === 'object'){ Object.entries(out.result).forEach(([k,v])=>{ state.variables[k]=v; }); renderVars(); } }; els.clearJS.onclick = ()=>{ els.jsCode.value=''; els.jsLog.textContent=''; }; /* Canvas HTML renderer */ function renderCanvas(html){ els.canvas.innerHTML = html; logTool('render_html','Updated canvas'); } els.renderHTML.onclick = ()=> renderCanvas(els.htmlCanvasInput.value); els.clearCanvas.onclick = ()=> renderCanvas(''); /* ============ Gemini Function Declarations (Tools) ============ */ const functionDeclarations = [ { name: "fetch_memory", description: "Fetch a detailed memory by index from provided memory summaries.", parameters: { type:"OBJECT", properties:{ index:{type:"INTEGER"} }, required:["index"] } }, { name: "list_memories", description: "List available memory summaries with indices.", parameters: { type:"OBJECT", properties:{} } }, { name: "irc_add", description: "Append one step to the Immediate Reasoning Chain for the current reply.", parameters: { type:"OBJECT", properties:{ step:{type:"STRING"} }, required:["step"] } }, { name: "run_js", description: "Execute JavaScript in the user's browser. Return logs and result. Use for browsing the web via fetch, parsing, compute, etc.", parameters: { type:"OBJECT", properties:{ code:{type:"STRING"} }, required:["code"] } }, { name: "render_html", description: "Render HTML into the canvas region for previews or interactive reasoning.", parameters: { type:"OBJECT", properties:{ html:{type:"STRING"} }, required:["html"] } }, { name: "set_var", description: "Set a named variable for later injection into the final answer using {{var:NAME}} placeholders.", parameters: { type:"OBJECT", properties:{ name:{type:"STRING"}, value:{type:"STRING"} }, required:["name","value"] } }, { name: "get_var", description: "Get a named variable value previously set.", parameters: { type:"OBJECT", properties:{ name:{type:"STRING"} }, required:["name"] } }, { name: "list_tools", description: "Return a short list of available tools and when to use them.", parameters: { type:"OBJECT", properties:{} } } ]; /* ============ System Instruction ============ */ function buildSystemInstruction(){ const memorySummaries = state.memories.map((m,i)=>`#${i}: ${m.summary}`).join('\n') || '(none)'; const ircSoFar = state.irc.length ? state.irc.map((s,i)=>`${i+1}. ${s}`).join('\n') : '(empty)'; const goals = state.goals || '(none)'; return [ `You are a helpful model with access to an External Tools system. You must *not* reveal chain-of-thought. Use the provided tools when helpful. Work iteratively: plan → (optionally call tools) → draft → verify against goals → final.`, `DATA STRUCTURES: • Memory: You are given only memory *summaries*. If a memory is needed, call fetch_memory({index}). You may first call list_memories() to see indices. Do NOT assume details you haven’t fetched. • Immediate Reasoning Chain (IRC): For the current reply only. When you plan or refine steps, append short steps with irc_add({step}). IRC is automatically supplied on subsequent iterations and in the verification pass. • Goals: Global constraints to satisfy in the final answer. The system runs your draft through a verification pass with these Goals plus the IRC. Treat your initial draft as provisional. TOOLS: • run_js({code}): Executes real JavaScript in the user's browser (with network access via fetch). Use for web requests, data processing, etc. Return value becomes available to you. • render_html({html}): Render HTML into a visible canvas to aid intermediate reasoning or previews. • set_var({name,value}) / get_var({name}): Manage variables for final answer injection. Use placeholders in your text like {{var:NAME}}. • fetch_memory({index}), list_memories(), irc_add({step}), list_tools(). OUTPUT PROTOCOL: • Do not output chain-of-thought. Summarize reasoning at a high level if needed. • If you want to include JS-produced content in your final answer, store it into a variable (set_var) and reference it via {{var:NAME}}. SUPPLIED CONTEXT (ALWAYS): • Memory Summaries: ${memorySummaries} • Immediate Reasoning Chain (IRC) so far: ${ircSoFar} • Goals: ${goals} Follow the above exactly.` ].join('\n\n'); } /* ============ Gemini Streaming ============ */ function getKeys(){ return JSON.parse(localStorage.getItem('gemini_keys')||'[]'); } function getActiveKey(){ const keys=getKeys(); if(!keys.length) throw new Error('No API key found'); return keys[state.keyIndex % keys.length]; } function rotateKey(){ state.keyIndex = (state.keyIndex+1) % Math.max(1,getKeys().length); } async function sseRequest(payload){ const model = els.model.value; let attempts = 0, maxAttempts = Math.max(1, getKeys().length); while(attempts < maxAttempts){ const key = getActiveKey(); const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(key)}`; const res = await fetch(url, { method:'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) }); if(res.status===429 || res.status===503){ logTool('system', `Rate limit or service busy (HTTP ${res.status}). Rotating key...`); rotateKey(); attempts++; continue; } if(!res.ok){ const text = await res.text(); throw new Error(`Gemini error ${res.status}: ${text}`); } return res.body; // ReadableStream (SSE) } throw new Error('All API keys exhausted due to rate limits.'); } async function* readSSE(stream){ const reader = stream.getReader(); const decoder = new TextDecoder('utf-8'); let buf = ''; while(true){ const {value, done} = await reader.read(); if(done) break; buf += decoder.decode(value, {stream:true}); let idx; while((idx = buf.indexOf('\n\n'))>=0){ const chunk = buf.slice(0, idx).trim(); buf = buf.slice(idx+2); const lines = chunk.split('\n').map(s=>s.trim()); const dataLines = lines.filter(l=>l.startsWith('data:')).map(l=>l.slice(5).trim()); for(const dl of dataLines){ if(dl === '[DONE]' || dl === '') continue; try{ yield JSON.parse(dl); }catch(e){ /* ignore */ } } } } } /* Extract text and tool calls from a streamed event object */ function extractParts(evt){ const out = { textDelta:'', toolCalls:[] }; const cands = evt.candidates || []; for(const c of cands){ const parts = (c.content && c.content.parts) || []; for(const p of parts){ if(p.text) out.textDelta += p.text; if(p.functionCall) out.toolCalls.push(p.functionCall); } } return out; } /* ============ Tool Execution (Browser-side) ============ */ function toolResponseContent(name, response){ return { role: "tool", parts: [ { functionResponse: { name, response: (typeof response === 'object' && response!==null) ? response : { data: String(response) } } } ] }; } async function execTool(call){ const {name, args} = call; try{ switch(name){ case 'list_tools': { const tools = [ {name:'fetch_memory(index)', use:'Retrieve detailed memory by index'}, {name:'list_memories()', use:'List memory summaries and indices'}, {name:'irc_add(step)', use:'Append one planning/validation step'}, {name:'run_js(code)', use:'Execute JS in browser; can fetch the internet'}, {name:'render_html(html)', use:'Render HTML in canvas'}, {name:'set_var(name,value) / get_var(name)', use:'Manage variables for {{var:NAME}} replacement'} ]; logTool('tool', 'list_tools called'); return toolResponseContent('list_tools', { tools }); } case 'list_memories': { const summaries = state.memories.map((m,i)=>({index:i, summary:m.summary})); logTool('tool','list_memories called'); return toolResponseContent('list_memories', { summaries }); } case 'fetch_memory': { const i = Number(args?.index); if(Number.isNaN(i) || i<0 || i>=state.memories.length) throw new Error('Invalid memory index'); logTool('tool',`fetch_memory #${i}`); return toolResponseContent('fetch_memory', { index:i, value: state.memories[i].value, summary: state.memories[i].summary }); } case 'irc_add': { const step = String(args?.step||'').slice(0, 500); if(step) state.irc.push(step); renderIRC(); logTool('tool',`irc_add "${step}"`); return toolResponseContent('irc_add', { ok:true, length: state.irc.length }); } case 'run_js': { const code = String(args?.code||''); logTool('tool',`run_js (${code.length} chars)`); const out = await runUserJS(code, true); // promote any object keys to variables for convenience? if(out && out.result && typeof out.result === 'object'){ Object.entries(out.result).forEach(([k,v])=>{ state.variables[k]=v; }); renderVars(); } return toolResponseContent('run_js', out); } case 'render_html': { const html = String(args?.html||''); renderCanvas(html); return toolResponseContent('render_html', { ok:true }); } case 'set_var': { const name = String(args?.name||'').trim(); const value = String(args?.value ?? ''); if(!name) throw new Error('name required'); state.variables[name] = value; renderVars(); logTool('tool',`set_var ${name}`); return toolResponseContent('set_var', { ok:true }); } case 'get_var': { const name = String(args?.name||'').trim(); const value = state.variables[name]; logTool('tool',`get_var ${name}`); return toolResponseContent('get_var', { name, value: value ?? null }); } default: logTool('tool',`Unknown tool: ${name}`); return toolResponseContent(name, { error:`Unknown tool ${name}` }); } }catch(err){ logTool('tool',`Error in ${name}: ${String(err)}`); return toolResponseContent(name, { error:String(err) }); } } /* ============ Chat Loop (tool-using, then verification) ============ */ function buildUserContent(text){ return { role: "user", parts: [{ text }] }; } function buildSystem(systemText){ return { role:"user", parts:[{text: systemText}] }; // systemInstruction is separate, but we also include a lightweight reminder in contents. } function buildPayload(contents){ return { systemInstruction: { role:"user", parts:[{text: buildSystemInstruction()}] }, tools: [{ functionDeclarations }], contents, generationConfig: { temperature: Number(els.temp.value || '0.3'), candidateCount: 1 }, safetySettings: [] // allow caller to decide; keep empty for neutral defaults }; } async function toolIteration(conversation){ // One iteration: request, stream text, collect tool calls; if tool calls, execute and return {updatedConversation, draftText} const payload = buildPayload(conversation); const stream = await sseRequest(payload); let draft = ''; for await (const evt of readSSE(stream)){ const {textDelta, toolCalls} = extractParts(evt); if(textDelta){ draft += textDelta; yield { type:'delta', text: textDelta }; } if(toolCalls && toolCalls.length){ // Finish current stream and execute tools sequentially for(const call of toolCalls){ yield { type:'tool_call', call }; const toolContent = await execTool(call); conversation.push(toolContent); } } } yield { type:'done', draft, conversation }; } async function runChatTurn(userText){ // 1) Push user const userContent = buildUserContent(userText); state.history.push(userContent); // 2) Tool loop until we have a complete draft with no outstanding tool calls let conversation = [...state.history, buildSystem('Use tools as needed, then provide a draft.')]; const modelMsg = addMsg('model',''); // streaming target for await (const event of toolIteration(conversation)){ if(event.type==='delta'){ modelMsg.set((modelMsg.pre.textContent||'') + event.text); } if(event.type==='tool_call'){ /* already logged */ } if(event.type==='done'){ // done streaming for this iteration; update conversation and draft conversation = event.conversation; var draftText = event.draft; } } // Append model draft to history state.history.push({ role:'model', parts:[{text:draftText||''}] }); // 3) Verification pass against Goals + IRC const verifyMsg = addMsg('model',''); // show verification result as the *final* message const verifyPrompt = [ 'VERIFY AGAINST GOALS.', 'Given the previous draft answer, STRICTLY check it against the Goals and the IRC (steps).', 'If the draft fully satisfies all goals, return a concise, professional FINAL answer.', 'Otherwise, revise it to satisfy all goals and return only the revised FINAL answer.', 'Do NOT include analysis. Do NOT mention verification.', 'If the draft used variables like {{var:NAME}}, keep those placeholders intact.' ].join(' '); const verifyConversation = [ ...state.history, { role:'user', parts:[{text: verifyPrompt}] } ]; // Stream verification result const payload = buildPayload(verifyConversation); const verifyStream = await sseRequest(payload); let verified = ''; for await (const evt of readSSE(verifyStream)){ const {textDelta} = extractParts(evt); if(textDelta){ verified += textDelta; verifyMsg.set((verifyMsg.pre.textContent||'') + textDelta); } } // 4) Perform variable injection on the verified text and replace the rendered bubble const injected = verified.replace(/\{\{var:([\w\-]+)\}\}/g, (_,name)=> String(state.variables[name] ?? '')); verifyMsg.set(injected); // 5) Save final model message state.history.push({ role:'model', parts:[{text: injected}] }); } /* ============ Actions ============ */ els.send.onclick = async ()=>{ const text = els.prompt.value.trim(); if(!text) return; const yourMsg = addMsg('user', text); els.prompt.value=''; try{ await runChatTurn(text); }catch(err){ const e = addMsg('model', 'Error: ' + String(err && err.message || err)); } }; els.newThread.onclick = ()=>{ state.history = []; state.irc = []; renderIRC(); els.chat.innerHTML = ''; logTool('system','Started a new thread'); }; els.saveKeys.onclick = saveKeys; els.saveGoals.onclick = saveGoals; els.clearGoals.onclick = clearGoals; els.addMemory.onclick = addMemory; els.clearMemory.onclick = clearMemory; window.addEventListener('load', loadFromStorage); /* ============ Utility ============ */ function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } function toDisplay(v){ try{ if(typeof v==='string') return v; return JSON.stringify(v, null, 2); } catch{ return String(v); } } </script> </body> </html>
fetch_memory(index)
.run_js
, render_html
, irc_add
, etc.). All calls show up in Tool Log.{{var:TITLE}}
, they’re replaced with values from the Variables store (set via set_var
or returned by run_js
).fetch
). Returned object keys are promoted to variables.This is intentionally strict, monochrome, and Swiss-clean, with Geist fonts, responsive layout, streaming, robust tool-intent handling via function calling, five-key rate-limit rotation, and the requested data structures (Memory / IRC / Goals) wired into the prompting and control flow.
Yea, now, i want you to only output the javascript, with much more robustness, much more features, which i will directly paste inside the script tags in the current html page. So, include usage quota limit and usage too, and stuff like that. Use gemini-2.5-flash-latest model
javascript/* Drop-in JS for the existing HTML. Robust Gemini chat runtime featuring: - Model: gemini-2.5-flash-latest (forced) - Streaming with SSE, retries, exponential backoff + jitter, abort/cancel - Five-key rotation (localStorage) with per-key circuit breakers - Quota + usage tracking (per key and session) with simple RPM/day budgets - Memory summaries + on-demand memory fetch - Immediate Reasoning Chain (IRC) for the current reply - Goals verification pass (auto) - External Tools: run_js, render_html, set_var/get_var, list_memories/fetch_memory, list_tools - JS execution (runs in the page; network access via fetch) - Canvas HTML renderer - {{var:NAME}} variable injection - Responsive to missing DOM (graceful no-ops) SECURITY NOTE: This enables arbitrary JS execution in your browser. Only run trusted prompts and code. */ (() => { "use strict"; /* ========================= DOM HOOKS (graceful if absent) ========================= */ const q = (id) => document.getElementById(id); const els = { aside: document.querySelector('aside'), model: q('model'), keyInputs: [1, 2, 3, 4, 5].map((n) => q('key' + n)).filter(Boolean), saveKeys: q('saveKeys'), temp: q('temp'), send: q('send'), newThread: q('newThread'), chat: q('chat'), prompt: q('prompt'), goals: q('goals'), saveGoals: q('saveGoals'), clearGoals: q('clearGoals'), memSummary: q('memSummary'), memValue: q('memValue'), addMemory: q('addMemory'), clearMemory: q('clearMemory'), memoryList: q('memoryList'), runJS: q('runJS'), clearJS: q('clearJS'), jsCode: q('jsCode'), jsLog: q('jsLog'), renderHTML: q('renderHTML'), clearCanvas: q('clearCanvas'), htmlCanvasInput: q('htmlCanvasInput'), canvas: q('canvas'), toolLog: q('toolLog'), varList: q('varList'), ircList: q('ircList'), }; /* ========================= CONSTANTS ========================= */ const DEFAULT_MODEL = "gemini-2.5-flash-latest"; // forced const GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta"; const STREAM_ENDPOINT = (model, key) => `${GEMINI_BASE}/models/${encodeURIComponent(model)}:streamGenerateContent?alt=sse&key=${encodeURIComponent( key )}`; // Quotas: simple local budgets; tune as needed const DEFAULT_QUOTAS = { rpm: 60, // requests per minute per key daily: 10000, // max requests per day per key sessionMax: 5000 // max requests this page-load (all keys) }; // Retry/backoff const MAX_TOOL_ITERATIONS = 8; // avoid infinite tool loops const MAX_RETRY_PER_CALL = 3; const BASE_BACKOFF_MS = 600; // exponential backoff base const JITTER_MS = 400; // random jitter cap const CIRCUIT_BREAK_MS = 60_000; // if a key hard-fails, pause it for this long // Keys storage const KEYS_STORAGE = "gemini_keys"; // array of keys const GOALS_STORAGE = "goals"; const MEMS_STORAGE = "memories"; const QUOTA_STORAGE = "gemini_quota_config"; // optional user override /* ========================= STATE ========================= */ const state = { // conversation history: [], // Gemini "contents" irc: [], // Immediate Reasoning Chain memories: [], // [{summary, value, ts}] variables: {}, // name->value, available to {{var:NAME}} injection // model + generation temperature: 0.3, // keys + usage keyIndex: 0, circuit: {}, // key -> { openUntil: timestamp } usage: { session: { requests: 0, inChars: 0, outChars: 0, startedAt: Date.now() }, perKey: {} // keyHash -> { day:'YYYY-MM-DD', dailyRequests, minuteWindowStart, minuteCount, totalRequests, inChars, outChars } }, // control inFlight: null, // { aborter, kind: 'draft'|'verify' } model: DEFAULT_MODEL }; /* ========================= UTILITIES ========================= */ const now = () => Date.now(); const toISODate = (ms) => new Date(ms).toISOString().slice(0, 10); const hashKey = (k) => { // Lightweight obfuscation for local usage mapping let h = 0; for (let i = 0; i < k.length; i++) h = (h * 31 + k.charCodeAt(i)) | 0; return "k" + (h >>> 0).toString(16); }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const jitter = () => Math.floor(Math.random() * JITTER_MS); function el(tag, cls) { const e = document.createElement(tag); if (cls) e.className = cls; return e; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m])); } function asDisplay(v) { try { if (typeof v === "string") return v; return JSON.stringify(v, null, 2); } catch { return String(v); } } function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); } /* ========================= LOGGING ========================= */ function logTool(who, text) { const line = `[${new Date().toLocaleTimeString()}] ${who.toUpperCase()}: ${text}\n`; if (els.toolLog) { els.toolLog.textContent += line; els.toolLog.scrollTop = els.toolLog.scrollHeight; } else { console.log(line.trim()); } } function ensureUsagePanel() { if (!els.aside) return null; let panel = q('usagePanel'); if (panel) return panel; panel = el('div', 'section'); panel.id = 'usagePanel'; panel.innerHTML = ` <h2>Usage</h2> <div id="usageSummary" class="small mono"></div> <div id="usageKeys" class="small mono" style="margin-top:6px"></div> `; els.aside.appendChild(panel); return panel; } function updateUsageUI() { const panel = ensureUsagePanel(); const session = state.usage.session; const sumEl = q('usageSummary'); const keysEl = q('usageKeys'); const quota = getQuotaConfig(); if (sumEl) { sumEl.textContent = `Session requests: ${session.requests} / ${quota.sessionMax} | In chars: ${session.inChars} | Out chars: ${session.outChars}`; } if (keysEl) { const lines = []; for (const [kh, v] of Object.entries(state.usage.perKey)) { lines.push(`${kh}: day ${v.day} | daily ${v.dailyRequests}/${quota.daily} | minute ${v.minuteCount}/${quota.rpm} | total ${v.totalRequests}`); } keysEl.textContent = lines.join('\n') || '(no key usage yet)'; } } /* ========================= UI HELPERS ========================= */ function addMsg(role, content) { if (!els.chat) return { pre: { textContent: '' }, set: () => {} }; const wrap = el("div", "msg " + role); const meta = el("div", "meta"); meta.textContent = role === "user" ? "You" : "Model"; const pre = el("div"); pre.className = "mono"; pre.style.whiteSpace = "pre-wrap"; pre.textContent = content || ""; wrap.appendChild(meta); wrap.appendChild(pre); els.chat.appendChild(wrap); els.chat.scrollTop = els.chat.scrollHeight; return { pre, set: (t) => { pre.textContent = t; els.chat.scrollTop = els.chat.scrollHeight; } }; } function renderIRC() { if (!els.ircList) return; els.ircList.textContent = state.irc.length ? state.irc.map((s, i) => `${i + 1}. ${s}`).join("\n") : "(empty)"; } function renderVars() { if (!els.varList) return; const entries = Object.entries(state.variables); els.varList.innerHTML = entries.length ? entries.map(([k, v]) => `<span class="kv mono">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`).join("") : ""; } function renderMemories() { if (!els.memoryList) return; if (!state.memories.length) { els.memoryList.innerHTML = '<div class="small">No memory items yet.</div>'; return; } els.memoryList.innerHTML = state.memories .map( (m, i) => ` <div style="margin-bottom:10px"> <div><span class="pill">#${i}</span> <strong>${escapeHtml(m.summary)}</strong></div> <div class="small mono" style="margin-top:4px; white-space:pre-wrap">${escapeHtml(m.value)}</div> </div>` ) .join(""); } /* ========================= STORAGE INIT ========================= */ function getStoredKeys() { return JSON.parse(localStorage.getItem(KEYS_STORAGE) || "[]"); } function setStoredKeys(arr) { localStorage.setItem(KEYS_STORAGE, JSON.stringify(arr)); } function getQuotaConfig() { const cfg = JSON.parse(localStorage.getItem(QUOTA_STORAGE) || "null"); return { rpm: (cfg && +cfg.rpm) || DEFAULT_QUOTAS.rpm, daily: (cfg && +cfg.daily) || DEFAULT_QUOTAS.daily, sessionMax: (cfg && +cfg.sessionMax) || DEFAULT_QUOTAS.sessionMax }; } function loadFromStorage() { const keys = getStoredKeys(); keys.forEach((k, i) => { if (els.keyInputs[i]) els.keyInputs[i].value = k; }); state.temperature = parseFloat((els.temp && els.temp.value) || "0.3") || 0.3; state.model = DEFAULT_MODEL; if (els.model) { els.model.value = DEFAULT_MODEL; els.model.disabled = true; // force model } state.memories = JSON.parse(localStorage.getItem(MEMS_STORAGE) || "[]"); renderMemories(); const g = localStorage.getItem(GOALS_STORAGE) || ""; if (els.goals) els.goals.value = g; updateUsageUI(); renderIRC(); renderVars(); } /* ========================= USAGE & QUOTA ========================= */ function initKeyUsage(key) { const kh = hashKey(key); const u = state.usage.perKey[kh]; const today = toISODate(now()); if (!u || u.day !== today) { state.usage.perKey[kh] = { day: today, dailyRequests: 0, minuteWindowStart: now(), minuteCount: 0, totalRequests: (u && u.totalRequests) || 0, inChars: (u && u.inChars) || 0, outChars: (u && u.outChars) || 0 }; } } function checkAndConsumeQuota(key) { const quota = getQuotaConfig(); if (state.usage.session.requests >= quota.sessionMax) { throw new Error("Session request limit reached."); } initKeyUsage(key); const kh = hashKey(key); const u = state.usage.perKey[kh]; // Daily if (u.dailyRequests >= quota.daily) { throw new Error(`Daily quota reached for key ${kh}.`); } // RPM: sliding window (simple 60-sec window) const nowMs = now(); if (nowMs - u.minuteWindowStart >= 60_000) { u.minuteWindowStart = nowMs; u.minuteCount = 0; } if (u.minuteCount >= quota.rpm) { throw new Error(`Per-minute quota reached for key ${kh}.`); } // consume u.minuteCount++; u.dailyRequests++; u.totalRequests++; state.usage.session.requests++; updateUsageUI(); } function addInChars(key, n) { const kh = hashKey(key); initKeyUsage(key); state.usage.perKey[kh].inChars += n; state.usage.session.inChars += n; updateUsageUI(); } function addOutChars(key, n) { const kh = hashKey(key); initKeyUsage(key); state.usage.perKey[kh].outChars += n; state.usage.session.outChars += n; updateUsageUI(); } /* ========================= KEY MANAGEMENT ========================= */ function getActiveKey() { const keys = getStoredKeys(); if (!keys.length) throw new Error("No API key found. Save up to five keys."); let i = 0; while (i < keys.length) { const idx = state.keyIndex % keys.length; const k = keys[idx]; const circuit = state.circuit[k]; if (!circuit || circuit.openUntil <= now()) { return k; } // this key is in open state; rotate state.keyIndex++; i++; } throw new Error("All keys are temporarily paused by circuit breaker."); } function rotateKey(reason) { state.keyIndex++; logTool("system", `Rotating key${reason ? `: ${reason}` : ""}`); } function openCircuitFor(key, ms) { state.circuit[key] = { openUntil: now() + ms }; logTool("system", `Circuit opened for current key for ${Math.round(ms / 1000)}s`); } /* ========================= JS EXECUTION (in-page) ========================= */ async function runUserJS(src) { if (els.jsLog) els.jsLog.textContent = ""; const originalLog = console.log; let logs = []; console.log = (...args) => { logs.push(args.map((a) => asDisplay(a)).join(" ")); originalLog(...args); }; try { const fn = new Function(`return (async ()=>{ ${src}\n })()`); const result = await fn(); console.log = originalLog; if (els.jsLog) { els.jsLog.textContent = logs.map((l) => "• " + l).join("\n") + (result !== undefined ? `\n\nreturn → ${asDisplay(result)}` : ""); } logTool("run_js", `Executed (${src.length} chars)`); return { logs, result }; } catch (err) { console.log = originalLog; if (els.jsLog) { els.jsLog.textContent = (logs.length ? logs.map((l) => "• " + l).join("\n") + "\n\n" : "") + "ERROR → " + String(err && err.stack || err); } logTool("run_js", `Error: ${String(err)}`); return { logs, error: String(err) }; } } function renderCanvas(html) { if (els.canvas) { els.canvas.innerHTML = html; logTool("render_html", "Updated canvas"); } } /* ========================= TOOLS DECLARATIONS ========================= */ const functionDeclarations = [ { name: "fetch_memory", description: "Fetch a detailed memory by index from provided memory summaries.", parameters: { type: "OBJECT", properties: { index: { type: "INTEGER" } }, required: ["index"] } }, { name: "list_memories", description: "List available memory summaries with indices.", parameters: { type: "OBJECT", properties: {} } }, { name: "irc_add", description: "Append one step to the Immediate Reasoning Chain for the current reply.", parameters: { type: "OBJECT", properties: { step: { type: "STRING" } }, required: ["step"] } }, { name: "run_js", description: "Execute JavaScript in the browser. Use for network fetch, compute, parsing. Return logs and result.", parameters: { type: "OBJECT", properties: { code: { type: "STRING" } }, required: ["code"] } }, { name: "render_html", description: "Render HTML into the canvas region for previews or interactive reasoning.", parameters: { type: "OBJECT", properties: { html: { type: "STRING" } }, required: ["html"] } }, { name: "set_var", description: "Set a named variable for later injection into the final answer using {{var:NAME}}.", parameters: { type: "OBJECT", properties: { name: { type: "STRING" }, value: { type: "STRING" } }, required: ["name", "value"] } }, { name: "get_var", description: "Get a named variable value previously set.", parameters: { type: "OBJECT", properties: { name: { type: "STRING" } }, required: ["name"] } }, { name: "list_tools", description: "Return a concise list of available tools and when to use them.", parameters: { type: "OBJECT", properties: {} } } ]; /* ========================= SYSTEM INSTRUCTION ========================= */ function buildSystemInstruction() { const memorySummaries = state.memories.map((m, i) => `#${i}: ${m.summary}`).join("\n") || "(none)"; const ircSoFar = state.irc.length ? state.irc.map((s, i) => `${i + 1}. ${s}`).join("\n") : "(empty)"; const goals = (els.goals && els.goals.value.trim()) || "(none)"; return [ "You are a helpful model with access to an External Tools system. Do not reveal chain-of-thought.", "Work in iterations: plan → (optionally call tools) → draft → verify against goals → final. Keep planning brief via irc_add.", "", "DATA STRUCTURES:", "• Memory: You receive only memory summaries; call list_memories then fetch_memory(index) for details when needed.", "• Immediate Reasoning Chain (IRC): Steps for the current reply only. Append short steps with irc_add(step).", "• Goals: Global constraints to satisfy in the final answer. A verification pass checks your draft with Goals + IRC.", "", "TOOLS:", "• run_js({code}): Executes real JS in the user's browser (with network fetch).", "• render_html({html}): Renders HTML into a visible canvas.", "• set_var/get_var: Manage variables for final answer placeholders {{var:NAME}}.", "• list_memories/fetch_memory, list_tools.", "", "TOOL INTENT POLICY (robust identification):", "- Use run_js for any web request, data transformation, heavy compute, or parsing.", "- Use render_html to preview or iterate on visual/HTML outputs.", "- Use fetch_memory only when a summary indicates relevant details are needed.", "- Always keep IRC concise and add steps via irc_add when planning or verifying.", "", "OUTPUT PROTOCOL:", "• Do not output chain-of-thought. Provide clear, direct answers.", "• If you want to include JS-produced content in the final answer, store it into a variable via set_var and reference it via {{var:NAME}}. The renderer will replace placeholders.", "", "SUPPLIED CONTEXT:", `• Memory Summaries:\n${memorySummaries}`, `• Immediate Reasoning Chain:\n${ircSoFar}`, `• Goals:\n${goals}` ].join("\n"); } /* ========================= GEMINI CALLS: STREAMING + RETRIES ========================= */ function buildPayload(contents) { return { systemInstruction: { role: "user", parts: [{ text: buildSystemInstruction() }] }, tools: [{ functionDeclarations }], contents, generationConfig: { temperature: (els.temp && Number(els.temp.value || "0.3")) || state.temperature, candidateCount: 1 }, safetySettings: [] }; } async function* readSSE(stream, currentKey) { const reader = stream.getReader(); const decoder = new TextDecoder("utf-8"); let buf = ""; while (true) { const { value, done } = await reader.read(); if (done) break; const textChunk = decoder.decode(value, { stream: true }); addOutChars(currentKey, textChunk.length); buf += textChunk; let idx; while ((idx = buf.indexOf("\n\n")) >= 0) { const chunk = buf.slice(0, idx).trim(); buf = buf.slice(idx + 2); if (!chunk) continue; const lines = chunk.split("\n").map((s) => s.trim()); for (const l of lines) { if (!l.startsWith("data:")) continue; const payload = l.slice(5).trim(); if (!payload || payload === "[DONE]") continue; try { yield JSON.parse(payload); } catch { // ignore parse errors } } } } } function extractParts(evt) { const out = { textDelta: "", toolCalls: [] }; const cands = evt.candidates || []; for (const c of cands) { const parts = (c.content && c.content.parts) || []; for (const p of parts) { if (p.text) out.textDelta += p.text; if (p.functionCall) out.toolCalls.push(p.functionCall); } } return out; } async function sseRequestWithRetry(payload, kind) { let attempts = 0; while (attempts < MAX_RETRY_PER_CALL) { const key = getActiveKey(); try { checkAndConsumeQuota(key); } catch (quotaErr) { // If quota exceeded on this key, rotate to next and try again. rotateKey(String(quotaErr)); attempts++; continue; } const url = STREAM_ENDPOINT(state.model, key); const bodyStr = JSON.stringify(payload); addInChars(key, bodyStr.length); const aborter = new AbortController(); state.inFlight = { aborter, kind }; let res; try { res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: bodyStr, signal: aborter.signal }); } catch (err) { // Network failure: open circuit briefly and retry with backoff openCircuitFor(key, CIRCUIT_BREAK_MS); rotateKey("network error"); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } finally { state.inFlight = null; } if (res.status === 429 || res.status === 503) { openCircuitFor(key, CIRCUIT_BREAK_MS); rotateKey(`HTTP ${res.status}`); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } if (res.status === 401 || res.status === 403) { // Invalid or unauthorized key: open circuit longer for this key openCircuitFor(key, 5 * CIRCUIT_BREAK_MS); rotateKey(`HTTP ${res.status}`); attempts++; continue; } if (!res.ok) { const text = await safeText(res); logTool("system", `Gemini error ${res.status}: ${text.slice(0, 200)}`); // For non-retryable 4xx other than 401/403/429, bubble up if (res.status >= 400 && res.status < 500 && res.status !== 429) { throw new Error(`Gemini error ${res.status}: ${text}`); } // Retryable? attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } return { stream: res.body, key }; // success } throw new Error("All retries exhausted."); } async function safeText(res) { try { return await res.text(); } catch { return "(no body)"; } } /* ========================= TOOL EXECUTION ========================= */ function toolResponseContent(name, response) { return { role: "tool", parts: [ { functionResponse: { name, response: typeof response === "object" && response !== null ? response : { data: String(response) } } } ] }; } async function execTool(call) { const { name, args } = call; try { switch (name) { case "list_tools": { const tools = [ { name: "fetch_memory(index)", use: "Retrieve detailed memory by index" }, { name: "list_memories()", use: "List memory summaries and indices" }, { name: "irc_add(step)", use: "Append one planning/validation step" }, { name: "run_js(code)", use: "Execute JS in browser; can fetch the internet" }, { name: "render_html(html)", use: "Render HTML in canvas" }, { name: "set_var(name,value) / get_var(name)", use: "Manage variables for {{var:NAME}} replacement" } ]; logTool("tool", "list_tools called"); return toolResponseContent("list_tools", { tools }); } case "list_memories": { const summaries = state.memories.map((m, i) => ({ index: i, summary: m.summary })); logTool("tool", "list_memories called"); return toolResponseContent("list_memories", { summaries }); } case "fetch_memory": { const i = Number(args?.index); if (Number.isNaN(i) || i < 0 || i >= state.memories.length) throw new Error("Invalid memory index"); logTool("tool", `fetch_memory #${i}`); return toolResponseContent("fetch_memory", { index: i, value: state.memories[i].value, summary: state.memories[i].summary }); } case "irc_add": { const step = String(args?.step || "").slice(0, 500); if (step) state.irc.push(step); renderIRC(); logTool("tool", `irc_add "${step}"`); return toolResponseContent("irc_add", { ok: true, length: state.irc.length }); } case "run_js": { const code = String(args?.code || ""); logTool("tool", `run_js (${code.length} chars)`); const out = await runUserJS(code); if (out && out.result && typeof out.result === "object") { Object.entries(out.result).forEach(([k, v]) => (state.variables[k] = v)); renderVars(); } return toolResponseContent("run_js", out); } case "render_html": { const html = String(args?.html || ""); renderCanvas(html); return toolResponseContent("render_html", { ok: true }); } case "set_var": { const name = String(args?.name || "").trim(); const value = String(args?.value ?? ""); if (!name) throw new Error("name required"); state.variables[name] = value; renderVars(); logTool("tool", `set_var ${name}`); return toolResponseContent("set_var", { ok: true }); } case "get_var": { const name = String(args?.name || "").trim(); const value = state.variables[name]; logTool("tool", `get_var ${name}`); return toolResponseContent("get_var", { name, value: value ?? null }); } default: logTool("tool", `Unknown tool: ${name}`); return toolResponseContent(name, { error: `Unknown tool ${name}` }); } } catch (err) { logTool("tool", `Error in ${name}: ${String(err)}`); return toolResponseContent(name, { error: String(err) }); } } /* ========================= CHAT ORCHESTRATION ========================= */ function buildUserContent(text) { return { role: "user", parts: [{ text }] }; } function buildSystem(text) { return { role: "user", parts: [{ text }] }; } async function toolIteration(conversation, streamSink) { let iteration = 0; while (iteration++ < MAX_TOOL_ITERATIONS) { const payload = buildPayload(conversation); let draft = ""; const { stream, key } = await sseRequestWithRetry(payload, "draft"); for await (const evt of readSSE(stream, key)) { const { textDelta, toolCalls } = extractParts(evt); if (textDelta) { draft += textDelta; streamSink(textDelta); } if (toolCalls && toolCalls.length) { // Execute tools, then continue with updated conversation for (const call of toolCalls) { logTool("intention", `Tool call requested: ${call.name}`); const toolContent = await execTool(call); conversation.push(toolContent); } } } // If we got tool calls, the conversation was extended; loop to let model consume tool responses if (conversation.length && conversation[conversation.length - 1].role === "tool") { continue; } // No outstanding tool calls; return final draft for this phase return { draft, conversation }; } throw new Error("Maximum tool iterations exceeded."); } async function verifyPass() { const verifyPrompt = [ "VERIFY AGAINST GOALS.", "Given the previous draft answer, STRICTLY check it against the Goals and the IRC (steps).", "If the draft fully satisfies all goals, return a concise, professional FINAL answer.", "Otherwise, revise it to satisfy all goals and return only the revised FINAL answer.", "Do NOT include analysis. Do NOT mention verification.", "If the draft used variables like {{var:NAME}}, keep those placeholders intact." ].join(" "); const verifyConversation = [ ...state.history, { role: "user", parts: [{ text: verifyPrompt }] } ]; const payload = buildPayload(verifyConversation); const { stream, key } = await sseRequestWithRetry(payload, "verify"); let verified = ""; for await (const evt of readSSE(stream, key)) { const { textDelta } = extractParts(evt); if (textDelta) verified += textDelta; } return verified; } async function runChatTurn(userText) { // Push user const userContent = buildUserContent(userText); state.history.push(userContent); // DRAFT PHASE (with tools) const modelMsg = addMsg("model", ""); const conv = [...state.history, buildSystem("Use tools as needed, then provide a draft.")]; let lastText = ""; const { draft, conversation } = await toolIteration(conv, (delta) => { lastText += delta; modelMsg.set(lastText); }); state.history.push({ role: "model", parts: [{ text: draft || "" }] }); // VERIFICATION PHASE const verifyMsg = addMsg("model", ""); const verified = await verifyPass(); // VARIABLE INJECTION const injected = verified.replace(/\{\{var:([\w\-]+)\}\}/g, (_, name) => String(state.variables[name] ?? "")); verifyMsg.set(injected); // Save final state.history.push({ role: "model", parts: [{ text: injected }] }); } /* ========================= EVENT BINDINGS ========================= */ function bindUI() { if (els.saveKeys) { els.saveKeys.onclick = () => { const keys = els.keyInputs.map((i) => i.value.trim()).filter(Boolean); setStoredKeys(keys); logTool("system", `Saved API keys (${keys.length})`); }; } if (els.saveGoals) { els.saveGoals.onclick = () => { const g = (els.goals && els.goals.value.trim()) || ""; localStorage.setItem(GOALS_STORAGE, g); logTool("system", "Saved goals"); }; } if (els.clearGoals) { els.clearGoals.onclick = () => { localStorage.removeItem(GOALS_STORAGE); if (els.goals) els.goals.value = ""; logTool("system", "Cleared goals"); }; } if (els.addMemory) { els.addMemory.onclick = () => { const s = (els.memSummary && els.memSummary.value.trim()) || ""; const v = (els.memValue && els.memValue.value.trim()) || ""; if (!s || !v) return; state.memories.push({ summary: s, value: v, ts: now() }); if (els.memSummary) els.memSummary.value = ""; if (els.memValue) els.memValue.value = ""; localStorage.setItem(MEMS_STORAGE, JSON.stringify(state.memories)); renderMemories(); }; } if (els.clearMemory) { els.clearMemory.onclick = () => { state.memories = []; localStorage.removeItem(MEMS_STORAGE); renderMemories(); logTool("system", "Cleared memory store"); }; } if (els.runJS) { els.runJS.onclick = async () => { const code = (els.jsCode && els.jsCode.value) || ""; const out = await runUserJS(code); if (out && out.result && typeof out.result === "object") { Object.entries(out.result).forEach(([k, v]) => (state.variables[k] = v)); renderVars(); } }; } if (els.clearJS) { els.clearJS.onclick = () => { if (els.jsCode) els.jsCode.value = ""; if (els.jsLog) els.jsLog.textContent = ""; }; } if (els.renderHTML) { els.renderHTML.onclick = () => { const html = (els.htmlCanvasInput && els.htmlCanvasInput.value) || ""; renderCanvas(html); }; } if (els.clearCanvas) { els.clearCanvas.onclick = () => renderCanvas(""); } // Send / Cancel behavior with AbortController let sending = false; const setSending = (on) => { sending = on; if (els.send) { els.send.textContent = on ? "Cancel" : "Send"; } }; if (els.send) { els.send.onclick = async () => { if (sending && state.inFlight && state.inFlight.aborter) { state.inFlight.aborter.abort(); logTool("system", "Aborted current request"); setSending(false); return; } const text = (els.prompt && els.prompt.value.trim()) || ""; if (!text) return; const yourMsg = addMsg("user", text); if (els.prompt) els.prompt.value = ""; try { setSending(true); await runChatTurn(text); } catch (err) { addMsg("model", "Error: " + String((err && err.message) || err)); } finally { setSending(false); } }; } // New thread if (els.newThread) { els.newThread.onclick = () => { state.history = []; state.irc = []; renderIRC(); if (els.chat) els.chat.innerHTML = ""; logTool("system", "Started a new thread"); }; } // Temperature sync (optional) if (els.temp) { els.temp.onchange = () => { const t = parseFloat(els.temp.value); state.temperature = isFinite(t) ? clamp(t, 0, 1) : 0.3; }; } // ESC to cancel window.addEventListener("keydown", (e) => { if (e.key === "Escape" && state.inFlight && state.inFlight.aborter) { state.inFlight.aborter.abort(); logTool("system", "Aborted current request (Esc)"); } }); } /* ========================= BOOT ========================= */ window.addEventListener("load", () => { // Force model if (els.model) { els.model.value = DEFAULT_MODEL; els.model.disabled = true; } loadFromStorage(); bindUI(); logTool("system", `Ready. Model: ${DEFAULT_MODEL}`); }); })();
Error: Gemini error 404: {
"error": {
"code": 404,
"message": "models/gemini-2.5-flash-latest is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.",
"status": "NOT_FOUND"
}
}
get the model list and make it update the list automatically. the current options must only be displayed if the list is not fetched from the internet only. Also, show rate limits for each API key entered. Output that js snippet. Make sure it will be independent or tell me explicitly which code to edit and where to find that code so that i dont mess up my copy pasting.
javascript/* DROP-IN REPLACEMENT for your previous <script>…</script> block. Paste this entire snippet inside your page’s <script> tags, replacing the old code. What’s new vs prior version: - Auto-fetches the live model list via ListModels and updates the <select id="model">. • If fetching fails (no internet/invalid keys), falls back to built-ins. • Chooses the correct streaming path per model (streamGenerateContent vs generateContent). - Shows per-key status & rate-limit view (validity + local quotas + live usage). - Keeps five-key rotation, circuit breakers, SSE streaming, JS execution, canvas rendering, Memory/IRC/Goals data-structures, verification pass, variable injection, cancel/abort. NOTE: Gemini's public API typically does not return explicit X-RateLimit headers. We display (a) whether the key is valid/usable, and (b) configurable local quotas & live usage. */ (() => { "use strict"; /* ========================= DOM HOOKS (graceful if absent) ========================= */ const q = (id) => document.getElementById(id); const els = { aside: document.querySelector('aside'), model: q('model'), keyInputs: [1, 2, 3, 4, 5].map((n) => q('key' + n)).filter(Boolean), saveKeys: q('saveKeys'), temp: q('temp'), send: q('send'), newThread: q('newThread'), chat: q('chat'), prompt: q('prompt'), goals: q('goals'), saveGoals: q('saveGoals'), clearGoals: q('clearGoals'), memSummary: q('memSummary'), memValue: q('memValue'), addMemory: q('addMemory'), clearMemory: q('clearMemory'), memoryList: q('memoryList'), runJS: q('runJS'), clearJS: q('clearJS'), jsCode: q('jsCode'), jsLog: q('jsLog'), renderHTML: q('renderHTML'), clearCanvas: q('clearCanvas'), htmlCanvasInput: q('htmlCanvasInput'), canvas: q('canvas'), toolLog: q('toolLog'), varList: q('varList'), ircList: q('ircList'), }; /* ========================= CONSTANTS ========================= */ const GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta"; const MODEL_LIST_PATH = () => `${GEMINI_BASE}/models`; const STREAM_ENDPOINT = (model, key, method) => `${GEMINI_BASE}/models/${encodeURIComponent(model)}:${method}?alt=sse&key=${encodeURIComponent(key)}`; // Fallback options ONLY if live fetch fails const FALLBACK_MODELS = [ "gemini-1.5-flash-latest", "gemini-1.5-pro-latest" ]; // Quotas: local budgets; tune with localStorage key "gemini_quota_config" const DEFAULT_QUOTAS = { rpm: 60, // requests per minute per key daily: 10000, // requests per calendar day per key sessionMax: 5000 // per page-load, across all keys }; // Retry/backoff const MAX_TOOL_ITERATIONS = 8; const MAX_RETRY_PER_CALL = 3; const BASE_BACKOFF_MS = 600; const JITTER_MS = 400; const CIRCUIT_BREAK_MS = 60_000; // Storage keys const KEYS_STORAGE = "gemini_keys"; const GOALS_STORAGE = "goals"; const MEMS_STORAGE = "memories"; const QUOTA_STORAGE = "gemini_quota_config"; const MODELS_CACHE_STORAGE = "gemini_models_cache"; // optional cache /* ========================= STATE ========================= */ const state = { // conversation history: [], irc: [], memories: [], variables: {}, // model & streaming method model: null, modelsByName: {}, // name -> meta streamingMethodByModel: {}, // name -> 'streamGenerateContent' | 'generateContent' // generation temperature: 0.3, // keys + usage keyIndex: 0, circuit: {}, // key -> { openUntil } usage: { session: { requests: 0, inChars: 0, outChars: 0, startedAt: Date.now() }, perKey: {} // keyHash -> { day, dailyRequests, minuteWindowStart, minuteCount, totalRequests, inChars, outChars } }, keyStatus: {}, // keyHash -> { lastChecked, ok, http, msg } // control inFlight: null }; /* ========================= UTILITIES ========================= */ const now = () => Date.now(); const toISODate = (ms) => new Date(ms).toISOString().slice(0, 10); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const jitter = () => Math.floor(Math.random() * JITTER_MS); const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n)); const safeJSON = (s) => { try { return JSON.parse(s); } catch { return null; } }; function el(tag, cls) { const e = document.createElement(tag); if (cls) e.className = cls; return e; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m])); } function asDisplay(v) { try { return typeof v === "string" ? v : JSON.stringify(v, null, 2); } catch { return String(v); } } function hashKey(k) { let h = 0; for (let i = 0; i < k.length; i++) h = (h * 31 + k.charCodeAt(i)) | 0; return "k" + (h >>> 0).toString(16); } /* ========================= LOGGING ========================= */ function logTool(who, text) { const line = `[${new Date().toLocaleTimeString()}] ${who.toUpperCase()}: ${text}\n`; if (els.toolLog) { els.toolLog.textContent += line; els.toolLog.scrollTop = els.toolLog.scrollHeight; } else { console.log(line.trim()); } } /* ========================= PANELS (Usage + Rate Limits) ========================= */ function ensureUsagePanel() { if (!els.aside) return null; let panel = q('usagePanel'); if (panel) return panel; panel = el('div', 'section'); panel.id = 'usagePanel'; panel.innerHTML = ` <h2>Usage</h2> <div id="usageSummary" class="small mono"></div> <div id="usageKeys" class="small mono" style="margin-top:6px"></div> <div class="divider"></div> <h2>Key Status & Rate Limits</h2> <div id="rateLimits" class="small mono"></div> `; els.aside.appendChild(panel); return panel; } function updateUsageUI() { ensureUsagePanel(); const session = state.usage.session; const sumEl = q('usageSummary'); const keysEl = q('usageKeys'); const rateEl = q('rateLimits'); const quota = getQuotaConfig(); if (sumEl) sumEl.textContent = `Session requests: ${session.requests}/${quota.sessionMax} | In chars: ${session.inChars} | Out chars: ${session.outChars}`; if (keysEl) { const lines = []; for (const [kh, v] of Object.entries(state.usage.perKey)) { lines.push(`${kh}: day ${v.day} | daily ${v.dailyRequests}/${quota.daily} | minute ${v.minuteCount}/${quota.rpm} | total ${v.totalRequests}`); } keysEl.textContent = lines.join('\n') || '(no key usage yet)'; } if (rateEl) { const keys = getStoredKeys(); if (!keys.length) { rateEl.textContent = '(enter and save API keys to view status)'; return; } const rows = []; for (const k of keys) { const kh = hashKey(k); const st = state.keyStatus[kh] || {}; const u = state.usage.perKey[kh]; const validity = st.ok ? 'OK' : (st.http ? `HTTP ${st.http}` : 'Unknown'); const msg = st.msg ? ` — ${st.msg}` : ''; rows.push(`${kh}: ${validity}${msg}`); if (u) rows.push(` usage → minute ${u.minuteCount}/${getQuotaConfig().rpm}, daily ${u.dailyRequests}/${getQuotaConfig().daily}, total ${u.totalRequests}`); } rateEl.textContent = rows.join('\n'); } } /* ========================= UI HELPERS ========================= */ function addMsg(role, content) { if (!els.chat) return { pre: { textContent: '' }, set: () => {} }; const wrap = el("div", "msg " + role); const meta = el("div", "meta"); meta.textContent = role === "user" ? "You" : "Model"; const pre = el("div"); pre.className = "mono"; pre.style.whiteSpace = "pre-wrap"; pre.textContent = content || ""; wrap.appendChild(meta); wrap.appendChild(pre); els.chat.appendChild(wrap); els.chat.scrollTop = els.chat.scrollHeight; return { pre, set: (t) => { pre.textContent = t; els.chat.scrollTop = els.chat.scrollHeight; } }; } function renderIRC() { if (els.ircList) els.ircList.textContent = state.irc.length ? state.irc.map((s, i) => `${i + 1}. ${s}`).join("\n") : "(empty)"; } function renderVars() { if (!els.varList) return; const entries = Object.entries(state.variables); els.varList.innerHTML = entries.length ? entries.map(([k, v]) => `<span class="kv mono">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`).join("") : ""; } function renderMemories() { if (!els.memoryList) return; if (!state.memories.length) { els.memoryList.innerHTML = '<div class="small">No memory items yet.</div>'; return; } els.memoryList.innerHTML = state.memories.map((m, i) => ` <div style="margin-bottom:10px"> <div><span class="pill">#${i}</span> <strong>${escapeHtml(m.summary)}</strong></div> <div class="small mono" style="margin-top:4px; white-space:pre-wrap">${escapeHtml(m.value)}</div> </div>`).join(""); } /* ========================= STORAGE ========================= */ function getStoredKeys() { return JSON.parse(localStorage.getItem(KEYS_STORAGE) || "[]"); } function setStoredKeys(arr) { localStorage.setItem(KEYS_STORAGE, JSON.stringify(arr)); } function getQuotaConfig() { const cfg = JSON.parse(localStorage.getItem(QUOTA_STORAGE) || "null"); return { rpm: (cfg && +cfg.rpm) || DEFAULT_QUOTAS.rpm, daily: (cfg && +cfg.daily) || DEFAULT_QUOTAS.daily, sessionMax: (cfg && +cfg.sessionMax) || DEFAULT_QUOTAS.sessionMax }; } /* ========================= USAGE & QUOTA ========================= */ function initKeyUsage(key) { const kh = hashKey(key); const today = toISODate(now()); const u = state.usage.perKey[kh]; if (!u || u.day !== today) { state.usage.perKey[kh] = { day: today, dailyRequests: 0, minuteWindowStart: now(), minuteCount: 0, totalRequests: (u && u.totalRequests) || 0, inChars: (u && u.inChars) || 0, outChars: (u && u.outChars) || 0 }; } } function addInChars(key, n) { const kh = hashKey(key); initKeyUsage(key); state.usage.perKey[kh].inChars += n; state.usage.session.inChars += n; updateUsageUI(); } function addOutChars(key, n) { const kh = hashKey(key); initKeyUsage(key); state.usage.perKey[kh].outChars += n; state.usage.session.outChars += n; updateUsageUI(); } function checkAndConsumeQuota(key) { const quota = getQuotaConfig(); if (state.usage.session.requests >= quota.sessionMax) throw new Error("Session request limit reached."); initKeyUsage(key); const kh = hashKey(key); const u = state.usage.perKey[kh]; // daily if (u.dailyRequests >= quota.daily) throw new Error(`Daily quota reached for key ${kh}.`); // rpm const nowMs = now(); if (nowMs - u.minuteWindowStart >= 60_000) { u.minuteWindowStart = nowMs; u.minuteCount = 0; } if (u.minuteCount >= quota.rpm) throw new Error(`Per-minute quota reached for key ${kh}.`); // consume u.minuteCount++; u.dailyRequests++; u.totalRequests++; state.usage.session.requests++; updateUsageUI(); } /* ========================= KEY MANAGEMENT ========================= */ function getActiveKey() { const keys = getStoredKeys(); if (!keys.length) throw new Error("No API key found. Save up to five keys."); let i = 0; while (i < keys.length) { const idx = state.keyIndex % keys.length; const k = keys[idx]; const circuit = state.circuit[k]; if (!circuit || circuit.openUntil <= now()) return k; state.keyIndex++; i++; } throw new Error("All keys are temporarily paused by circuit breaker."); } function rotateKey(reason) { state.keyIndex++; logTool("system", `Rotating key${reason ? `: ${reason}` : ""}`); } function openCircuitFor(key, ms) { state.circuit[key] = { openUntil: now() + ms }; logTool("system", `Circuit opened for current key for ${Math.round(ms / 1000)}s`); } /* ========================= JS EXECUTION & CANVAS ========================= */ async function runUserJS(src) { if (els.jsLog) els.jsLog.textContent = ""; const originalLog = console.log; let logs = []; console.log = (...args) => { logs.push(args.map((a) => asDisplay(a)).join(" ")); originalLog(...args); }; try { const fn = new Function(`return (async ()=>{ ${src}\n })()`); const result = await fn(); console.log = originalLog; if (els.jsLog) els.jsLog.textContent = logs.map((l) => "• " + l).join("\n") + (result !== undefined ? `\n\nreturn → ${asDisplay(result)}` : ""); logTool("run_js", `Executed (${src.length} chars)`); return { logs, result }; } catch (err) { console.log = originalLog; if (els.jsLog) els.jsLog.textContent = (logs.length ? logs.map((l) => "• " + l).join("\n") + "\n\n" : "") + "ERROR → " + String(err && err.stack || err); logTool("run_js", `Error: ${String(err)}`); return { logs, error: String(err) }; } } function renderCanvas(html) { if (els.canvas) { els.canvas.innerHTML = html; logTool("render_html", "Updated canvas"); } } /* ========================= TOOLS DECLARATIONS ========================= */ const functionDeclarations = [ { name: "fetch_memory", description: "Fetch a detailed memory by index from provided memory summaries.", parameters: { type: "OBJECT", properties: { index: { type: "INTEGER" } }, required: ["index"] } }, { name: "list_memories", description: "List available memory summaries with indices.", parameters: { type: "OBJECT", properties: {} } }, { name: "irc_add", description: "Append one step to the Immediate Reasoning Chain for the current reply.", parameters: { type: "OBJECT", properties: { step: { type: "STRING" } }, required: ["step"] } }, { name: "run_js", description: "Execute JavaScript in the browser. Use for network fetch, compute, parsing. Return logs and result.", parameters: { type: "OBJECT", properties: { code: { type: "STRING" } }, required: ["code"] } }, { name: "render_html", description: "Render HTML into the canvas region for previews or interactive reasoning.", parameters: { type: "OBJECT", properties: { html: { type: "STRING" } }, required: ["html"] } }, { name: "set_var", description: "Set a named variable for later injection into the final answer using {{var:NAME}}.", parameters: { type: "OBJECT", properties: { name: { type: "STRING" }, value: { type: "STRING" } }, required: ["name", "value"] } }, { name: "get_var", description: "Get a named variable value previously set.", parameters: { type: "OBJECT", properties: { name: { type: "STRING" } }, required: ["name"] } }, { name: "list_tools", description: "Return a concise list of available tools and when to use them.", parameters: { type: "OBJECT", properties: {} } } ]; /* ========================= SYSTEM INSTRUCTION ========================= */ function buildSystemInstruction() { const memorySummaries = state.memories.map((m, i) => `#${i}: ${m.summary}`).join("\n") || "(none)"; const ircSoFar = state.irc.length ? state.irc.map((s, i) => `${i + 1}. ${s}`).join("\n") : "(empty)"; const goals = (els.goals && els.goals.value.trim()) || "(none)"; return [ "You are a helpful model with access to an External Tools system. Do not reveal chain-of-thought.", "Work in iterations: plan → (optionally call tools) → draft → verify against goals → final. Keep planning brief via irc_add.", "", "DATA STRUCTURES:", "• Memory: You receive only memory summaries; call list_memories then fetch_memory(index) when needed.", "• Immediate Reasoning Chain (IRC): Steps for the current reply only. Append short steps with irc_add(step).", "• Goals: Global constraints to satisfy in the final answer. A verification pass checks your draft with Goals + IRC.", "", "TOOLS:", "• run_js({code}): Executes real JS in the user's browser (network fetch allowed).", "• render_html({html}): Renders HTML into a visible canvas.", "• set_var/get_var: Manage variables for final answer placeholders {{var:NAME}}.", "• list_memories/fetch_memory, list_tools.", "", "OUTPUT PROTOCOL:", "• Do not output chain-of-thought.", "• If you include JS-produced content in the final answer, set a variable via set_var and reference it as {{var:NAME}}.", "", "SUPPLIED CONTEXT:", `• Memory Summaries:\n${memorySummaries}`, `• Immediate Reasoning Chain:\n${ircSoFar}`, `• Goals:\n${goals}` ].join("\n"); } /* ========================= GEMINI: MODEL LIST (ListModels) ========================= */ async function fetchAllModelsWithAnyKey() { const keys = getStoredKeys(); if (!keys.length) throw new Error("No API key found to fetch model list."); const models = []; let ok = false; for (let i = 0; i < keys.length; i++) { const key = keys[(state.keyIndex + i) % keys.length]; try { checkAndConsumeQuota(key); } catch (e) { continue; // try next key } let pageToken = null; try { do { const url = new URL(MODEL_LIST_PATH()); url.searchParams.set("key", key); url.searchParams.set("pageSize", "100"); if (pageToken) url.searchParams.set("pageToken", pageToken); const res = await fetch(url.toString(), { method: "GET" }); const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: res.ok, http: res.status, msg: res.ok ? "Listed models" : "Failed to list models" }; if (!res.ok) { if (res.status === 401 || res.status === 403) { openCircuitFor(key, 5 * CIRCUIT_BREAK_MS); } if (res.status === 429 || res.status === 503) { openCircuitFor(key, CIRCUIT_BREAK_MS); } rotateKey(`ListModels HTTP ${res.status}`); break; // try next key } const txt = await res.text(); addOutChars(key, txt.length); const json = safeJSON(txt) || {}; const pageModels = Array.isArray(json.models) ? json.models : []; models.push(...pageModels); pageToken = json.nextPageToken || null; ok = true; } while (pageToken); if (ok) { state.keyIndex = (state.keyIndex + i) % keys.length; break; } // lock onto the working key } catch (err) { const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: false, http: 0, msg: String(err) }; openCircuitFor(key, CIRCUIT_BREAK_MS); continue; } } updateUsageUI(); if (!ok) throw new Error("Could not fetch models with any available key."); return models; } function selectStreamingMethod(meta) { const methods = meta.supportedGenerationMethods || []; // Prefer explicit streaming method if advertised if (methods.includes("streamGenerateContent")) return "streamGenerateContent"; // Fallback: many models stream via generateContent with alt=sse if (methods.includes("generateContent") || methods.includes("create")) return "generateContent"; return null; // unsupported for our chat } function populateModelSelect(models) { if (!els.model) return; els.model.innerHTML = ""; const usable = []; for (const m of models) { const method = selectStreamingMethod(m); if (!method) continue; usable.push({ name: m.name, method, displayName: m.displayName || m.name }); state.modelsByName[m.name] = m; state.streamingMethodByModel[m.name] = method; } if (usable.length === 0) { // Fallback only if nothing usable came back for (const f of FALLBACK_MODELS) { const opt = el("option"); opt.value = f; opt.textContent = f; els.model.appendChild(opt); state.streamingMethodByModel[f] = "streamGenerateContent"; // best guess } // If we previously forced a model, keep it; else choose first fallback state.model = state.model || FALLBACK_MODELS[0]; els.model.value = state.model; logTool("system", "Using fallback model list (live fetch yielded no usable models)."); return; } // Sort a bit: prefer "-latest", then flash/pro usable.sort((a, b) => { const la = a.name.includes("latest") ? 1 : 0; const lb = b.name.includes("latest") ? 1 : 0; if (la !== lb) return lb - la; const fa = a.name.includes("flash") ? 1 : 0; const fb = b.name.includes("flash") ? 1 : 0; if (fa !== fb) return fb - fa; const pa = a.name.includes("pro") ? 1 : 0; const pb = b.name.includes("pro") ? 1 : 0; return pb - pa; }); for (const u of usable) { const opt = el("option"); opt.value = u.name; opt.textContent = u.displayName; els.model.appendChild(opt); } // Choose currently selected if still present, else first usable if (!state.model || !state.streamingMethodByModel[state.model]) { state.model = usable[0].name; } els.model.value = state.model; logTool("system", `Model list updated (${usable.length} usable). Selected: ${state.model}`); } async function refreshModelList() { if (!els.model) return; els.model.disabled = true; const prev = els.model.value; els.model.innerHTML = ""; const loading = el("option"); loading.value = ""; loading.textContent = "Loading models…"; els.model.appendChild(loading); try { const cached = safeJSON(localStorage.getItem(MODELS_CACHE_STORAGE)); if (cached && Array.isArray(cached.models) && now() - (cached.cachedAt || 0) < 30 * 60 * 1000) { populateModelSelect(cached.models); } else { const live = await fetchAllModelsWithAnyKey(); localStorage.setItem(MODELS_CACHE_STORAGE, JSON.stringify({ models: live, cachedAt: now() })); populateModelSelect(live); } } catch (e) { logTool("system", "Model list fetch failed. Using fallback options."); populateModelSelect([]); // triggers fallback list only } finally { els.model.disabled = false; if (prev && state.streamingMethodByModel[prev]) { els.model.value = prev; state.model = prev; } updateUsageUI(); } } async function recheckKeyStatuses() { const keys = getStoredKeys(); if (!keys.length) { updateUsageUI(); return; } for (let i = 0; i < keys.length; i++) { const key = keys[i]; const url = new URL(MODEL_LIST_PATH()); url.searchParams.set("key", key); url.searchParams.set("pageSize", "1"); try { // We COUNT this as a request so it participates in local quotas. checkAndConsumeQuota(key); const res = await fetch(url.toString(), { method: "GET" }); const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: res.ok, http: res.status, msg: res.ok ? "OK" : "Not authorized or invalid" }; if (res.status === 401 || res.status === 403) openCircuitFor(key, 5 * CIRCUIT_BREAK_MS); if (res.status === 429 || res.status === 503) openCircuitFor(key, CIRCUIT_BREAK_MS); } catch (err) { const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: false, http: 0, msg: String(err) }; } } updateUsageUI(); } /* ========================= GEMINI STREAMING ========================= */ function buildPayload(contents) { return { systemInstruction: { role: "user", parts: [{ text: buildSystemInstruction() }] }, tools: [{ functionDeclarations }], contents, generationConfig: { temperature: (els.temp && Number(els.temp.value || "0.3")) || state.temperature, candidateCount: 1 }, safetySettings: [] }; } async function* readSSE(stream, currentKey) { const reader = stream.getReader(); const decoder = new TextDecoder("utf-8"); let buf = ""; while (true) { const { value, done } = await reader.read(); if (done) break; const textChunk = decoder.decode(value, { stream: true }); addOutChars(currentKey, textChunk.length); buf += textChunk; let idx; while ((idx = buf.indexOf("\n\n")) >= 0) { const chunk = buf.slice(0, idx).trim(); buf = buf.slice(idx + 2); if (!chunk) continue; const lines = chunk.split("\n").map((s) => s.trim()); for (const l of lines) { if (!l.startsWith("data:")) continue; const payload = l.slice(5).trim(); if (!payload || payload === "[DONE]") continue; const obj = safeJSON(payload); if (obj) yield obj; } } } } function extractParts(evt) { const out = { textDelta: "", toolCalls: [] }; const cands = evt.candidates || []; for (const c of cands) { const parts = (c.content && c.content.parts) || []; for (const p of parts) { if (p.text) out.textDelta += p.text; if (p.functionCall) out.toolCalls.push(p.functionCall); } } return out; } async function sseRequestWithRetry(payload, kind) { let attempts = 0; while (attempts < MAX_RETRY_PER_CALL) { const key = getActiveKey(); try { checkAndConsumeQuota(key); } catch (quotaErr) { rotateKey(String(quotaErr)); attempts++; continue; } // Choose path based on selected model’s capabilities const modelName = state.model; const method = state.streamingMethodByModel[modelName] || "streamGenerateContent"; const url = STREAM_ENDPOINT(modelName, key, method); const bodyStr = JSON.stringify(payload); addInChars(key, bodyStr.length); const aborter = new AbortController(); state.inFlight = { aborter, kind }; let res; try { res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: bodyStr, signal: aborter.signal }); } catch (err) { openCircuitFor(key, CIRCUIT_BREAK_MS); rotateKey("network error"); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } finally { state.inFlight = null; } if (res.status === 429 || res.status === 503) { openCircuitFor(key, CIRCUIT_BREAK_MS); rotateKey(`HTTP ${res.status}`); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } if (res.status === 401 || res.status === 403) { openCircuitFor(key, 5 * CIRCUIT_BREAK_MS); rotateKey(`HTTP ${res.status}`); attempts++; continue; } if (!res.ok) { const text = await (async()=>{ try { return await res.text(); } catch { return "(no body)"; }})(); logTool("system", `Gemini error ${res.status}: ${text.slice(0, 200)}`); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } return { stream: res.body, key }; // success } throw new Error("All retries exhausted."); } /* ========================= TOOLS RUNTIME ========================= */ function toolResponseContent(name, response) { return { role: "tool", parts: [{ functionResponse: { name, response: typeof response === "object" && response !== null ? response : { data: String(response) } } }] }; } async function execTool(call) { const { name, args } = call; try { switch (name) { case "list_tools": { const tools = [ { name: "fetch_memory(index)", use: "Retrieve detailed memory by index" }, { name: "list_memories()", use: "List memory summaries and indices" }, { name: "irc_add(step)", use: "Append one planning/validation step" }, { name: "run_js(code)", use: "Execute JS in browser; can fetch the internet" }, { name: "render_html(html)", use: "Render HTML in canvas" }, { name: "set_var(name,value) / get_var(name)", use: "Manage variables for {{var:NAME}} replacement" } ]; logTool("tool", "list_tools called"); return toolResponseContent("list_tools", { tools }); } case "list_memories": { const summaries = state.memories.map((m, i) => ({ index: i, summary: m.summary })); logTool("tool", "list_memories called"); return toolResponseContent("list_memories", { summaries }); } case "fetch_memory": { const i = Number(args?.index); if (Number.isNaN(i) || i < 0 || i >= state.memories.length) throw new Error("Invalid memory index"); logTool("tool", `fetch_memory #${i}`); return toolResponseContent("fetch_memory", { index: i, value: state.memories[i].value, summary: state.memories[i].summary }); } case "irc_add": { const step = String(args?.step || "").slice(0, 500); if (step) state.irc.push(step); renderIRC(); logTool("tool", `irc_add "${step}"`); return toolResponseContent("irc_add", { ok: true, length: state.irc.length }); } case "run_js": { const code = String(args?.code || ""); logTool("tool", `run_js (${code.length} chars)`); const out = await runUserJS(code); if (out && out.result && typeof out.result === "object") { Object.entries(out.result).forEach(([k, v]) => (state.variables[k] = v)); renderVars(); } return toolResponseContent("run_js", out); } case "render_html": { const html = String(args?.html || ""); renderCanvas(html); return toolResponseContent("render_html", { ok: true }); } case "set_var": { const name = String(args?.name || "").trim(); const value = String(args?.value ?? ""); if (!name) throw new Error("name required"); state.variables[name] = value; renderVars(); logTool("tool", `set_var ${name}`); return toolResponseContent("set_var", { ok: true }); } case "get_var": { const name = String(args?.name || "").trim(); const value = state.variables[name]; logTool("tool", `get_var ${name}`); return toolResponseContent("get_var", { name, value: value ?? null }); } default: logTool("tool", `Unknown tool: ${name}`); return toolResponseContent(name, { error: `Unknown tool ${name}` }); } } catch (err) { logTool("tool", `Error in ${name}: ${String(err)}`); return toolResponseContent(name, { error: String(err) }); } } /* ========================= CHAT LOOP ========================= */ function buildUserContent(text) { return { role: "user", parts: [{ text }] }; } function buildSystem(text) { return { role: "user", parts: [{ text }] }; } async function toolIteration(conversation, streamSink) { let iteration = 0; while (iteration++ < MAX_TOOL_ITERATIONS) { const payload = buildPayload(conversation); let draft = ""; const { stream, key } = await sseRequestWithRetry(payload, "draft"); for await (const evt of readSSE(stream, key)) { const { textDelta, toolCalls } = extractParts(evt); if (textDelta) { draft += textDelta; streamSink(textDelta); } if (toolCalls && toolCalls.length) { for (const call of toolCalls) { logTool("intention", `Tool call requested: ${call.name}`); const toolContent = await execTool(call); conversation.push(toolContent); } } } if (conversation.length && conversation[conversation.length - 1].role === "tool") continue; return { draft, conversation }; } throw new Error("Maximum tool iterations exceeded."); } async function verifyPass() { const verifyPrompt = [ "VERIFY AGAINST GOALS.", "Given the previous draft answer, STRICTLY check it against the Goals and the IRC (steps).", "If the draft fully satisfies all goals, return a concise, professional FINAL answer.", "Otherwise, revise it to satisfy all goals and return only the revised FINAL answer.", "Do NOT include analysis. Do NOT mention verification.", "If the draft used variables like {{var:NAME}}, keep those placeholders intact." ].join(" "); const verifyConversation = [...state.history, { role: "user", parts: [{ text: verifyPrompt }] }]; const payload = buildPayload(verifyConversation); const { stream, key } = await sseRequestWithRetry(payload, "verify"); let verified = ""; for await (const evt of readSSE(stream, key)) { const { textDelta } = extractParts(evt); if (textDelta) verified += textDelta; } return verified; } async function runChatTurn(userText) { const userContent = buildUserContent(userText); state.history.push(userContent); const modelMsg = addMsg("model", ""); const conv = [...state.history, buildSystem("Use tools as needed, then provide a draft.")]; let lastText = ""; const { draft, conversation } = await toolIteration(conv, (delta) => { lastText += delta; modelMsg.set(lastText); }); state.history.push({ role: "model", parts: [{ text: draft || "" }] }); const verifyMsg = addMsg("model", ""); const verified = await verifyPass(); const injected = verified.replace(/\{\{var:([\w\-]+)\}\}/g, (_, name) => String(state.variables[name] ?? "")); verifyMsg.set(injected); state.history.push({ role: "model", parts: [{ text: injected }] }); } /* ========================= BINDINGS & BOOTSTRAP ========================= */ function bindUI() { // Save Keys => persist + recheck + refresh models if (els.saveKeys) { els.saveKeys.onclick = async () => { const keys = els.keyInputs.map((i) => i.value.trim()).filter(Boolean); setStoredKeys(keys); logTool("system", `Saved API keys (${keys.length})`); await recheckKeyStatuses(); await refreshModelList(); }; } if (els.saveGoals) els.saveGoals.onclick = () => { const g = (els.goals && els.goals.value.trim()) || ""; localStorage.setItem(GOALS_STORAGE, g); logTool("system", "Saved goals"); }; if (els.clearGoals) els.clearGoals.onclick = () => { localStorage.removeItem(GOALS_STORAGE); if (els.goals) els.goals.value = ""; logTool("system", "Cleared goals"); }; if (els.addMemory) els.addMemory.onclick = () => { const s = (els.memSummary && els.memSummary.value.trim()) || ""; const v = (els.memValue && els.memValue.value.trim()) || ""; if (!s || !v) return; state.memories.push({ summary: s, value: v, ts: now() }); if (els.memSummary) els.memSummary.value = ""; if (els.memValue) els.memValue.value = ""; localStorage.setItem(MEMS_STORAGE, JSON.stringify(state.memories)); renderMemories(); }; if (els.clearMemory) els.clearMemory.onclick = () => { state.memories = []; localStorage.removeItem(MEMS_STORAGE); renderMemories(); logTool("system", "Cleared memory store"); }; if (els.runJS) els.runJS.onclick = async () => { const code = (els.jsCode && els.jsCode.value) || ""; const out = await runUserJS(code); if (out && out.result && typeof out.result === "object") { Object.entries(out.result).forEach(([k, v]) => (state.variables[k] = v)); renderVars(); } }; if (els.clearJS) els.clearJS.onclick = () => { if (els.jsCode) els.jsCode.value = ""; if (els.jsLog) els.jsLog.textContent = ""; }; if (els.renderHTML) els.renderHTML.onclick = () => { const html = (els.htmlCanvasInput && els.htmlCanvasInput.value) || ""; renderCanvas(html); }; if (els.clearCanvas) els.clearCanvas.onclick = () => renderCanvas(""); // Send / Cancel let sending = false; const setSending = (on) => { sending = on; if (els.send) els.send.textContent = on ? "Cancel" : "Send"; }; if (els.send) { els.send.onclick = async () => { if (sending && state.inFlight && state.inFlight.aborter) { state.inFlight.aborter.abort(); logTool("system", "Aborted current request"); setSending(false); return; } const text = (els.prompt && els.prompt.value.trim()) || ""; if (!text) return; addMsg("user", text); if (els.prompt) els.prompt.value = ""; try { setSending(true); await runChatTurn(text); } catch (err) { addMsg("model", "Error: " + String((err && err.message) || err)); } finally { setSending(false); } }; } // New thread if (els.newThread) els.newThread.onclick = () => { state.history = []; state.irc = []; renderIRC(); if (els.chat) els.chat.innerHTML = ""; logTool("system", "Started a new thread"); }; // Model select: update chosen model if (els.model) { els.model.onchange = () => { const sel = els.model.value; state.model = sel; logTool("system", `Selected model: ${sel} (method: ${state.streamingMethodByModel[sel] || 'streamGenerateContent'})`); }; } // Temperature if (els.temp) els.temp.onchange = () => { const t = parseFloat(els.temp.value); state.temperature = isFinite(t) ? clamp(t, 0, 1) : 0.3; }; // ESC to cancel window.addEventListener("keydown", (e) => { if (e.key === "Escape" && state.inFlight && state.inFlight.aborter) { state.inFlight.aborter.abort(); logTool("system", "Aborted current request (Esc)"); } }); } function loadFromStorage() { const keys = getStoredKeys(); keys.forEach((k, i) => { if (els.keyInputs[i]) els.keyInputs[i].value = k; }); const t = parseFloat((els.temp && els.temp.value) || "0.3"); state.temperature = isFinite(t) ? t : 0.3; state.memories = JSON.parse(localStorage.getItem(MEMS_STORAGE) || "[]"); renderMemories(); const g = localStorage.getItem(GOALS_STORAGE) || ""; if (els.goals) els.goals.value = g; // If a cached model list exists, seed UI immediately (refresh will update) const cached = safeJSON(localStorage.getItem(MODELS_CACHE_STORAGE)); if (cached && Array.isArray(cached.models)) populateModelSelect(cached.models); else populateModelSelect([]); // ensures fallback options until we fetch updateUsageUI(); renderIRC(); renderVars(); } window.addEventListener("load", async () => { loadFromStorage(); bindUI(); logTool("system", "Booting… fetching model list & key statuses."); try { await recheckKeyStatuses(); await refreshModelList(); } catch (e) { // Already handled with fallbacks } // Auto-refresh models every 30 minutes setInterval(refreshModelList, 30 * 60 * 1000); }); })();
GET https://assets.vercel.com/raw/upload/v1699973030/fonts/geist/GeistVF.woff2 net::ERR_ABORTED 404 (Not Found)Understand this error
(index):1260 GET https://assets.vercel.com/raw/upload/v1699973030/fonts/geist/GeistMonoVF.woff2 net::ERR_ABORTED 404 (Not Found)Understand this error
favicon.ico:1 GET https://k4lp.github.io/favicon.ico 404 (Not Found)Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-2.5-flash-preview-05-20:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-2.5-flash-preview-05-20:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-2.5-flash-preview-05-20:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-2.5-flash:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-2.5-flash:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-2.5-flash:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-flash-lite-latest:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-flash-lite-latest:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-flash-lite-latest:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-flash-latest:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-flash-latest:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
(index):991 POST https://generativelanguage.googleapis.com/v1beta/models/models%2Fgemini-flash-latest:generateContent?alt=sse&key=AIzaSyDWE8mmkh9gJr1ZT9_dU3QsByhYxGm5aq8 400 (Bad Request)
sseRequestWithRetry @ (index):991
await in sseRequestWithRetry
toolIteration @ (index):1103
runChatTurn @ (index):1148
els.send.onclick @ (index):1201Understand this error
fix this properly. Conduct searches properly.
javascript/* DROP-IN REPLACEMENT for your previous <script>…</script> block. Paste this WHOLE snippet inside your page’s <script> tags, replacing the old code. Fixes & additions for your errors: - 400 Bad Request from Gemini: • The API expected snake_case fields and correct function-calling shapes. • Model names from ListModels already include "models/…"; we now sanitize so the endpoint is not "models/models%2F…". • Tool responses must be sent as role:"user" with a `function_response` part (not role:"tool"). - Auto model list: • Fetches live models via ListModels using your keys and builds the <select id="model">. • If fetch fails, we fall back to a small built-in list. • Picks the right streaming method per model (streamGenerateContent or generateContent with alt=sse). - Per-key status + usage panel: • Validity checks, HTTP status, local RPM/daily/session usage counters. - Fonts & favicon 404: • These are outside JS; update your CSS @font-face URLs and <link rel="icon"> in HTML. • See comments at the bottom of this file for EXACT lines to change. SECURITY NOTE: This enables arbitrary JS execution (tool: run_js). Only run trusted code. */ (() => { "use strict"; /* ========================= DOM HOOKS ========================= */ const q = (id) => document.getElementById(id); const els = { aside: document.querySelector('aside'), model: q('model'), keyInputs: [1, 2, 3, 4, 5].map((n) => q('key' + n)).filter(Boolean), saveKeys: q('saveKeys'), temp: q('temp'), send: q('send'), newThread: q('newThread'), chat: q('chat'), prompt: q('prompt'), goals: q('goals'), saveGoals: q('saveGoals'), clearGoals: q('clearGoals'), memSummary: q('memSummary'), memValue: q('memValue'), addMemory: q('addMemory'), clearMemory: q('clearMemory'), memoryList: q('memoryList'), runJS: q('runJS'), clearJS: q('clearJS'), jsCode: q('jsCode'), jsLog: q('jsLog'), renderHTML: q('renderHTML'), clearCanvas: q('clearCanvas'), htmlCanvasInput: q('htmlCanvasInput'), canvas: q('canvas'), toolLog: q('toolLog'), varList: q('varList'), ircList: q('ircList'), }; /* ========================= CONSTANTS ========================= */ const GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta"; const MODEL_LIST_PATH = () => `${GEMINI_BASE}/models`; const STREAM_ENDPOINT = (model, key, method) => `${GEMINI_BASE}/models/${encodeURIComponent(model)}:${method}?alt=sse&key=${encodeURIComponent(key)}`; // Fallback options ONLY if live fetch fails const FALLBACK_MODELS = [ "gemini-1.5-flash-latest", "gemini-1.5-pro-latest" ]; // Quotas: local budgets; tune via localStorage key "gemini_quota_config" const DEFAULT_QUOTAS = { rpm: 60, daily: 10000, sessionMax: 5000 }; // Retry/backoff const MAX_TOOL_ITERATIONS = 8; const MAX_RETRY_PER_CALL = 3; const BASE_BACKOFF_MS = 600; const JITTER_MS = 400; const CIRCUIT_BREAK_MS = 60_000; // Storage keys const KEYS_STORAGE = "gemini_keys"; const GOALS_STORAGE = "goals"; const MEMS_STORAGE = "memories"; const QUOTA_STORAGE = "gemini_quota_config"; const MODELS_CACHE_STORAGE = "gemini_models_cache"; /* ========================= STATE ========================= */ const state = { history: [], irc: [], memories: [], variables: {}, model: null, modelsByName: {}, // name -> meta streamingMethodByModel: {}, // name -> 'streamGenerateContent' | 'generateContent' temperature: 0.3, keyIndex: 0, circuit: {}, // key -> { openUntil } usage: { session: { requests: 0, inChars: 0, outChars: 0, startedAt: Date.now() }, perKey: {} // keyHash -> { day, dailyRequests, minuteWindowStart, minuteCount, totalRequests, inChars, outChars } }, keyStatus: {}, // keyHash -> { lastChecked, ok, http, msg } inFlight: null }; /* ========================= UTILITIES ========================= */ const now = () => Date.now(); const toISODate = (ms) => new Date(ms).toISOString().slice(0, 10); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const jitter = () => Math.floor(Math.random() * JITTER_MS); const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n)); const safeJSON = (s) => { try { return JSON.parse(s); } catch { return null; } }; const has = (o,k) => Object.prototype.hasOwnProperty.call(o,k); function el(tag, cls) { const e = document.createElement(tag); if (cls) e.className = cls; return e; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m])); } function asDisplay(v) { try { return typeof v === "string" ? v : JSON.stringify(v, null, 2); } catch { return String(v); } } function hashKey(k) { let h = 0; for (let i = 0; i < k.length; i++) h = (h * 31 + k.charCodeAt(i)) | 0; return "k" + (h >>> 0).toString(16); } // Normalize model name from ListModels: // API returns "models/gemini-…". For endpoints we need only the suffix after "models/". function normalizedModelName(name) { return name.startsWith("models/") ? name.slice("models/".length) : name; } /* ========================= LOGGING ========================= */ function logTool(who, text) { const line = `[${new Date().toLocaleTimeString()}] ${who.toUpperCase()}: ${text}\n`; if (els.toolLog) { els.toolLog.textContent += line; els.toolLog.scrollTop = els.toolLog.scrollHeight; } else { console.log(line.trim()); } } /* ========================= USAGE PANEL ========================= */ function ensureUsagePanel() { if (!els.aside) return null; let panel = q('usagePanel'); if (panel) return panel; panel = el('div', 'section'); panel.id = 'usagePanel'; panel.innerHTML = ` <h2>Usage</h2> <div id="usageSummary" class="small mono"></div> <div id="usageKeys" class="small mono" style="margin-top:6px"></div> <div class="divider"></div> <h2>Key Status & Rate Limits</h2> <div id="rateLimits" class="small mono"></div> `; els.aside.appendChild(panel); return panel; } function updateUsageUI() { ensureUsagePanel(); const session = state.usage.session; const sumEl = q('usageSummary'); const keysEl = q('usageKeys'); const rateEl = q('rateLimits'); const quota = getQuotaConfig(); if (sumEl) sumEl.textContent = `Session requests: ${session.requests}/${quota.sessionMax} | In chars: ${session.inChars} | Out chars: ${session.outChars}`; if (keysEl) { const lines = []; for (const [kh, v] of Object.entries(state.usage.perKey)) { lines.push(`${kh}: day ${v.day} | daily ${v.dailyRequests}/${quota.daily} | minute ${v.minuteCount}/${quota.rpm} | total ${v.totalRequests}`); } keysEl.textContent = lines.join('\n') || '(no key usage yet)'; } if (rateEl) { const keys = getStoredKeys(); if (!keys.length) { rateEl.textContent = '(enter and save API keys to view status)'; return; } const rows = []; for (const k of keys) { const kh = hashKey(k); const st = state.keyStatus[kh] || {}; const u = state.usage.perKey[kh]; const validity = st.ok ? 'OK' : (st.http ? `HTTP ${st.http}` : 'Unknown'); const msg = st.msg ? ` — ${st.msg}` : ''; rows.push(`${kh}: ${validity}${msg}`); if (u) rows.push(` usage → minute ${u.minuteCount}/${getQuotaConfig().rpm}, daily ${u.dailyRequests}/${getQuotaConfig().daily}, total ${u.totalRequests}`); } rateEl.textContent = rows.join('\n'); } } /* ========================= UI HELPERS ========================= */ function addMsg(role, content) { if (!els.chat) return { pre: { textContent: '' }, set: () => {} }; const wrap = el("div", "msg " + role); const meta = el("div", "meta"); meta.textContent = role === "user" ? "You" : "Model"; const pre = el("div"); pre.className = "mono"; pre.style.whiteSpace = "pre-wrap"; pre.textContent = content || ""; wrap.appendChild(meta); wrap.appendChild(pre); els.chat.appendChild(wrap); els.chat.scrollTop = els.chat.scrollHeight; return { pre, set: (t) => { pre.textContent = t; els.chat.scrollTop = els.chat.scrollHeight; } }; } function renderIRC() { if (els.ircList) els.ircList.textContent = state.irc.length ? state.irc.map((s, i) => `${i + 1}. ${s}`).join("\n") : "(empty)"; } function renderVars() { if (!els.varList) return; const entries = Object.entries(state.variables); els.varList.innerHTML = entries.length ? entries.map(([k, v]) => `<span class="kv mono">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`).join("") : ""; } function renderMemories() { if (!els.memoryList) return; if (!state.memories.length) { els.memoryList.innerHTML = '<div class="small">No memory items yet.</div>'; return; } els.memoryList.innerHTML = state.memories.map((m, i) => ` <div style="margin-bottom:10px"> <div><span class="pill">#${i}</span> <strong>${escapeHtml(m.summary)}</strong></div> <div class="small mono" style="margin-top:4px; white-space:pre-wrap">${escapeHtml(m.value)}</div> </div>`).join(""); } /* ========================= STORAGE ========================= */ function getStoredKeys() { return JSON.parse(localStorage.getItem(KEYS_STORAGE) || "[]"); } function setStoredKeys(arr) { localStorage.setItem(KEYS_STORAGE, JSON.stringify(arr)); } function getQuotaConfig() { const cfg = JSON.parse(localStorage.getItem(QUOTA_STORAGE) || "null"); return { rpm: (cfg && +cfg.rpm) || DEFAULT_QUOTAS.rpm, daily: (cfg && +cfg.daily) || DEFAULT_QUOTAS.daily, sessionMax: (cfg && +cfg.sessionMax) || DEFAULT_QUOTAS.sessionMax }; } /* ========================= USAGE & QUOTA ========================= */ function initKeyUsage(key) { const kh = hashKey(key); const today = toISODate(now()); const u = state.usage.perKey[kh]; if (!u || u.day !== today) { state.usage.perKey[kh] = { day: today, dailyRequests: 0, minuteWindowStart: now(), minuteCount: 0, totalRequests: (u && u.totalRequests) || 0, inChars: (u && u.inChars) || 0, outChars: (u && u.outChars) || 0 }; } } function addInChars(key, n) { const kh = hashKey(key); initKeyUsage(key); state.usage.perKey[kh].inChars += n; state.usage.session.inChars += n; updateUsageUI(); } function addOutChars(key, n) { const kh = hashKey(key); initKeyUsage(key); state.usage.perKey[kh].outChars += n; state.usage.session.outChars += n; updateUsageUI(); } function checkAndConsumeQuota(key) { const quota = getQuotaConfig(); if (state.usage.session.requests >= quota.sessionMax) throw new Error("Session request limit reached."); initKeyUsage(key); const kh = hashKey(key); const u = state.usage.perKey[kh]; const nowMs = now(); if (u.dailyRequests >= quota.daily) throw new Error(`Daily quota reached for key ${kh}.`); if (nowMs - u.minuteWindowStart >= 60_000) { u.minuteWindowStart = nowMs; u.minuteCount = 0; } if (u.minuteCount >= quota.rpm) throw new Error(`Per-minute quota reached for key ${kh}.`); u.minuteCount++; u.dailyRequests++; u.totalRequests++; state.usage.session.requests++; updateUsageUI(); } /* ========================= KEY MANAGEMENT ========================= */ function getActiveKey() { const keys = getStoredKeys(); if (!keys.length) throw new Error("No API key found. Save up to five keys."); let i = 0; while (i < keys.length) { const idx = state.keyIndex % keys.length; const k = keys[idx]; const circuit = state.circuit[k]; if (!circuit || circuit.openUntil <= now()) return k; state.keyIndex++; i++; } throw new Error("All keys are temporarily paused by circuit breaker."); } function rotateKey(reason) { state.keyIndex++; logTool("system", `Rotating key${reason ? `: ${reason}` : ""}`); } function openCircuitFor(key, ms) { state.circuit[key] = { openUntil: now() + ms }; logTool("system", `Circuit opened for current key for ${Math.round(ms / 1000)}s`); } /* ========================= JS EXECUTION & CANVAS ========================= */ async function runUserJS(src) { if (els.jsLog) els.jsLog.textContent = ""; const originalLog = console.log; let logs = []; console.log = (...args) => { logs.push(args.map((a) => asDisplay(a)).join(" ")); originalLog(...args); }; try { const fn = new Function(`return (async ()=>{ ${src}\n })()`); const result = await fn(); console.log = originalLog; if (els.jsLog) els.jsLog.textContent = logs.map((l) => "• " + l).join("\n") + (result !== undefined ? `\n\nreturn → ${asDisplay(result)}` : ""); logTool("run_js", `Executed (${src.length} chars)`); return { logs, result }; } catch (err) { console.log = originalLog; if (els.jsLog) els.jsLog.textContent = (logs.length ? logs.map((l) => "• " + l).join("\n") + "\n\n" : "") + "ERROR → " + String(err && err.stack || err); logTool("run_js", `Error: ${String(err)}`); return { logs, error: String(err) }; } } function renderCanvas(html) { if (els.canvas) { els.canvas.innerHTML = html; logTool("render_html", "Updated canvas"); } } /* ========================= TOOLS DECLARATIONS ========================= */ const functionDeclarations = [ { name: "fetch_memory", description: "Fetch a detailed memory by index from provided memory summaries.", parameters: { type: "OBJECT", properties: { index: { type: "INTEGER" } }, required: ["index"] } }, { name: "list_memories", description: "List available memory summaries with indices.", parameters: { type: "OBJECT", properties: {} } }, { name: "irc_add", description: "Append one step to the Immediate Reasoning Chain for the current reply.", parameters: { type: "OBJECT", properties: { step: { type: "STRING" } }, required: ["step"] } }, { name: "run_js", description: "Execute JavaScript in the browser. Use for network fetch, compute, parsing. Return logs and result.", parameters: { type: "OBJECT", properties: { code: { type: "STRING" } }, required: ["code"] } }, { name: "render_html", description: "Render HTML into the canvas region for previews or interactive reasoning.", parameters: { type: "OBJECT", properties: { html: { type: "STRING" } }, required: ["html"] } }, { name: "set_var", description: "Set a named variable for later injection into the final answer using {{var:NAME}}.", parameters: { type: "OBJECT", properties: { name: { type: "STRING" }, value: { type: "STRING" } }, required: ["name", "value"] } }, { name: "get_var", description: "Get a named variable value previously set.", parameters: { type: "OBJECT", properties: { name: { type: "STRING" } }, required: ["name"] } }, { name: "list_tools", description: "Return a concise list of available tools and when to use them.", parameters: { type: "OBJECT", properties: {} } } ]; /* ========================= SYSTEM INSTRUCTION ========================= */ function buildSystemInstruction() { const memorySummaries = state.memories.map((m, i) => `#${i}: ${m.summary}`).join("\n") || "(none)"; const ircSoFar = state.irc.length ? state.irc.map((s, i) => `${i + 1}. ${s}`).join("\n") : "(empty)"; const goals = (els.goals && els.goals.value.trim()) || "(none)"; return [ "You are a helpful model with access to an External Tools system. Do not reveal chain-of-thought.", "Work in iterations: plan → (optionally call tools) → draft → verify against goals → final. Keep planning brief via irc_add.", "", "DATA STRUCTURES:", "• Memory: You receive only memory summaries; call list_memories then fetch_memory(index) when needed.", "• Immediate Reasoning Chain (IRC): Steps for the current reply only. Append short steps with irc_add(step).", "• Goals: Global constraints to satisfy in the final answer. A verification pass checks your draft with Goals + IRC.", "", "TOOLS:", "• run_js({code}): Executes real JS in the user's browser (network fetch allowed).", "• render_html({html}): Renders HTML into a visible canvas.", "• set_var/get_var: Manage variables for final answer placeholders {{var:NAME}}.", "• list_memories/fetch_memory, list_tools.", "", "OUTPUT PROTOCOL:", "• Do not output chain-of-thought.", "• If you include JS-produced content in the final answer, set a variable via set_var and reference it as {{var:NAME}}.", "", "SUPPLIED CONTEXT:", `• Memory Summaries:\n${memorySummaries}`, `• Immediate Reasoning Chain:\n${ircSoFar}`, `• Goals:\n${goals}` ].join("\n"); } /* ========================= MODEL LIST (ListModels) ========================= */ async function fetchAllModelsWithAnyKey() { const keys = getStoredKeys(); if (!keys.length) throw new Error("No API key found to fetch model list."); const models = []; let ok = false; for (let i = 0; i < keys.length; i++) { const key = keys[(state.keyIndex + i) % keys.length]; try { checkAndConsumeQuota(key); } catch { continue; } let pageToken = null; try { do { const url = new URL(MODEL_LIST_PATH()); url.searchParams.set("key", key); url.searchParams.set("pageSize", "100"); if (pageToken) url.searchParams.set("pageToken", pageToken); const res = await fetch(url.toString(), { method: "GET" }); const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: res.ok, http: res.status, msg: res.ok ? "Listed models" : "Failed to list models" }; if (!res.ok) { if (res.status === 401 || res.status === 403) openCircuitFor(key, 5 * CIRCUIT_BREAK_MS); if (res.status === 429 || res.status === 503) openCircuitFor(key, CIRCUIT_BREAK_MS); rotateKey(`ListModels HTTP ${res.status}`); break; // try next key } const txt = await res.text(); addOutChars(key, txt.length); const json = safeJSON(txt) || {}; const pageModels = Array.isArray(json.models) ? json.models : []; models.push(...pageModels); pageToken = json.nextPageToken || null; ok = true; } while (pageToken); if (ok) { state.keyIndex = (state.keyIndex + i) % keys.length; break; } } catch (err) { const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: false, http: 0, msg: String(err) }; openCircuitFor(key, CIRCUIT_BREAK_MS); continue; } } updateUsageUI(); if (!ok) throw new Error("Could not fetch models with any available key."); return models; } function selectStreamingMethod(meta) { const methods = meta.supportedGenerationMethods || []; if (methods.includes("streamGenerateContent")) return "streamGenerateContent"; if (methods.includes("generateContent") || methods.includes("create")) return "generateContent"; return null; } function populateModelSelect(models) { if (!els.model) return; els.model.innerHTML = ""; const usable = []; for (const m of models) { const method = selectStreamingMethod(m); if (!method) continue; const human = m.displayName || m.name; const canonical = normalizedModelName(m.name); usable.push({ name: canonical, method, displayName: human }); state.modelsByName[canonical] = m; state.streamingMethodByModel[canonical] = method; } if (usable.length === 0) { // Fallback only if nothing usable came back for (const f of FALLBACK_MODELS) { const opt = el("option"); opt.value = f; opt.textContent = f; els.model.appendChild(opt); state.streamingMethodByModel[f] = "streamGenerateContent"; // sensible default } state.model = state.model || FALLBACK_MODELS[0]; els.model.value = state.model; logTool("system", "Using fallback model list (live fetch yielded no usable models)."); return; } usable.sort((a, b) => { const la = a.name.includes("latest") ? 1 : 0; const lb = b.name.includes("latest") ? 1 : 0; if (la !== lb) return lb - la; const fa = a.name.includes("flash") ? 1 : 0; const fb = b.name.includes("flash") ? 1 : 0; if (fa !== fb) return fb - fa; const pa = a.name.includes("pro") ? 1 : 0; const pb = b.name.includes("pro") ? 1 : 0; return pb - pa; }); for (const u of usable) { const opt = el("option"); opt.value = u.name; opt.textContent = u.displayName; els.model.appendChild(opt); } if (!state.model || !state.streamingMethodByModel[state.model]) state.model = usable[0].name; els.model.value = state.model; logTool("system", `Model list updated (${usable.length} usable). Selected: ${state.model}`); } async function refreshModelList() { if (!els.model) return; els.model.disabled = true; const prev = els.model.value; els.model.innerHTML = ""; const loading = el("option"); loading.value = ""; loading.textContent = "Loading models…"; els.model.appendChild(loading); try { const cached = safeJSON(localStorage.getItem(MODELS_CACHE_STORAGE)); if (cached && Array.isArray(cached.models) && now() - (cached.cachedAt || 0) < 30 * 60 * 1000) { populateModelSelect(cached.models); } else { const live = await fetchAllModelsWithAnyKey(); localStorage.setItem(MODELS_CACHE_STORAGE, JSON.stringify({ models: live, cachedAt: now() })); populateModelSelect(live); } } catch (e) { logTool("system", "Model list fetch failed. Using fallback options."); populateModelSelect([]); // triggers fallback list only } finally { els.model.disabled = false; if (prev && state.streamingMethodByModel[prev]) { els.model.value = prev; state.model = prev; } updateUsageUI(); } } async function recheckKeyStatuses() { const keys = getStoredKeys(); if (!keys.length) { updateUsageUI(); return; } for (let i = 0; i < keys.length; i++) { const key = keys[i]; const url = new URL(MODEL_LIST_PATH()); url.searchParams.set("key", key); url.searchParams.set("pageSize", "1"); try { checkAndConsumeQuota(key); const res = await fetch(url.toString(), { method: "GET" }); const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: res.ok, http: res.status, msg: res.ok ? "OK" : "Not authorized or invalid" }; if (res.status === 401 || res.status === 403) openCircuitFor(key, 5 * CIRCUIT_BREAK_MS); if (res.status === 429 || res.status === 503) openCircuitFor(key, CIRCUIT_BREAK_MS); } catch (err) { const kh = hashKey(key); state.keyStatus[kh] = { lastChecked: now(), ok: false, http: 0, msg: String(err) }; } } updateUsageUI(); } /* ========================= GEMINI STREAMING (snake_case payloads) ========================= */ function buildPayload(contents) { return { system_instruction: { parts: [{ text: buildSystemInstruction() }] }, tools: [{ function_declarations: functionDeclarations }], contents, generation_config: { temperature: (els.temp && Number(els.temp.value || "0.3")) || state.temperature, candidate_count: 1 }, safety_settings: [] }; } async function* readSSE(stream, currentKey) { const reader = stream.getReader(); const decoder = new TextDecoder("utf-8"); let buf = ""; while (true) { const { value, done } = await reader.read(); if (done) break; const textChunk = decoder.decode(value, { stream: true }); addOutChars(currentKey, textChunk.length); buf += textChunk; let idx; while ((idx = buf.indexOf("\n\n")) >= 0) { const chunk = buf.slice(0, idx).trim(); buf = buf.slice(idx + 2); if (!chunk) continue; const lines = chunk.split("\n").map((s) => s.trim()); for (const l of lines) { if (!l.startsWith("data:")) continue; const payload = l.slice(5).trim(); if (!payload || payload === "[DONE]") continue; const obj = safeJSON(payload); if (obj) yield obj; } } } } function extractParts(evt) { const out = { textDelta: "", toolCalls: [] }; const cands = evt.candidates || []; for (const c of cands) { const parts = (c.content && c.content.parts) || []; for (const p of parts) { if (p.text) out.textDelta += p.text; // Accept either snake_case or camelCase from API if (p.function_call) out.toolCalls.push(p.function_call); else if (p.functionCall) out.toolCalls.push(p.functionCall); } } return out; } async function sseRequestWithRetry(payload, kind) { let attempts = 0; while (attempts < MAX_RETRY_PER_CALL) { const key = getActiveKey(); try { checkAndConsumeQuota(key); } catch (quotaErr) { rotateKey(String(quotaErr)); attempts++; continue; } const modelName = state.model; const method = state.streamingMethodByModel[modelName] || "streamGenerateContent"; const url = STREAM_ENDPOINT(modelName, key, method); const bodyStr = JSON.stringify(payload); addInChars(key, bodyStr.length); const aborter = new AbortController(); state.inFlight = { aborter, kind }; let res; try { res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: bodyStr, signal: aborter.signal }); } catch (err) { openCircuitFor(key, CIRCUIT_BREAK_MS); rotateKey("network error"); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } finally { state.inFlight = null; } if (res.status === 429 || res.status === 503) { openCircuitFor(key, CIRCUIT_BREAK_MS); rotateKey(`HTTP ${res.status}`); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } if (res.status === 401 || res.status === 403) { openCircuitFor(key, 5 * CIRCUIT_BREAK_MS); rotateKey(`HTTP ${res.status}`); attempts++; continue; } if (!res.ok) { const text = await (async()=>{ try { return await res.text(); } catch { return "(no body)"; }})(); logTool("system", `Gemini error ${res.status}: ${text.slice(0, 400)}`); attempts++; await sleep(BASE_BACKOFF_MS * Math.pow(2, attempts - 1) + jitter()); continue; } return { stream: res.body, key }; // success } throw new Error("All retries exhausted."); } /* ========================= TOOLS RUNTIME (function_response as user) ========================= */ function toolResponseContent(name, response) { return { role: "user", parts: [ { function_response: { name, response: typeof response === "object" && response !== null ? response : { data: String(response) } } } ] }; } async function execTool(call) { const { name, args } = call; try { switch (name) { case "list_tools": { const tools = [ { name: "fetch_memory(index)", use: "Retrieve detailed memory by index" }, { name: "list_memories()", use: "List memory summaries and indices" }, { name: "irc_add(step)", use: "Append one planning/validation step" }, { name: "run_js(code)", use: "Execute JS in browser; can fetch the internet" }, { name: "render_html(html)", use: "Render HTML in canvas" }, { name: "set_var(name,value) / get_var(name)", use: "Manage variables for {{var:NAME}} replacement" } ]; logTool("tool", "list_tools called"); return toolResponseContent("list_tools", { tools }); } case "list_memories": { const summaries = state.memories.map((m, i) => ({ index: i, summary: m.summary })); logTool("tool", "list_memories called"); return toolResponseContent("list_memories", { summaries }); } case "fetch_memory": { const i = Number(args?.index); if (Number.isNaN(i) || i < 0 || i >= state.memories.length) throw new Error("Invalid memory index"); logTool("tool", `fetch_memory #${i}`); return toolResponseContent("fetch_memory", { index: i, value: state.memories[i].value, summary: state.memories[i].summary }); } case "irc_add": { const step = String(args?.step || "").slice(0, 500); if (step) state.irc.push(step); renderIRC(); logTool("tool", `irc_add "${step}"`); return toolResponseContent("irc_add", { ok: true, length: state.irc.length }); } case "run_js": { const code = String(args?.code || ""); logTool("tool", `run_js (${code.length} chars)`); const out = await runUserJS(code); if (out && out.result && typeof out.result === "object") { Object.entries(out.result).forEach(([k, v]) => (state.variables[k] = v)); renderVars(); } return toolResponseContent("run_js", out); } case "render_html": { const html = String(args?.html || ""); renderCanvas(html); return toolResponseContent("render_html", { ok: true }); } case "set_var": { const name = String(args?.name || "").trim(); const value = String(args?.value ?? ""); if (!name) throw new Error("name required"); state.variables[name] = value; renderVars(); logTool("tool", `set_var ${name}`); return toolResponseContent("set_var", { ok: true }); } case "get_var": { const name = String(args?.name || "").trim(); const value = state.variables[name]; logTool("tool", `get_var ${name}`); return toolResponseContent("get_var", { name, value: value ?? null }); } default: logTool("tool", `Unknown tool: ${name}`); return toolResponseContent(name, { error: `Unknown tool ${name}` }); } } catch (err) { logTool("tool", `Error in ${name}: ${String(err)}`); return toolResponseContent(name, { error: String(err) }); } } /* ========================= CHAT LOOP ========================= */ function buildUserContent(text) { return { role: "user", parts: [{ text }] }; } function buildSystem(text) { return { role: "user", parts: [{ text }] }; } async function toolIteration(conversation, streamSink) { let iteration = 0; while (iteration++ < MAX_TOOL_ITERATIONS) { const payload = buildPayload(conversation); let draft = ""; let toolCallsHappened = false; const { stream, key } = await sseRequestWithRetry(payload, "draft"); for await (const evt of readSSE(stream, key)) { const { textDelta, toolCalls } = extractParts(evt); if (textDelta) { draft += textDelta; streamSink(textDelta); } if (toolCalls && toolCalls.length) { toolCallsHappened = true; for (const call of toolCalls) { logTool("intention", `Tool call requested: ${call.name}`); const toolContent = await execTool(call); conversation.push(toolContent); } } } if (toolCallsHappened) continue; // let the model consume tool results return { draft, conversation }; } throw new Error("Maximum tool iterations exceeded."); } async function verifyPass() { const verifyPrompt = [ "VERIFY AGAINST GOALS.", "Given the previous draft answer, STRICTLY check it against the Goals and the IRC (steps).", "If the draft fully satisfies all goals, return a concise, professional FINAL answer.", "Otherwise, revise it to satisfy all goals and return only the revised FINAL answer.", "Do NOT include analysis. Do NOT mention verification.", "If the draft used variables like {{var:NAME}}, keep those placeholders intact." ].join(" "); const verifyConversation = [...state.history, { role: "user", parts: [{ text: verifyPrompt }] }]; const payload = buildPayload(verifyConversation); const { stream, key } = await sseRequestWithRetry(payload, "verify"); let verified = ""; for await (const evt of readSSE(stream, key)) { const { textDelta } = extractParts(evt); if (textDelta) verified += textDelta; } return verified; } async function runChatTurn(userText) { const userContent = buildUserContent(userText); state.history.push(userContent); const modelMsg = addMsg("model", ""); const conv = [...state.history, buildSystem("Use tools as needed, then provide a draft.")]; let lastText = ""; const { draft, conversation } = await toolIteration(conv, (delta) => { lastText += delta; modelMsg.set(lastText); }); state.history.push({ role: "model", parts: [{ text: draft || "" }] }); const verifyMsg = addMsg("model", ""); const verified = await verifyPass(); const injected = verified.replace(/\{\{var:([\w\-]+)\}\}/g, (_, name) => String(state.variables[name] ?? "")); verifyMsg.set(injected); state.history.push({ role: "model", parts: [{ text: injected }] }); } /* ========================= BINDINGS & BOOTSTRAP ========================= */ function bindUI() { if (els.saveKeys) { els.saveKeys.onclick = async () => { const keys = els.keyInputs.map((i) => i.value.trim()).filter(Boolean); localStorage.setItem("gemini_keys", JSON.stringify(keys)); logTool("system", `Saved API keys (${keys.length})`); await recheckKeyStatuses(); await refreshModelList(); }; } if (els.saveGoals) els.saveGoals.onclick = () => { const g = (els.goals && els.goals.value.trim()) || ""; localStorage.setItem(GOALS_STORAGE, g); logTool("system", "Saved goals"); }; if (els.clearGoals) els.clearGoals.onclick = () => { localStorage.removeItem(GOALS_STORAGE); if (els.goals) els.goals.value = ""; logTool("system", "Cleared goals"); }; if (els.addMemory) els.addMemory.onclick = () => { const s = (els.memSummary && els.memSummary.value.trim()) || ""; const v = (els.memValue && els.memValue.value.trim()) || ""; if (!s || !v) return; state.memories.push({ summary: s, value: v, ts: now() }); if (els.memSummary) els.memSummary.value = ""; if (els.memValue) els.memValue.value = ""; localStorage.setItem(MEMS_STORAGE, JSON.stringify(state.memories)); renderMemories(); }; if (els.clearMemory) els.clearMemory.onclick = () => { state.memories = []; localStorage.removeItem(MEMS_STORAGE); renderMemories(); logTool("system", "Cleared memory store"); }; if (els.runJS) els.runJS.onclick = async () => { const code = (els.jsCode && els.jsCode.value) || ""; const out = await runUserJS(code); if (out && out.result && typeof out.result === "object") { Object.entries(out.result).forEach(([k, v]) => (state.variables[k] = v)); renderVars(); } }; if (els.clearJS) els.clearJS.onclick = () => { if (els.jsCode) els.jsCode.value = ""; if (els.jsLog) els.jsLog.textContent = ""; }; if (els.renderHTML) els.renderHTML.onclick = () => { const html = (els.htmlCanvasInput && els.htmlCanvasInput.value) || ""; renderCanvas(html); }; if (els.clearCanvas) els.clearCanvas.onclick = () => renderCanvas(""); // Send / Cancel let sending = false; const setSending = (on) => { sending = on; if (els.send) els.send.textContent = on ? "Cancel" : "Send"; }; if (els.send) { els.send.onclick = async () => { if (sending && state.inFlight && state.inFlight.aborter) { state.inFlight.aborter.abort(); logTool("system", "Aborted current request"); setSending(false); return; } const text = (els.prompt && els.prompt.value.trim()) || ""; if (!text) return; addMsg("user", text); if (els.prompt) els.prompt.value = ""; try { setSending(true); await runChatTurn(text); } catch (err) { addMsg("model", "Error: " + String((err && err.message) || err)); } finally { setSending(false); } }; } // New thread if (els.newThread) els.newThread.onclick = () => { state.history = []; state.irc = []; renderIRC(); if (els.chat) els.chat.innerHTML = ""; logTool("system", "Started a new thread"); }; // Model select if (els.model) { els.model.onchange = () => { const sel = els.model.value; state.model = sel; logTool("system", `Selected model: ${sel} (method: ${state.streamingMethodByModel[sel] || 'streamGenerateContent'})`); }; } // Temperature if (els.temp) els.temp.onchange = () => { const t = parseFloat(els.temp.value); state.temperature = isFinite(t) ? clamp(t, 0, 1) : 0.3; }; // ESC to cancel window.addEventListener("keydown", (e) => { if (e.key === "Escape" && state.inFlight && state.inFlight.aborter) { state.inFlight.aborter.abort(); logTool("system", "Aborted current request (Esc)"); } }); } function loadFromStorage() { const keys = getStoredKeys(); keys.forEach((k, i) => { if (els.keyInputs[i]) els.keyInputs[i].value = k; }); const t = parseFloat((els.temp && els.temp.value) || "0.3"); state.temperature = isFinite(t) ? t : 0.3; state.memories = JSON.parse(localStorage.getItem(MEMS_STORAGE) || "[]"); renderMemories(); const g = localStorage.getItem(GOALS_STORAGE) || ""; if (els.goals) els.goals.value = g; // Seed model select quickly const cached = safeJSON(localStorage.getItem(MODELS_CACHE_STORAGE)); if (cached && Array.isArray(cached.models)) populateModelSelect(cached.models); else populateModelSelect([]); // ensures fallback options until we fetch updateUsageUI(); renderIRC(); renderVars(); } window.addEventListener("load", async () => { loadFromStorage(); bindUI(); logTool("system", "Booting… checking keys & fetching model list."); try { await recheckKeyStatuses(); await refreshModelList(); } catch {} setInterval(refreshModelList, 30 * 60 * 1000); }); /* ========================= ADDITIONAL NOTES (fonts & favicon) ========================= To fix the 404s you saw: 1) Fonts (Geist URLs 404): - In your <style> @font-face blocks, replace the old URLs with working ones or host your own. - Example (replace in your HTML/CSS, not here in JS): @font-face { font-family: "Geist"; src: url("https://cdn.jsdelivr.net/gh/vercel/geist-font/packages/geist/dist/GeistVF.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; } @font-face { font-family: "Geist Mono"; src: url("https://cdn.jsdelivr.net/gh/vercel/geist-font/packages/geist/dist/GeistMonoVF.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; } 2) Favicon 404: - Add/replace in <head>: <link rel="icon" href="/favicon.ico"> - Or host one at your GitHub Pages path and point to it. These two changes are in your HTML/CSS files; this JS does not alter them. ================================================================== */ })();