workshop: страница в общей оболочке сайта + компоненты
Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js, общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/ workshop.css), компоненты читают данные пода по SSE /workshop/sse. Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/. build.js собирает страницу (active: workshop).
This commit is contained in:
parent
e2fe11f723
commit
cc3d6c5ca0
15 changed files with 1627 additions and 2 deletions
67
assets/workshop/workshop-memory.js
Normal file
67
assets/workshop/workshop-memory.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* <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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue