Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js, общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/ workshop.css), компоненты читают данные пода по SSE /workshop/sse. Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/. build.js собирает страницу (active: workshop).
242 lines
16 KiB
JavaScript
242 lines
16 KiB
JavaScript
/**
|
||
* 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>`;
|