feat(lab-content-engine): phase 5 frontend — чип «Связано с программой»

Реальный фронт Ф5 (ранее ошибочно считал его сделанным параллельной сессией —
его не было). _loadRelated(simId) в lab-glue.js: GET /api/lab/sims/:id/related,
рендерит чипы-ссылки рядом с заголовком симуляции; контейнер #sim-related
создаётся динамически (без правок lab.html/CSS). Вызов из openSim (lab-init.js).
Тихо прячется при отсутствии связей/ошибке. Иконка — inline SVG .ic, без эмодзи.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 17:18:06 +03:00
parent 7d86c155c8
commit 6b0d556347
2 changed files with 55 additions and 0 deletions
+52
View File
@@ -971,6 +971,58 @@
});
}
/* ── Контент-движок, Фаза 5: чип «Связано с программой» ──────────────────
Подтягивает курикулумные связи симуляции (GET /api/lab/sims/:id/related) и
рендерит чипы-ссылки рядом с заголовком симуляции. Самодостаточно: создаёт
контейнер #sim-related динамически (без правок lab.html/CSS — меньше риск
конфликта с параллельными сессиями). Тихо прячется, если связей нет/ошибка. */
var _LAB_LINK_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
function _labRelEsc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function _ensureRelatedHost() {
var host = document.getElementById('sim-related');
if (host) return host;
host = document.createElement('div');
host.id = 'sim-related';
host.style.cssText = 'display:none;align-items:center;gap:6px;flex-wrap:wrap;margin-left:14px;min-width:0';
var title = document.getElementById('sim-topbar-title');
if (title && title.parentNode) title.parentNode.insertBefore(host, title.nextSibling);
return host;
}
function _loadRelated(simId) {
var host = _ensureRelatedHost();
host.style.display = 'none';
host.innerHTML = '';
if (!window.LS || !LS.api) return;
LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related')
.then(function (data) {
var links = (data && data.links) || {};
var all = [].concat(links.textbook || [], links.topic || [], links.kmap || [], links.question || []);
if (!all.length) return;
var chipBase = 'display:inline-flex;align-items:center;gap:4px;font-size:.72rem;padding:3px 9px;border-radius:999px;';
var html = '<span style="font-size:.68rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.05em">'
+ _LAB_LINK_ICON + ' Связано с программой</span>';
all.forEach(function (l) {
var label = _labRelEsc(l.label || (l.kind + ':' + l.ref_id));
if (l.href) {
html += '<a href="' + _labRelEsc(l.href) + '" title="Открыть в учебнике" style="' + chipBase
+ 'background:rgba(155,93,229,.14);color:var(--violet);text-decoration:none;border:1px solid rgba(155,93,229,.32)">' + label + '</a>';
} else {
html += '<span style="' + chipBase
+ 'background:rgba(255,255,255,.06);color:var(--text-2);border:1px solid rgba(255,255,255,.12)">' + label + '</span>';
}
});
host.innerHTML = html;
host.style.display = 'flex';
if (window.lucide) lucide.createIcons();
})
.catch(function () { /* нет связей или ошибка — чип просто не показываем */ });
}
window._loadRelated = _loadRelated;
/* ── embed mode + auto-open from ?sim= ── */
const _qp = new URLSearchParams(location.search);
var _embedMode = _qp.get('embed') === '1';
+3
View File
@@ -119,6 +119,9 @@
// load theory for this sim
loadTheory(id.includes(':') ? id.split(':')[0] : id);
// Фаза 5: чип «Связано с программой» (курикулумные связи симуляции).
if (typeof _loadRelated === 'function') _loadRelated(id.includes(':') ? id.split(':')[0] : id);
// ── Контент-движок (Фаза 1): диспетчеризация через реестр ──
// Все каталожные симуляции зарегистрированы в _register-all.js.
// Алиасы deep-link (magnetic/coulomb/thinlens/mirrors/refraction) нормализуем