eureka-web/lib/workshop.js

243 lines
16 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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>`;