eureka-web/assets/workshop/workshop-memory.js
eureka cc3d6c5ca0 workshop: страница в общей оболочке сайта + компоненты
Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js,
общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/
workshop.css), компоненты читают данные пода по SSE /workshop/sse.
Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/.
build.js собирает страницу (active: workshop).
2026-10-04 01:08:06 +03:00

67 lines
2.7 KiB
JavaScript

/* <workshop-memory> — 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 `<span class="wm-word${eaten}" style="opacity:${opacity}" ` +
`title="sim=${sim}">${escapeHtml(w.term)}</span>`;
}).join("");
this.innerHTML = html;
}
renderEmpty() {
this.innerHTML =
'<span class="wm-empty">память пуста — муха ещё ничего не нанюхала</span>';
}
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
})[c]);
}
customElements.define("workshop-memory", WorkshopMemory);