File size: 5,397 Bytes
78cc96f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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) => `<span class="etk"><b>${escHtml(it.source)}</b> ${escHtml(it.title)}</span>`)
    .join('<span class="esep">β€’</span>');
}
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(() => {});
}