Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js, общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/ workshop.css), компоненты читают данные пода по SSE /workshop/sse. Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/. build.js собирает страницу (active: workshop).
67 lines
2.7 KiB
JavaScript
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) => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
})[c]);
|
|
}
|
|
|
|
customElements.define("workshop-memory", WorkshopMemory);
|