eureka-web/assets/workshop/workshop-target.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

71 lines
No EOL
2.9 KiB
JavaScript

/* <workshop-target> — where the fly is heading right now.
*
* The flight reports a `target` event on every step (throttled server-side to a
* sane rate), naming the word it currently pulls toward. That word changes
* fast — several per second — and this element shows it as a running line, with
* a short trail of the words it passed on the way. This is the fly *flying*,
* not the fly eating: the meal stream (workshop-words) stays separate and still
* arrives ~once every ten seconds.
*
* A `target` with `none: true` means every word was on cooldown — there is no
* destination at that moment, and the element says so instead of freezing on a
* stale word.
*
* Honesty: the fly does not "want" or "aim" — "target" is our layer over the
* brain's state. Which word wins is measured (`sim`); the hungriness shown is
* the author's frame, labelled on the page.
*/
class WorkshopTarget extends HTMLElement {
connectedCallback() {
this.trailLen = parseInt(this.getAttribute("trail") || "8", 10);
this.trail = [];
this.current = null;
this.render();
this._off = WorkshopStream.on("target", (ev) => this.push(ev));
}
disconnectedCallback() {
if (this._off) this._off();
}
push(ev) {
this.current = ev;
if (!ev.none && ev.term && ev.term !== (this.trail[0] && this.trail[0].term)) {
this.trail.unshift(ev);
if (this.trail.length > this.trailLen) this.trail.pop();
}
this.render();
}
render() {
const cur = this.current;
let head;
if (!cur) {
head = '<span class="wt-idle">полёт ещё не пошёл — муха ждёт настроения от мозга</span>';
} else if (cur.none) {
head = '<span class="wt-none" title="все слова на кулдауне — цели нет">нет цели · все слова отрастают</span>';
} else {
const sim = typeof cur.sim === "number" ? cur.sim.toFixed(3) : "—";
const hunger = typeof cur.hunger === "number" ? cur.hunger.toFixed(2) : "—";
const entropy = typeof cur.entropy === "number" ? cur.entropy.toFixed(2) : "—";
const tip = `lang=${cur.lang} sim=${sim} step=${cur.step} ` +
`hunger=${hunger} entropy=${entropy}`;
head = `<span class="wt-cur" title="${tip}">` +
`${escapeHtml(cur.term)}<i>${sim}</i></span>`;
}
const trail = this.trail.slice(1).map((t) =>
`<span class="wt-past" title="sim=${t.sim}">${escapeHtml(t.term)}</span>`
).join("");
this.innerHTML = `<div class="wt-line"><span class="wt-label">цель сейчас:</span>` +
`${head}</div>` +
(trail ? `<div class="wt-trail">${trail}</div>` : "");
}
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
})[c]);
}
customElements.define("workshop-target", WorkshopTarget);