// Ever-present WEED-SIM agent HUD — injects itself on every page, persists the // conversation + window box across navigations, draggable + resizable. (function () { if (window.__awLoaded) return; window.__awLoaded = true; const LS = localStorage; const get = (k, d) => { try { return JSON.parse(LS.getItem(k)) ?? d; } catch { return LS.getItem(k) ?? d; } }; const set = (k, v) => LS.setItem(k, typeof v === 'string' ? v : JSON.stringify(v)); // ---- inject DOM ---- const wrap = document.createElement('div'); wrap.innerHTML = `
${esc(typeof res === 'string' ? res : JSON.stringify(res, null, 2))}`; log.appendChild(d); log.scrollTop = log.scrollHeight; }
function intro() { bub('intro', `Ever-present agent. It drives WEED-SIM on your live session — load, grow, breed, clone, analyze the germplasm, predict crosses, read the bus — and explains as it goes. Set a key in ⚙, then try “load starters and breed me a purple high-THC strain”. Drag the title bar to move, drag the corner to resize.`); }
// ---- conversation state (persisted across pages) ----
let messages = get('aw_chat', null) || [{ role: 'system', content: SYSTEM }];
function renderHistory() {
log.innerHTML = '';
const tr = {}; messages.forEach(m => { if (m.role === 'tool') tr[m.tool_call_id] = m.content; });
let any = false;
messages.forEach(m => {
if (m.role === 'user') { bub('user', fmt(m.content)); any = true; }
else if (m.role === 'assistant') {
if (m.content) { bub('assistant', fmt(m.content)); any = true; }
(m.tool_calls || []).forEach(tc => { let a = {}; try { a = JSON.parse(tc.function.arguments || '{}'); } catch {} let r = tr[tc.id]; try { r = JSON.parse(r); } catch {} toolCard(tc.function.name, a, r); any = true; });
}
});
if (!any) intro();
}
function persist() { const trimmed = [messages[0], ...messages.slice(1).slice(-40)]; messages = trimmed; set('aw_chat', messages); }
// ---- LLM + loop ----
async function callLLM() {
const token = $('aw-tok').value.trim();
if (!token) throw new Error('Set an API key in ⚙ settings first.');
const r = await fetch('/api/agent/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-HF-Token': token }, body: JSON.stringify({ provider: $('aw-prov').value, model: $('aw-mdl').value.trim(), messages, tools: TOOLS, tool_choice: 'auto' }) });
const d = await r.json(); if (!r.ok) throw new Error(d.detail || ('HTTP ' + r.status)); return d.choices[0].message;
}
let busy = false;
async function send() {
if (busy) return; const text = $('aw-msg').value.trim(); if (!text) return; $('aw-msg').value = '';
bub('user', fmt(text)); messages.push({ role: 'user', content: text }); persist();
busy = true; $('aw-send').disabled = true;
const think = document.createElement('div'); think.className = 'aw-think'; think.textContent = 'thinking'; log.appendChild(think);
try {
for (let step = 0; step < 12; step++) {
const msg = await callLLM(); messages.push(msg); persist();
if (msg.content) bub('assistant', fmt(msg.content));
const calls = msg.tool_calls || []; if (!calls.length) break;
for (const tc of calls) {
let a = {}; try { a = JSON.parse(tc.function.arguments || '{}'); } catch {}
let res; try { res = await (IMPL[tc.function.name] || (async () => ({ error: 'unknown tool' })))(a); } catch (e) { res = { error: String(e.message || e) }; }
toolCard(tc.function.name, a, res);
messages.push({ role: 'tool', tool_call_id: tc.id, name: tc.function.name, content: JSON.stringify(res).slice(0, 6000) }); persist();
}
}
} catch (e) { bub('error', 'Agent error: ' + esc(e.message || e)); }
finally { think.remove(); busy = false; $('aw-send').disabled = false; }
}
// ---- models ----
let MODELS = [];
const ctxFmt = (n) => !n ? '' : n >= 1e6 ? (n / 1e6).toFixed(n % 1e6 ? 1 : 0) + 'M' : n >= 1e3 ? Math.round(n / 1e3) + 'k' : '' + n;
const mKey = () => 'weedsim_model_' + $('aw-prov').value, tKey = () => 'weedsim_tok_' + $('aw-prov').value;
async function loadModels() {
const token = $('aw-tok').value.trim(); if (!token) return;
$('aw-mdl').placeholder = 'loading…';
try {
const r = await fetch('/api/agent/models?provider=' + $('aw-prov').value, { headers: { 'X-HF-Token': token } });
const d = await r.json(); if (!r.ok) throw new Error(d.detail || r.status);
MODELS = d.models; renderModels(); $('aw-mdl').placeholder = 'model';
if (!$('aw-mdl').value) { const def = MODELS.find(m => m.tools) || MODELS[0]; if (def) { $('aw-mdl').value = def.id; set(mKey(), def.id); } }
} catch (e) { $('aw-mdl').placeholder = 'load failed: ' + (e.message || e); }
}
function renderModels() {
const t = $('aw-ftools').checked, fr = $('aw-ffree').checked;
const list = MODELS.filter(m => (!t || m.tools) && (!fr || m.free));
$('aw-mdls').innerHTML = list.map(m => ``).join('');
$('aw-cnt').textContent = `${list.length}/${MODELS.length} models`;
}
function loadTok() { $('aw-tok').value = LS.getItem(tKey()) || ''; $('aw-tok').placeholder = ($('aw-prov').value === 'openrouter' ? 'OpenRouter API key' : 'HF token') + ' (browser-only)'; $('aw-mdl').value = LS.getItem(mKey()) || ''; }
// ---- wire ----
$('aw-prov').value = LS.getItem('weedsim_provider') || 'openrouter'; loadTok();
$('aw-prov').onchange = () => { set('weedsim_provider', $('aw-prov').value); loadTok(); $('aw-tok').value ? loadModels() : (MODELS = [], renderModels()); };
$('aw-tok').onchange = () => { set(tKey(), $('aw-tok').value.trim()); loadModels(); };
$('aw-mdl').onchange = () => set(mKey(), $('aw-mdl').value.trim());
$('aw-ftools').onchange = renderModels; $('aw-ffree').onchange = renderModels;
$('aw-send').onclick = send;
$('aw-msg').onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } };
// Make any "Agent" nav link open the HUD on the current page instead of navigating.
document.addEventListener('click', (e) => { const a = e.target.closest('a[href="/agent"]'); if (a) { e.preventDefault(); open(true); } });
renderHistory();
if (get('aw_open', false)) open(true);
if ($('aw-tok').value) loadModels();
})();