/* — what the fly is carrying as "recently sniffed or eaten". * * Task 014 gave the fly a memory: the ~10 words it lately passed or ate repel * it, so it leaves a cluster instead of circling it. That memory is internal to * the flight; it is not a separate event, but it is rebuildable from the two * streams that are — `pass` (sniffed) and `word` (eaten), deduplicated by term * in arrival order, exactly the way the flight dedups its own `recent` list. * The last 10 distinct words are therefore the fly's working memory, worn * visibly: oldest faintest, newest brightest. * * Honesty: this strip is our *reconstruction* over the events, not a dump of * the flight's internal vector — the server's decay threshold can drop a word * a little before we do. It shows what repels the fly in shape, not to the * last decimal, and is labelled as the author's frame. */ class WorkshopMemory extends HTMLElement { connectedCallback() { this.count = parseInt(this.getAttribute("count") || "10", 10); this.items = []; this.renderEmpty(); this._offPass = WorkshopStream.on("pass", (ev) => this.remember(ev, false)); this._offWord = WorkshopStream.on("word", (ev) => this.remember(ev, true)); } disconnectedCallback() { if (this._offPass) this._offPass(); if (this._offWord) this._offWord(); } remember(ev, eaten) { if (!ev || !ev.term) return; // Dedup as the fly does: a word already remembered is not added again. if (this.items.some((w) => w.term === ev.term)) return; ev.__eaten = !!eaten; this.items.push(ev); if (this.items.length > this.count) this.items = this.items.slice(-this.count); this.render(); } render() { if (!this.items.length) return this.renderEmpty(); const n = this.items.length; const html = this.items.map((w, i) => { // Newest = last. Fade the oldest so the memory reads as a sliding window. const age = (n - 1 - i) / Math.max(1, n - 1); // 0 newest .. 1 oldest const opacity = (1 - age * 0.72).toFixed(3); const eaten = w.__eaten ? " wm-eaten" : ""; const sim = typeof w.sim === "number" ? w.sim.toFixed(3) : "—"; return `${escapeHtml(w.term)}`; }).join(""); this.innerHTML = html; } renderEmpty() { this.innerHTML = 'память пуста — муха ещё ничего не нанюхала'; } } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[c]); } customElements.define("workshop-memory", WorkshopMemory);