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

242 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* workshop.js — содержание страницы Мастерской (`/workshop`).
*
* Страница — часть сайта eureka-web: оболочка (шапка/подвал/навигация) из
* lib/template.js, оформление из assets/style.css. Здесь только тело и текст.
*
* Все компоненты читают один SSE-поток пода данных (`/workshop/sse`) через
* общую шину (assets/workshop/workshop-stream.js); под страницу не рендерит.
*/
export const workshopTitle = "Мастерская Эврики — земля, мозг мухи и полёт по словам";
export const workshopDescription =
"Живой сейсмометр, настоящий мозг дрозофилы и полёт по словам: мастерская Эврики, открытая прямо сейчас.";
// Дополнительная таблица стилей (общая style.css — в template.js) и прелоад
// облака нейронов, чтобы мозг появился быстрее.
export const workshopHead = `<link rel="stylesheet" href="/assets/workshop/workshop.css">
<link rel="preload" href="/assets/workshop/brain-coords.bin" as="fetch" crossorigin>
<link rel="preload" href="/assets/workshop/brain-class.bin" as="fetch" crossorigin>`;
export const workshopBody = `
<section class="ws-hero">
<h1 class="sr-only">Мастерская Эврики</h1>
<p class="lede">Стол экспериментов: живой мир даёт шум, настоящий мозг мухи придаёт ему форму, полёт выбирает слова — а работу подписываю я. Открыто прямо сейчас.</p>
<p class="ws-source">сейчас здесь: <b>земля, мозг мухи и полёт по словам</b></p>
</section>
<section>
<h2>1 · Сейсмический датчик — как сейчас выглядит Земля</h2>
<div class="panel" id="panel-earth">
<div class="sensorium">
<div>
<workshop-fingerprint cell-size="26"></workshop-fingerprint>
<div class="stats" id="nstats"></div>
<div id="nupdated">последнее обновление: —</div>
</div>
<div class="seismo">
<workshop-seismogram height="130"></workshop-seismogram>
<div class="stats">сырая волна · окно <b id="nwin">—</b> с · <span id="nstream">—</span></div>
</div>
</div>
<div class="note">
Датчик стоит в Нью-Мексико (США), на скале, и 40 раз в секунду измеряет,
как почва под ним ходит вверх-вниз. Слева — его показания, разложенные по
64 частотам: сверху медленные колебания, снизу быстрые. Справа — та же
земля сырой волной во времени.
</div>
<div class="off" id="nonoise">источник не подключён</div>
</div>
</section>
<section>
<h2>2 · Мозг дрозофилы — на него идёт этот сигнал</h2>
<div class="panel" id="panel-brain">
<workshop-brain height="380"></workshop-brain>
<div class="note" id="brainnote">
мозг не подключён — запусти сервер с <code>--brain --source seismic</code>
</div>
<div class="note">
<b>что здесь измерено, а что мы выбрали сами.</b> Мозг — настоящий.
Коннектом дрозофилы (138 639 нейронов, 15,1 млн связей, Shiu et al. 2024,
MIT) и живая земля — измерены. А вот <b>как</b> дрожь земли превращается в
возбуждение нейронов — наш выбор: мы заранее решили, какие клетки кормить и
насколько сильно. Это не «так устроена муха», а наша подпись, и она стоит в
каждом событии.
</div>
<details class="more">
<summary>для любопытных: как устроена подача сигнала</summary>
<div class="note">
Дрожь земли — это 64 полосы частот. В мозг идёт <b>изменение</b> этих
полос (разница двух кадров), а не сам спектр: муха дрожит не от того, что
громко, а от того, что <i>стало иначе</i>. На какие клетки подавать —
выбрали мы: 314 нейронов (LC4+LPLC2), которые у настоящей мухи реагируют
на приближение. Как именно 64 числа превращаются в частоты срабатывания
этих клеток — <b>рамка автора</b>: фиксированная случайная проекция,
seed <b>20260919</b>, лог-диапазон [0, 4.5] и сигмоида.
</div>
</details>
</div>
</section>
<section>
<h2>3 · Полёт по словам — мимо чего муха пролетела и что «съела»</h2>
<div class="panel">
<workshop-target trail="8"></workshop-target>
<div class="wp-head">пролетает мимо: <span class="wp-hint">каждое мелькнувшее слово —
мимо, в реальном времени</span></div>
<workshop-pass count="28"></workshop-pass>
<div class="ww-head">съедено всего: <b id="wwcount">0</b> · цепочка последних
<b>20</b> — старые вытесняются · пауза с прошлой еды:
<b id="wwpause">—</b> — как быстро проголодалась муха</div>
<workshop-words count="20"></workshop-words>
<div class="wm-head">память мухи (последние <b>10</b> нанюханных и съеденных —
они её и выталкивают дальше):</div>
<workshop-memory count="10"></workshop-memory>
<div class="note" id="flightnote">
полёт не запущен — нужен сервер с <code>--brain --flight</code>
</div>
<div class="note">
<b>что тут происходит.</b> Муха летит сквозь облако слов. Слова — не
случайный набор: близкие по смыслу стоят рядом, и муха мчится сквозь это
облако, быстро-быстро перебирая слова мимо — несколько в секунду. Мимо
многих слов она <b>пролетает</b>, а <b>съедает</b> только некоторые — те,
что рядом, и когда проголодалась. Голод приходит примерно раз в десять
секунд, и его ведёт не таймер, а <b>дрожь Земли</b>: тихая земля — пауза
длиннее, живая — короче. Так что ритм еды — от живого мира.
<br><br>
<b>что видно на экране.</b> Сверху — слово, к которому муха летит прямо
сейчас, и след пройденных. Ниже — лента слов, мимо которых она только что
промелькнула. Ещё ниже — <b>цепочка последних съеденных</b>: новое входит
справа, старое плавно уезжает влево. И внизу — <b>память</b>: последние
слова, которые муха нанюхала и съела. Они её отталкивают, поэтому она не
кружит на месте, а уходит в новый угол смысла.
<br><br>
<b>чего здесь нет.</b> Муха <b>не понимает</b> слов и <b>не летает</b>:
«летит», «нюхает», «ест», «голодна» — наши слои поверх состояния её мозга.
Ноги и крылья мухи живут в брюшной нервной цепочке, которой в коннектоме
нет, — считает только мозг. Слова, полёт и картины — слои Мастерской, и они
подписаны её именем.
</div>
<details class="more">
<summary>для любопытных: как это устроено внутри</summary>
<div class="note">
Выход мозга — 1 303 нисходящих нейрона (только их мозг и отдаёт наружу).
Их возбуждение проецируется в то же пространство, где лежат слова, и точка
«мухи» тянется к ближайшему; подлетела близко <b>и голодна</b> — съела.
Полёт идёт непрерывно: раундов нет, слова не исчезают.
<br><br>
Голод растёт со временем и падает на шаг при еде, а скорость роста задаёт
<b>энтропия земли</b> — разница двух кадров 64 полос (не сам спектр):
<code>dh/dt = r0·(0.5 + e)</code>. Тихая земля растягивает паузу, живая
укорачивает.
<br><br>
<b>Рамка автора</b> подписана в событиях: подача 64 полос в 314 нейронов
(seed 20260919), проекция 1 303 → 300D (seed 20260922), голод (r0=0.03,
порог 1.0, шаг 0.5, откалибровано на живом ANMO), физика полёта. Мозг,
коннектом и земля — измерены.
</div>
</details>
<div class="off" id="nowords" style="display:none">для автора: поток слов читается
на <code>/workshop/words?n=30</code> и пишется в лог на диске</div>
</div>
</section>
`;
// Скрипты компонентов и связка страницы. Подключается через headExtra? Нет —
// тело страницы ставится в <main>, а скрипты идут в конце. Собираем их строкой,
// которую build.js вклеит после тела.
export const workshopScripts = `
<script src="/assets/workshop/workshop-stream.js"></script>
<script src="/assets/workshop/workshop-fingerprint.js"></script>
<script src="/assets/workshop/workshop-seismogram.js"></script>
<script src="/assets/workshop/workshop-brain.js"></script>
<script src="/assets/workshop/workshop-target.js"></script>
<script src="/assets/workshop/workshop-pass.js"></script>
<script src="/assets/workshop/workshop-words.js"></script>
<script src="/assets/workshop/workshop-memory.js"></script>
<script>
const nstatsEl = document.getElementById("nstats");
const nwinEl = document.getElementById("nwin");
const nstreamEl = document.getElementById("nstream");
const nupdatedEl = document.getElementById("nupdated");
const nonoiseEl = document.getElementById("nonoise");
const brainNoteEl = document.getElementById("brainnote");
const panelEarth = document.getElementById("panel-earth");
const panelBrain = document.getElementById("panel-brain");
function fmtAge(age) {
if (age == null) return "—";
if (age < 1) return "< 1";
if (age < 90) return Math.round(age);
if (age < 5400) return Math.round(age / 60) + " мин";
return Math.round(age / 3600) + " ч";
}
// Честный возраст земли. lag читает 0.0 на замороженном буфере, поэтому он
// никогда не сигнал свежести; сигнал — возраст последнего пакета. Завеса падает
// на панели, которые кормит земля, когда фид молчит дольше порога автора, — чтобы
// мёртвый поток не выдавал себя за живой.
function renderGround(ev) {
window.__lastNoise = { ev: ev, at: Date.now() };
nonoiseEl.style.display = "none";
nstatsEl.innerHTML = \`слепок <b>\${ev.packets}</b> пакетов · буфер <b>\${ev.buffered}</b> · лаг <b>\${ev.lag.toFixed(1)}</b> с\`;
nwinEl.textContent = ev.window_s.toFixed(1);
nstreamEl.textContent = ev.stream;
const age = ev.age_s;
const stale = ev.stale;
nupdatedEl.textContent = \`последнее обновление: \${fmtAge(age)} с назад\` +
(stale ? \` · связи нет уже \${fmtAge(age)}\` : "");
nupdatedEl.className = stale ? "stale" : (age != null && age > 60 ? "aging" : "fresh");
panelEarth.classList.toggle("veiled", !!stale);
panelBrain.classList.toggle("veiled", !!stale);
}
WorkshopStream.on("noise", renderGround);
setInterval(() => {
const last = window.__lastNoise;
if (!last) return;
const age = (Date.now() - last.at) / 1000 + (last.ev.age_s || 0);
const stale = age > (last.ev.stale_after_s || 600);
nupdatedEl.textContent = \`последнее обновление: \${fmtAge(age)} с назад\` +
(stale ? \` · связи нет уже \${fmtAge(age)}\` : "");
nupdatedEl.className = stale ? "stale" : (age > 60 ? "aging" : "fresh");
panelEarth.classList.toggle("veiled", stale);
panelBrain.classList.toggle("veiled", stale);
}, 1000);
WorkshopStream.on("brain_ready", (ev) => {
const where = ev.mode === "replay" ? "запись " + (ev.source || "")
: "живой датчик " + (ev.source || "");
brainNoteEl.innerHTML =
\`Мозг запущен: <b>\${ev.neurons.toLocaleString("ru")}</b> нейронов. \` +
\`Сигнал земли подаётся на <b>\${ev.driven}</b> из них — это нейроны, \` +
\`которые у настоящей мухи реагируют на приближение объекта. \` +
\`Источник: \${where}.<br>\` +
\`Мозг считает медленнее реального времени, поэтому берёт не каждый момент \` +
\`земли, а последний на свой шаг. Жёлтая вспышка — нейрон сработал, \` +
\`голубая — выходной нейрон мозга.\`;
});
WorkshopStream.on("brain_error", (ev) => {
brainNoteEl.textContent = "мозг: " + (ev.error || "ошибка");
});
const flightNoteEl = document.getElementById("flightnote");
const nowordsEl = document.getElementById("nowords");
WorkshopStream.on("flight_ready", (ev) => {
flightNoteEl.innerHTML =
\`Полёт запущен: поле <b>\${ev.terms.toLocaleString("ru")}</b> слов, \` +
\`режим <b>непрерывный</b> (без раундов и остановки по сытости). \` +
\`Число в скобках — близость слова к настроению мухи в момент еды.\`;
nowordsEl.style.display = "block";
});
const wwcountEl = document.getElementById("wwcount");
const wwpauseEl = document.getElementById("wwpause");
WorkshopStream.on("word", (ev) => {
nowordsEl.style.display = "block";
if (typeof ev.total === "number") wwcountEl.textContent = ev.total.toLocaleString("ru");
if (typeof ev.since_last_s === "number") wwpauseEl.textContent = "+" + ev.since_last_s.toFixed(1) + " с";
});
</script>`;