// index.js — browser-only wiring. No backend, no API keys.
import { mountBrowserAgent } from "./src/ui/browser-agent.js";
import { mountNewsPage } from "./src/ui/news-page.js";
import { mountMeshPanel } from "./src/ui/mesh-panel.js";
import { createWebLLM, MODELS, hasWebGPU } from "./src/llm/webllm.js";
import { scrapeUrl, webSearchNews } from "./src/news/ingest.js";
import { ragIndex, ragSearch } from "./src/rag/rag.js";
// ── model picker + progress ───────────────────────────────────────────────
const modelSel = document.getElementById("model");
const progressEl = document.getElementById("model-progress");
MODELS.forEach((m) => {
const o = document.createElement("option");
o.value = m.id;
o.textContent = m.label;
modelSel.appendChild(o);
});
if (!hasWebGPU()) {
progressEl.textContent = "⚠ WebGPU unavailable — use Chrome/Edge to run the local model. News + mesh still work.";
progressEl.className = "warn";
}
const webllm = createWebLLM({
onProgress: (p) => {
progressEl.textContent = p.text || "";
progressEl.className = p.stage === "ready" ? "ok" : "";
},
});
// LLM adapter the agent runtime expects, pinned to the selected model.
const llm = {
chat: (args) => webllm.chat({ ...args, model: modelSel.value }),
};
// ── browser-local tools ─────────────────────────────────────────────────────
const deps = {
webSearch: (q) => webSearchNews(q),
scrape: (url) => scrapeUrl(url),
summarize: async (text, focus) => {
const sys = "Summarize the text concisely. " + (focus ? `Focus on: ${focus}.` : "");
const { text: out } = await webllm.chat({
messages: [
{ role: "system", content: sys },
{ role: "user", content: String(text).slice(0, 8000) },
],
stream: false,
model: modelSel.value,
});
return out;
},
remember: (content) => {
const mem = JSON.parse(localStorage.getItem("hearthnet_memory") || "[]");
mem.push({ content, ts: Date.now() });
localStorage.setItem("hearthnet_memory", JSON.stringify(mem));
return "saved";
},
schedule: (delaySec, message) => {
const ms = Math.max(1, Number(delaySec) || 1) * 1000;
setTimeout(() => {
if (Notification?.permission === "granted") new Notification("HearthNet", { body: message });
else alert(`HearthNet: ${message}`);
}, ms);
return `scheduled in ${Math.round(ms / 1000)}s`;
},
ragindex: (text, source) => ragIndex(text, source),
ragsearch: (query, topK) => ragSearch(query, topK || 4),
};
// ── tabs ─────────────────────────────────────────────────────────────────────
const tabs = document.querySelectorAll(".tab");
const panes = document.querySelectorAll(".pane");
tabs.forEach((t) => {
t.onclick = () => {
tabs.forEach((x) => x.classList.remove("active"));
panes.forEach((x) => x.classList.remove("active"));
t.classList.add("active");
document.getElementById(`pane-${t.dataset.tab}`).classList.add("active");
};
});
// ── mount UIs ─────────────────────────────────────────────────────────────────
mountBrowserAgent(document.getElementById("pane-agent"), llm, deps);
let lastSignals = [];
const news = mountNewsPage(document.getElementById("pane-news"), {
onSignals: (active) => { lastSignals = active; },
});
const meshUI = mountMeshPanel(document.getElementById("mesh-mount"), {
onShareSignals: (signals, from) => {
console.log("received signals from", from, signals);
},
});
// share current active signals to the mesh
document.getElementById("share-signals").onclick = () => {
meshUI.shareSignals(news.signals.filter((s) => s.active));
};
// ── easter egg: press "e" to toggle a global live news ticker ───────────────
let eggOn = false;
function isTyping() {
const t = document.activeElement;
return t && /^(input|textarea|select)$/i.test(t.tagName);
}
function escHtml(s) {
return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
}
function populateEgg() {
const track = document.getElementById("egg-track");
const items = news.items || [];
if (!items.length) {
track.textContent = "fetching live news…";
news.refresh().then(() => { if (eggOn) populateEgg(); });
return;
}
track.innerHTML = items
.slice(0, 40)
.map((it) => `${escHtml(it.source)} ${escHtml(it.title)}`)
.join('•');
}
document.addEventListener("keydown", (e) => {
if (e.key.toLowerCase() !== "e" || isTyping() || e.ctrlKey || e.metaKey || e.altKey) return;
eggOn = !eggOn;
document.getElementById("egg-ticker").classList.toggle("hidden", !eggOn);
if (eggOn) populateEgg();
});
// ask notification permission early (used by schedule tool)
if (typeof Notification !== "undefined" && Notification.permission === "default") {
Notification.requestPermission().catch(() => {});
}