Files
Learn_System/frontend/biochem-pathways.html
T
Maxim Dolgolyov edb4c211a0 feat: universal sidebar via sidebar.js + stale ID cleanup
- Add js/sidebar.js: generates full sidebar HTML into #app-sidebar,
  handles role-based visibility, active link (with prefix matching),
  toggle wiring, collapsed state, board/features/notif init
- Replace <aside class="sidebar">...</aside> with <aside id="app-sidebar">
  across all 35 standard-layout pages via scripts/apply-sidebar.js
- Add notifications.js to 5 pages that were missing it
- Fix api.js initPage(): skip toggle re-wiring if data-sb-wired set,
  fix active link selector .sb-item → .sb-link
- Remove stale sbl-*/nav-admin/btn-upload-nav getElementById calls
  that crashed after sidebar replacement (lab, classes, collection,
  crossword, hangman, knowledge-map, library, pet, profile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:22:21 +03:00

1241 lines
80 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Метаболические пути — Биохимия — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
html, body { height: 100%; overflow: hidden; }
.app-layout { height: 100vh; overflow: hidden; }
.sb-content { padding: 0 !important; overflow: hidden; display: flex; flex-direction: column; background: #07070f; }
.sb-sub-link { padding-left: 28px !important; font-size: 0.76rem !important; opacity: .75; }
.sb-sub-link:hover { opacity: 1; }
/* ── Page header ── */
.page-header {
padding: 18px 28px 14px;
background: linear-gradient(135deg, rgba(16,185,129,.1) 0%, rgba(155,93,229,.08) 100%);
border-bottom: 1px solid rgba(16,185,129,.15);
flex-shrink: 0;
display: flex; align-items: center; gap: 16px;
}
.page-header-icon {
width: 44px; height: 44px; border-radius: 14px;
background: rgba(16,185,129,.12); border: 1.5px solid rgba(16,185,129,.25);
display: flex; align-items: center; justify-content: center;
font-size: 1.35rem; flex-shrink: 0;
}
.page-title {
font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
background: linear-gradient(135deg,#34d399,#a78bfa); -webkit-background-clip:text; -webkit-text-fill-color:transparent;
margin-bottom: 2px;
}
.page-subtitle { font-size: 0.78rem; color: #555; }
.header-right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
/* ── Pathway selector ── */
.path-selector {
display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
padding: 10px 20px;
background: rgba(7,7,20,.95);
border-bottom: 1px solid rgba(255,255,255,.06);
flex-shrink: 0;
}
.path-chip {
padding: 5px 14px; border-radius: 999px;
border: 1.5px solid rgba(255,255,255,.09);
background: rgba(255,255,255,.04); color: #666;
font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 700;
cursor: pointer; transition: all .18s; white-space: nowrap;
display: flex; align-items: center; gap: 5px;
}
.path-chip:hover { border-color: rgba(52,211,153,.35); color: #ccc; background: rgba(52,211,153,.07); }
.path-chip.active { background: rgba(52,211,153,.16); border-color: rgba(52,211,153,.55); color: #34d399; }
.path-chip .chip-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.path-chip[data-path="glycolysis"].active { color: #f59e0b; border-color: rgba(245,158,11,.5); background: rgba(245,158,11,.12); }
.path-chip[data-path="glycolysis"] .chip-dot { background: #f59e0b; }
.path-chip[data-path="krebs"].active { color: #06b6d4; border-color: rgba(6,182,212,.5); background: rgba(6,182,212,.12); }
.path-chip[data-path="krebs"] .chip-dot { background: #06b6d4; }
.path-chip[data-path="oxidation"].active { color: #fb923c; border-color: rgba(251,146,60,.5); background: rgba(251,146,60,.12); }
.path-chip[data-path="oxidation"] .chip-dot { background: #fb923c; }
.path-chip[data-path="synthesis"].active { color: #a78bfa; border-color: rgba(167,139,250,.5); background: rgba(167,139,250,.12); }
.path-chip[data-path="synthesis"] .chip-dot { background: #a78bfa; }
.sel-divider { width: 1px; height: 20px; background: rgba(255,255,255,.07); margin: 0 4px; }
.learn-btn {
margin-left: auto; padding: 5px 16px; border-radius: 8px;
background: linear-gradient(135deg,#34d399,#059669); border: none;
color: #fff; font-family:'Manrope',sans-serif; font-size:.8rem; font-weight:700;
cursor: pointer; transition: opacity .18s; display: flex; align-items: center; gap: 6px;
}
.learn-btn:hover { opacity: .85; }
.anim-btn {
padding: 5px 14px; border-radius: 8px;
background: rgba(255,255,255,.05); border: 1.5px solid rgba(255,255,255,.1);
color: #aaa; font-family:'Manrope',sans-serif; font-size:.78rem; font-weight:600;
cursor: pointer; transition: all .18s; display: flex; align-items: center; gap: 5px;
}
.anim-btn.on { background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.4); color: #34d399; }
.anim-btn:hover { border-color: rgba(255,255,255,.2); color: #ccc; }
/* ── Main layout ── */
.paths-main {
flex: 1; display: flex; overflow: hidden; min-height: 0;
}
/* ── SVG Canvas area ── */
.svg-area {
flex: 1; overflow: hidden; position: relative;
background: radial-gradient(ellipse at 40% 40%, rgba(16,185,129,.04) 0%, transparent 60%),
radial-gradient(ellipse at 70% 70%, rgba(155,93,229,.04) 0%, transparent 60%),
#07070f;
cursor: grab;
}
.svg-area:active { cursor: grabbing; }
#pathway-svg {
width: 100%; height: 100%;
overflow: visible;
}
/* SVG node styles */
.mol-node { cursor: pointer; transition: filter .18s; }
.mol-node:hover .node-circle { filter: brightness(1.4); }
.mol-node .node-circle { transition: r .15s; }
.mol-node.active .node-circle { filter: drop-shadow(0 0 8px currentColor); }
.node-label {
font-family: 'Manrope', sans-serif; font-size: 11px; font-weight: 700;
fill: #e0e0f0; pointer-events: none; dominant-baseline: central; text-anchor: middle;
}
.node-formula {
font-family: 'Manrope', sans-serif; font-size: 9.5px;
fill: #777; pointer-events: none; dominant-baseline: central; text-anchor: middle;
}
.edge-arrow { marker-end: url(#arrowhead); }
.edge-label {
font-family: 'Manrope', sans-serif; font-size: 9px;
fill: #555; dominant-baseline: central; text-anchor: middle;
pointer-events: none;
}
.edge-enzyme {
font-family: 'Manrope', sans-serif; font-size: 8.5px; font-style: italic;
fill: #494; pointer-events: none; dominant-baseline: central; text-anchor: middle;
}
/* Particle animation */
.flow-particle { pointer-events: none; }
@keyframes particleAppear { from { opacity:0; transform:scale(0); } to { opacity:1; transform:scale(1); } }
/* ── Side panel ── */
.side-panel {
width: 320px; flex-shrink: 0;
border-left: 1px solid rgba(255,255,255,.07);
background: rgba(8,8,20,.9);
display: flex; flex-direction: column; overflow: hidden;
transition: width .25s;
}
.side-panel.hidden { width: 0; overflow: hidden; }
.panel-tabs {
display: flex; border-bottom: 1px solid rgba(255,255,255,.07); flex-shrink: 0;
}
.ptab {
flex: 1; padding: 10px 8px; text-align: center;
font-family: 'Manrope', sans-serif; font-size: .75rem; font-weight: 700;
color: #555; cursor: pointer; border-bottom: 2px solid transparent;
transition: all .18s; white-space: nowrap;
}
.ptab.active { color: #34d399; border-bottom-color: #34d399; }
.ptab:hover { color: #aaa; }
.panel-body { flex: 1; overflow-y: auto; padding: 16px; }
.panel-body::-webkit-scrollbar { width: 4px; }
.panel-body::-webkit-scrollbar-thumb { background: rgba(52,211,153,.25); border-radius: 4px; }
/* Mol popup card */
.mol-card { display: none; }
.mol-card.show { display: block; }
.mol-card-name {
font-family: 'Unbounded', sans-serif; font-size: .9rem; font-weight: 800;
color: #e0e0f0; margin-bottom: 3px;
}
.mol-card-formula {
font-size: .8rem; color: #888; margin-bottom: 12px; font-family: 'Manrope', sans-serif;
}
.mol-card-desc {
font-size: .8rem; color: #aaa; line-height: 1.55; font-family: 'Manrope', sans-serif;
margin-bottom: 12px;
}
.mol-card-props { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.mol-prop { padding: 3px 10px; border-radius: 6px; font-size: .72rem; font-weight: 600;
font-family: 'Manrope',sans-serif; }
.mol-prop.atp { background: rgba(245,158,11,.15); color: #f59e0b; border: 1px solid rgba(245,158,11,.25); }
.mol-prop.nadh { background: rgba(6,182,212,.15); color: #06b6d4; border: 1px solid rgba(6,182,212,.25); }
.mol-prop.co2 { background: rgba(239,68,68,.12); color: #f87171; border: 1px solid rgba(239,68,68,.2); }
.mol-prop.key { background: rgba(52,211,153,.12); color: #34d399; border: 1px solid rgba(52,211,153,.2); }
.mol-card-btn {
display: block; width: 100%; padding: 7px; border-radius: 9px; text-align: center;
background: rgba(52,211,153,.1); border: 1.5px solid rgba(52,211,153,.25);
color: #34d399; font-family:'Manrope',sans-serif; font-size:.78rem; font-weight:700;
cursor: pointer; text-decoration: none; transition: all .18s; margin-top: 4px;
}
.mol-card-btn:hover { background: rgba(52,211,153,.2); }
.mol-empty {
text-align: center; padding: 40px 20px;
color: #444; font-family:'Manrope',sans-serif; font-size:.83rem;
}
.mol-empty .hint-icon { font-size: 2.2rem; margin-bottom: 8px; }
/* ── Learn mode ── */
.learn-panel { display: none; }
.learn-panel.active { display: block; }
.learn-progress-bar {
height: 4px; border-radius: 2px; background: rgba(255,255,255,.07); margin-bottom: 14px;
}
.learn-progress-fill {
height: 100%; border-radius: 2px;
background: linear-gradient(90deg,#34d399,#a78bfa);
transition: width .4s;
}
.step-counter {
font-family:'Manrope',sans-serif; font-size:.72rem; color:#555; margin-bottom:10px;
}
.step-num {
display: inline-flex; align-items:center; justify-content:center;
width:28px; height:28px; border-radius:50%;
background: linear-gradient(135deg,#34d399,#059669);
color:#fff; font-family:'Unbounded',sans-serif; font-size:.7rem; font-weight:800;
margin-right: 8px; flex-shrink:0;
}
.step-title-row { display:flex; align-items:center; margin-bottom:10px; }
.step-title {
font-family:'Unbounded',sans-serif; font-size:.82rem; font-weight:800; color:#e0e0f0;
}
.step-desc {
font-family:'Manrope',sans-serif; font-size:.8rem; color:#aaa; line-height:1.6;
margin-bottom: 12px;
}
.step-energy {
display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;
}
.energy-badge {
padding: 3px 10px; border-radius:6px; font-family:'Manrope',sans-serif;
font-size:.72rem; font-weight:700;
}
.energy-badge.atp-prod { background:rgba(245,158,11,.15); color:#f59e0b; }
.energy-badge.atp-used { background:rgba(239,68,68,.12); color:#f87171; }
.energy-badge.nadh { background:rgba(6,182,212,.12); color:#06b6d4; }
.energy-badge.fadh2 { background:rgba(167,139,250,.12);color:#a78bfa; }
.energy-badge.co2 { background:rgba(74,222,128,.1); color:#4ade80; }
/* Mini-quiz */
.mini-quiz { margin-top:12px; }
.quiz-q {
font-family:'Manrope',sans-serif; font-size:.8rem; font-weight:700; color:#ddd;
margin-bottom:8px;
}
.quiz-opts { display:flex; flex-direction:column; gap:5px; }
.quiz-opt {
padding:7px 12px; border-radius:8px;
border:1.5px solid rgba(255,255,255,.08); background:rgba(255,255,255,.03);
color:#aaa; font-family:'Manrope',sans-serif; font-size:.78rem;
cursor:pointer; transition:all .16s; text-align:left;
}
.quiz-opt:hover { border-color:rgba(52,211,153,.35); color:#ccc; background:rgba(52,211,153,.06); }
.quiz-opt.correct { border-color:rgba(52,211,153,.6) !important; background:rgba(52,211,153,.15) !important; color:#34d399 !important; }
.quiz-opt.wrong { border-color:rgba(239,68,68,.5) !important; background:rgba(239,68,68,.1) !important; color:#f87171 !important; }
.quiz-feedback {
margin-top:8px; font-family:'Manrope',sans-serif; font-size:.78rem;
padding:7px 12px; border-radius:8px;
}
.quiz-feedback.ok { background:rgba(52,211,153,.1); color:#34d399; }
.quiz-feedback.err { background:rgba(239,68,68,.08); color:#f87171; }
.step-nav { display:flex; gap:8px; margin-top:14px; }
.step-btn {
flex:1; padding:8px; border-radius:9px;
font-family:'Manrope',sans-serif; font-size:.78rem; font-weight:700;
cursor:pointer; transition:all .18s; border:none;
}
.step-btn.prev { background:rgba(255,255,255,.06); color:#888; }
.step-btn.prev:hover { background:rgba(255,255,255,.1); color:#ccc; }
.step-btn.next { background:linear-gradient(135deg,#34d399,#059669); color:#fff; }
.step-btn.next:hover { opacity:.88; }
.step-btn:disabled { opacity:.3; cursor:default; }
.learn-complete {
text-align:center; padding:20px 10px;
}
.complete-icon { font-size:2.5rem; margin-bottom:10px; }
.complete-title {
font-family:'Unbounded',sans-serif; font-size:.9rem; font-weight:800; color:#34d399;
margin-bottom:6px;
}
.complete-text { font-family:'Manrope',sans-serif; font-size:.78rem; color:#888; }
/* ── Path legend ── */
.path-legend {
padding: 12px 16px;
border-top: 1px solid rgba(255,255,255,.06);
flex-shrink: 0;
}
.legend-title {
font-family:'Manrope',sans-serif; font-size:.7rem; font-weight:700; color:#444;
text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px;
}
.legend-items { display:flex; flex-wrap:wrap; gap:6px; }
.legend-item {
display:flex; align-items:center; gap:4px;
font-family:'Manrope',sans-serif; font-size:.72rem; color:#666;
}
.legend-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
.legend-line { width:18px; height:2px; flex-shrink:0; border-radius:1px; }
/* zoom controls */
.zoom-controls {
position: absolute; bottom: 16px; right: 16px;
display: flex; gap: 6px;
}
.zoom-btn {
width: 34px; height: 34px; border-radius: 9px;
background: rgba(15,15,30,.9); border: 1.5px solid rgba(255,255,255,.09);
color: #aaa; font-size: 1.1rem; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all .16s;
}
.zoom-btn:hover { background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.3); color: #34d399; }
/* path stats bar */
.path-stats {
position: absolute; top: 12px; left: 16px;
display: flex; gap: 8px; pointer-events: none;
}
.pstat {
padding: 4px 12px; border-radius: 8px;
background: rgba(8,8,20,.85); border: 1px solid rgba(255,255,255,.08);
font-family:'Manrope',sans-serif; font-size:.73rem; font-weight:700;
backdrop-filter: blur(8px);
}
.pstat.atp { color:#f59e0b; }
.pstat.nadh { color:#06b6d4; }
.pstat.co2 { color:#f87171; }
/* ── Mobile ── */
@media (max-width: 768px) {
.sb-content { flex-direction: column; overflow: auto; }
.page-header { padding: 10px 14px 0; gap: 8px; }
.page-title { font-size: 0.85rem !important; }
.page-subtitle { display: none; }
.header-right { gap: 4px; }
.path-selector { padding: 8px 12px; gap: 5px; flex-wrap: wrap; }
.paths-main { flex-direction: column-reverse; }
.svg-area { min-height: 55vw; flex: none; height: 55vw; }
.side-panel { width: 100% !important; max-height: 40vh; border-left: none !important; border-top: 1px solid rgba(255,255,255,.07); overflow-y: auto; }
.path-stats { top: 6px; left: 8px; gap: 4px; }
.zoom-controls { bottom: 8px; right: 8px; }
}
@media (max-width: 480px) {
.svg-area { min-height: 260px; height: 260px; }
.path-chip { font-size: 0.68rem; padding: 4px 10px; }
.side-panel { max-height: 45vh; }
}
</style>
</head>
<body>
<div class="app-layout" id="app">
<aside class="sidebar" id="app-sidebar"></aside>
<div class="notif-drop" id="notif-drop"></div>
<div class="sb-content">
<!-- Header -->
<div class="page-header">
<div class="page-header-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M6 18h8"/><path d="M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14l2-7"/><path d="M12 14l2-7"/></svg></div>
<div>
<div class="page-title">Метаболические пути</div>
<div class="page-subtitle" id="path-subtitle">Интерактивные схемы обмена веществ</div>
</div>
<div class="header-right">
<div class="nav-user-chip">
<div class="nav-avatar" id="nav-avatar2"></div>
</div>
</div>
</div>
<!-- Pathway selector -->
<div class="path-selector">
<span style="font-family:'Manrope',sans-serif;font-size:.75rem;color:#444;font-weight:700;">Путь:</span>
<button class="path-chip active" data-path="glycolysis" onclick="selectPath('glycolysis')">
<span class="chip-dot"></span>Гликолиз
</button>
<button class="path-chip" data-path="krebs" onclick="selectPath('krebs')">
<span class="chip-dot"></span>Цикл Кребса
</button>
<button class="path-chip" data-path="oxidation" onclick="selectPath('oxidation')">
<span class="chip-dot"></span>β-Окисление
</button>
<button class="path-chip" data-path="synthesis" onclick="selectPath('synthesis')">
<span class="chip-dot"></span>Синтез белка
</button>
<div class="sel-divider"></div>
<button class="anim-btn on" id="anim-toggle" onclick="toggleAnimation()">
<i data-lucide="play-circle" style="width:14px;height:14px"></i> Анимация
</button>
<button class="learn-btn" onclick="startLearn()">
<i data-lucide="graduation-cap" style="width:14px;height:14px"></i> Пройти путь
</button>
</div>
<!-- Main content -->
<div class="paths-main">
<!-- SVG area -->
<div class="svg-area" id="svg-area">
<svg id="pathway-svg" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="rgba(120,120,160,0.7)" />
</marker>
<marker id="arrowhead-active" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#34d399" />
</marker>
<marker id="arrowhead-gly" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#f59e0b" />
</marker>
<marker id="arrowhead-krebs" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#06b6d4" />
</marker>
<filter id="glow-green">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-amber">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<g id="svg-root" transform="translate(0,0) scale(1)">
<g id="edges-layer"></g>
<g id="nodes-layer"></g>
<g id="particles-layer"></g>
</g>
</svg>
<!-- Path stats overlay -->
<div class="path-stats" id="path-stats"></div>
<!-- Zoom controls -->
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoom(1.2)" title="Увеличить">+</button>
<button class="zoom-btn" onclick="zoom(0.83)" title="Уменьшить"></button>
<button class="zoom-btn" onclick="resetView()" title="Сбросить"></button>
</div>
</div>
<!-- Side panel -->
<div class="side-panel" id="side-panel">
<div class="panel-tabs">
<div class="ptab active" id="tab-mol" onclick="switchTab('mol')">Молекула</div>
<div class="ptab" id="tab-learn" onclick="switchTab('learn')">Обучение</div>
<div class="ptab" id="tab-info" onclick="switchTab('info')">Путь</div>
</div>
<div class="panel-body" id="panel-mol">
<div class="mol-empty" id="mol-empty">
<div class="hint-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M4 4l7.07 17 2.51-7.39L21 11.07z"/></svg></div>
<div>Кликни на молекулу<br/>на схеме</div>
</div>
<div class="mol-card" id="mol-card">
<div class="mol-card-name" id="mc-name"></div>
<div class="mol-card-formula" id="mc-formula"></div>
<div class="mol-card-desc" id="mc-desc"></div>
<div class="mol-card-props" id="mc-props"></div>
<a class="mol-card-btn" id="mc-link">Открыть в редакторе <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></a>
</div>
</div>
<div class="panel-body" id="panel-learn" style="display:none">
<div class="learn-panel" id="learn-inactive" style="display:block">
<div class="mol-empty">
<div class="hint-icon"><svg class="ic" viewBox="0 0 24 24"><polygon points="22 10 12 5 2 10 12 15 22 10"/><polyline points="6 12 6 17"/><path d="M18 13.5V17c-3 1.5-9 1.5-12 0v-3.5"/></svg></div>
<div>Нажми «Пройти путь»<br/>чтобы начать обучение</div>
</div>
</div>
<div class="learn-panel" id="learn-active"></div>
</div>
<div class="panel-body" id="panel-info" style="display:none">
<div id="path-info-content"></div>
</div>
<div class="path-legend" id="path-legend">
<div class="legend-title">Обозначения</div>
<div class="legend-items" id="legend-items"></div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script>
// ═══════════════════════════════════════════════════════
// PATHWAY DATA
// ═══════════════════════════════════════════════════════
const PATHWAYS = {
glycolysis: {
name: 'Гликолиз',
color: '#f59e0b',
colorRgb: '245,158,11',
desc: '10 реакций расщепления глюкозы до пирувата. Происходит в цитоплазме. Выход: 2 АТФ (нетто), 2 НАДН, 2 пируват.',
stats: [
{ label: '2 АТФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> +4 АТФ', cls: 'atp' },
{ label: '2 НАДН', cls: 'nadh' },
],
legend: [
{ color: '#f59e0b', type: 'circle', label: 'Метаболит' },
{ color: '#f59e0b', type: 'line', label: 'Реакция' },
{ color: '#f59e0b88', type: 'circle-sm', label: 'Кофактор (АТФ/НАД)' },
],
// nodes: id, label, formula, x, y, role
nodes: [
{ id:'glc', label:'Глюкоза', formula:'C₆H₁₂O₆', x:400, y:60, role:'substrate', desc:'Исходный субстрат гликолиза. 6-углеродный сахар, главный источник энергии клетки.', props:[] },
{ id:'g6p', label:'Глюкозо-6-Ф', formula:'C₆H₁₃O₉P', x:400, y:145, role:'inter', desc:'Глюкозо-6-фосфат. Образуется при фосфорилировании глюкозы за счёт АТФ. Удерживает молекулу в клетке.', props:['1 АТФ'] },
{ id:'f6p', label:'Фруктозо-6-Ф',formula:'C₆H₁₃O₉P', x:400, y:225, role:'inter', desc:'Изомер глюкозо-6-фосфата. Образуется при изомеризации ферментом фосфоглюкоизомеразой.', props:[] },
{ id:'f16bp', label:'Фруктозо-1,6-бФ',formula:'C₆H₁₄O₁₂P₂', x:400, y:310, role:'key', desc:'Фруктозо-1,6-бисфосфат — ключевой регуляторный метаболит. Образование катализирует фосфофруктокиназа-1 (ФФК-1).', props:['1 АТФ', 'Контроль скорости'] },
{ id:'dhap', label:'ДГАФ', formula:'C₃H₇O₆P', x:260, y:395, role:'inter', desc:'Дигидроксиацетонфосфат — один из двух триозофосфатов при расщеплении фруктозо-1,6-бисфосфата. Быстро конвертируется в ГАФ.', props:[] },
{ id:'gap', label:'ГАФ', formula:'C₃H₇O₆P', x:540, y:395, role:'inter', desc:'Глицеральдегид-3-фосфат (ГАФ) — непосредственный субстрат следующих реакций. Оба триозофосфата канализируются через ГАФ.', props:[] },
{ id:'bpg', label:'1,3-бФГ', formula:'C₃H₈O₁₀P₂',x:540, y:480, role:'inter', desc:'1,3-бисфосфоглицерат. Образуется при окислении ГАФ, сопряжённом с восстановлением НАД⁺ в НАДН.', props:['2 НАДН'] },
{ id:'pg3', label:'3-ФГК', formula:'C₃H₇O₇P', x:540, y:560, role:'inter', desc:'3-фосфоглицерат. Образуется при субстратном фосфорилировании АДФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> АТФ ферментом фосфоглицераткиназой.', props:['+2 АТФ'] },
{ id:'pg2', label:'2-ФГК', formula:'C₃H₇O₇P', x:540, y:635, role:'inter', desc:'2-фосфоглицерат. Образуется при перемещении фосфатной группы с 3 на 2 положение.', props:[] },
{ id:'pep', label:'ФЕП', formula:'C₃H₅O₆P', x:540, y:710, role:'inter', desc:'Фосфоенолпируват (ФЕП) — высокоэнергетический промежуточный продукт. Образуется при дегидратации 2-ФГК.', props:[] },
{ id:'pyr', label:'Пируват', formula:'C₃H₄O₃', x:400, y:795, role:'product', desc:'Конечный продукт гликолиза. В аэробных условиях переходит в ацетил-КоА (цикл Кребса). В анаэробных <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> лактат или этанол.', props:['+2 АТФ', '2 молекулы'] },
],
edges: [
{ from:'glc', to:'g6p', enzyme:'Гексокиназа', co:'-АТФ', curveX:0 },
{ from:'g6p', to:'f6p', enzyme:'ФГИ', curveX:0 },
{ from:'f6p', to:'f16bp', enzyme:'ФФК-1', co:'-АТФ', curveX:0 },
{ from:'f16bp',to:'dhap', enzyme:'Альдолаза', curveX:0 },
{ from:'f16bp',to:'gap', enzyme:'Альдолаза', curveX:0 },
{ from:'dhap', to:'gap', enzyme:'ТФИ', curveX:0 },
{ from:'gap', to:'bpg', enzyme:'ГАФДГ', co:'+НАДН', curveX:0 },
{ from:'bpg', to:'pg3', enzyme:'ФГК', co:'+АТФ', curveX:0 },
{ from:'pg3', to:'pg2', enzyme:'Фосфоглицератмутаза', curveX:0 },
{ from:'pg2', to:'pep', enzyme:'Енолаза', curveX:0 },
{ from:'pep', to:'pyr', enzyme:'Пируваткиназа', co:'+АТФ', curveX:0 },
],
steps: [
{
title:'Фосфорилирование глюкозы',
mol:'g6p',
desc:'Гексокиназа катализирует перенос фосфатной группы с АТФ на глюкозу, образуя глюкозо-6-фосфат (Г6Ф). Реакция необратима и «ловит» глюкозу в клетке.',
energy:[{label:'-1 АТФ', cls:'atp-used'}],
quiz:{ q:'Зачем глюкозу фосфорилируют в первой реакции?', opts:['Для выхода из клетки','Чтобы удержать глюкозу в клетке','Для образования НАДН','Для расщепления кольца'], ans:1 }
},
{
title:'Изомеризация',
mol:'f6p',
desc:'Фосфоглюкоизомераза превращает Г6Ф в фруктозо-6-фосфат (Ф6Ф). Реакция обратима и перестраивает альдозный сахар в кетозный.',
energy:[],
quiz:{ q:'Какой фермент катализирует изомеризацию Г6Ф <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Ф6Ф?', opts:['Гексокиназа','Альдолаза','Фосфоглюкоизомераза','Пируваткиназа'], ans:2 }
},
{
title:'Ключевой контрольный шаг',
mol:'f16bp',
desc:'Фосфофруктокиназа-1 (ФФК-1) фосфорилирует Ф6Ф <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> фруктозо-1,6-бисфосфат. Это необратимая реакция — главный регуляторный пункт гликолиза. АТФ ингибирует, АМФ/АДФ активирует.',
energy:[{label:'-1 АТФ', cls:'atp-used'}],
quiz:{ q:'Что является главным аллостерическим активатором ФФК-1?', opts:['АТФ','АМФ','НАДН','Пируват'], ans:1 }
},
{
title:'Расщепление на триозы',
mol:'gap',
desc:'Альдолаза расщепляет фруктозо-1,6-бисфосфат на два триозофосфата: ДГАФ и ГАФ (глицеральдегид-3-фосфат). Триозофосфатизомераза быстро конвертирует ДГАФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ГАФ.',
energy:[],
quiz:{ q:'Сколько молекул ГАФ образуется из одной глюкозы?', opts:['1','2','3','4'], ans:1 }
},
{
title:'Окислительное фосфорилирование',
mol:'bpg',
desc:'ГАФДГ окисляет ГАФ и присоединяет неорганический фосфат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 1,3-бисфосфоглицерат. Сопряжено с восстановлением НАД⁺ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> НАДН. Реакция субстратного фосфорилирования.',
energy:[{label:'+2 НАДН', cls:'nadh'}],
quiz:{ q:'Чем восстанавливается НАД⁺ в этой реакции?', opts:['ГАФ','Пируват','ДГАФ','АТФ'], ans:0 }
},
{
title:'Первая выработка АТФ',
mol:'pg3',
desc:'Фосфоглицераткиназа переносит фосфат с 1,3-бФГ на АДФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> АТФ. Это субстратное фосфорилирование — первый синтез АТФ в гликолизе. С каждой глюкозы получаем 2 АТФ.',
energy:[{label:'+2 АТФ', cls:'atp-prod'}],
quiz:{ q:'Как называется тип синтеза АТФ в этой реакции?', opts:['Окислительное фосфорилирование','Субстратное фосфорилирование','Фотофосфорилирование','Трансфосфорилирование'], ans:1 }
},
{
title:'Мутация фосфатной группы',
mol:'pg2',
desc:'Фосфоглицератмутаза перемещает фосфатную группу с 3-го на 2-е углеродное положение, подготавливая молекулу к дегидратации.',
energy:[],
quiz:{ q:'Какой продукт образуется из 3-ФГК под действием мутазы?', opts:['ФЕП','2-ФГК','Пируват','1,3-бФГ'], ans:1 }
},
{
title:'Образование ФЕП',
mol:'pep',
desc:'Енолаза катализирует дегидратацию 2-фосфоглицерата <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> фосфоенолпируват (ФЕП). ФЕП — высокоэнергетический соединение с большой отрицательной ΔG° гидролиза фосфата.',
energy:[],
quiz:{ q:'Почему ФЕП называют «высокоэнергетическим»?', opts:['Содержит много атомов С','Большая ΔG° гидролиза фосфатной связи','Растворяется в жирах','Содержит двойную связь'], ans:1 }
},
{
title:'Финальная реакция — пируват',
mol:'pyr',
desc:'Пируваткиназа переносит фосфат с ФЕП на АДФ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> АТФ + пируват. Необратимая реакция. Итог: из 1 глюкозы 2 пирувата, 2 НАДН, +2 АТФ нетто.',
energy:[{label:'+2 АТФ', cls:'atp-prod'},{label:'2 пируват', cls:'co2'}],
quiz:{ q:'Каков нетто-выход АТФ на 1 молекулу глюкозы в гликолизе?', opts:['1','2','4','36'], ans:1 }
},
]
},
krebs: {
name: 'Цикл Кребса',
color: '#06b6d4',
colorRgb: '6,182,212',
desc: '8 реакций окисления ацетил-КоА. Происходит в матриксе митохондрий. Выход на 1 оборот: 3 НАДН, 1 ФАДН₂, 1 ГТФ, 2 СО₂.',
stats: [
{ label: '3 НАДН / оборот', cls: 'nadh' },
{ label: '2 CO₂', cls: 'co2' },
{ label: '1 ГТФ', cls: 'atp' },
],
legend: [
{ color: '#06b6d4', type: 'circle', label: 'Промежуточный метаболит' },
{ color: '#06b6d4', type: 'line', label: 'Реакция цикла' },
],
nodes: [
{ id:'acetcoa', label:'Ацетил-КоА', formula:'CH₃CO-SCoA',x:440, y:80, role:'substrate', desc:'Активированный ацетат. Образуется из пирувата (гликолиз), жирных кислот (β-окисление) и аминокислот.', props:['Входит в цикл'] },
{ id:'oaa', label:'ОАА', formula:'C₄H₄O₅', x:260, y:140, role:'key', desc:'Оксалоацетат — акцептор ацетил-КоА. Регенерируется в каждом обороте цикла. Ключевой анаплеротический метаболит.', props:['Акцептор'] },
{ id:'cit', label:'Цитрат', formula:'C₆H₈O₇', x:160, y:260, role:'inter', desc:'Цитрат — первый продукт цикла. Синтезируется цитратсинтазой из ацетил-КоА и ОАА.', props:[] },
{ id:'isocit', label:'Изоцитрат', formula:'C₆H₈O₇', x:120, y:390, role:'inter', desc:'Изоцитрат — изомер цитрата. Субстрат изоцитратдегидрогеназы — ключевого регуляторного фермента.', props:[] },
{ id:'akg', label:'α-КГ', formula:'C₅H₆O₅', x:160, y:520, role:'inter', desc:'α-кетоглутарат (α-КГ). Образуется при окислительном декарбоксилировании изоцитрата. Выделяется CO₂.', props:['CO₂','+НАДН'] },
{ id:'succoa', label:'Сукцинил-КоА',formula:'C₅H₆O₃S', x:300, y:620, role:'inter', desc:'Сукцинил-КоА — высокоэнергетический тиоэфир. Образуется при окислительном декарбоксилировании α-КГ.', props:['+НАДН','+ГТФ','-CO₂'] },
{ id:'succ', label:'Сукцинат', formula:'C₄H₆O₄', x:480, y:620, role:'inter', desc:'Сукцинат. Окисляется сукцинатдегидрогеназой (СДГ) — единственным мембранным ферментом цикла.', props:['+ФАДН₂'] },
{ id:'fum', label:'Фумарат', formula:'C₄H₄O₄', x:620, y:520, role:'inter', desc:'Фумарат — транс-изомер. Образуется при окислении сукцината. Гидратируется фумаразой.', props:[] },
{ id:'mal', label:'Малат', formula:'C₄H₆O₅', x:660, y:390, role:'inter', desc:'Малат (яблочная кислота). Образуется при гидратации фумарата. Окисляется малатдегидрогеназой.', props:['+НАДН'] },
],
edges: [
{ from:'acetcoa',to:'cit', enzyme:'Цитратсинтаза', co:'+ОАА', curveX:0 },
{ from:'oaa', to:'cit', enzyme:'', curveX:0 },
{ from:'cit', to:'isocit', enzyme:'Аконитаза', curveX:0 },
{ from:'isocit', to:'akg', enzyme:'ИзоцитратДГ', co:'+НАДН,-CO₂', curveX:0 },
{ from:'akg', to:'succoa', enzyme:'α-КГДГ-комплекс', co:'+НАДН,-CO₂', curveX:0 },
{ from:'succoa', to:'succ', enzyme:'Сукцинил-КоА-синтетаза', co:'+ГТФ', curveX:0 },
{ from:'succ', to:'fum', enzyme:'Сукцинатдегидрогеназа', co:'+ФАДН₂', curveX:0 },
{ from:'fum', to:'mal', enzyme:'Фумараза', curveX:0 },
{ from:'mal', to:'oaa', enzyme:'МалатДГ', co:'+НАДН', curveX:0 },
],
steps: [
{ title:'Конденсация с ОАА', mol:'cit', desc:'Цитратсинтаза присоединяет ацетил-КоА (2C) к оксалоацетату (4C) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> цитрат (6C). Это необратимая реакция, запускающая цикл.', energy:[], quiz:{q:'Сколько углеродов в цитрате?', opts:['2','4','6','8'], ans:2} },
{ title:'Изомеризация цитрата', mol:'isocit', desc:'Аконитаза через промежуточный цис-аконитат превращает цитрат в изоцитрат. Реакция обратима, равновесие сдвинуто в сторону цитрата.', energy:[], quiz:{q:'Какой фермент изомеризует цитрат?', opts:['Фумараза','Аконитаза','Малатдегидрогеназа','Цитратсинтаза'], ans:1} },
{ title:'Первое окислительное декарбоксилирование', mol:'akg', desc:'Изоцитратдегидрогеназа окисляет изоцитрат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> α-кетоглутарат с выделением CO₂ и НАДН. Ключевой регуляторный шаг — активируется изоцитратом, ингибируется НАДН.', energy:[{label:'+НАДН',cls:'nadh'},{label:'-CO₂',cls:'co2'}], quiz:{q:'Сколько углеродов в α-кетоглутарате?', opts:['2','4','5','6'], ans:2} },
{ title:'Второе окислительное декарбоксилирование', mol:'succoa', desc:'α-кетоглутаратдегидрогеназный комплекс (аналог ПДК) окисляет α-КГ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> сукцинил-КоА. Выделяется ещё одна CO₂ и НАДН.', energy:[{label:'+НАДН',cls:'nadh'},{label:'-CO₂',cls:'co2'}], quiz:{q:'Чем структурно похож α-КГДК на пируватдегидрогеназный комплекс?', opts:['Использует ФАДН₂','Механизм окислительного декарбоксилирования с КоА','Находится в цитоплазме','Требует витамин К'], ans:1} },
{ title:'Субстратное фосфорилирование', mol:'succ', desc:'Сукцинил-КоА-синтетаза расщепляет тиоэфирную связь сукцинил-КоА, сопрягая это с синтезом ГТФ (или АТФ). Единственная реакция субстратного фосфорилирования в цикле.', energy:[{label:'+ГТФ',cls:'atp-prod'}], quiz:{q:'Что синтезируется при реакции сукцинил-КоА-синтетазы?', opts:['НАДН','ФАДН₂','ГТФ','CO₂'], ans:2} },
{ title:'Окисление сукцината', mol:'fum', desc:'Сукцинатдегидрогеназа (комплекс II дыхательной цепи) окисляет сукцинат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> фумарат, восстанавливая ФАД <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ФАДН₂.', energy:[{label:'+ФАДН₂',cls:'fadh2'}], quiz:{q:'К какому комплексу дыхательной цепи относится СДГ?', opts:['Комплекс I','Комплекс II','Комплекс III','АТФ-синтаза'], ans:1} },
{ title:'Гидратация фумарата', mol:'mal', desc:'Фумараза присоединяет воду к фумарату <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> L-малат. Реакция стереоспецифична — образуется только L-изомер.', energy:[], quiz:{q:'Что присоединяется к фумарату в этой реакции?', opts:['CO₂','АТФ','H₂O','НАДН'], ans:2} },
{ title:'Регенерация ОАА', mol:'oaa', desc:'Малатдегидрогеназа окисляет малат <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> оксалоацетат с образованием НАДН. Регенерируется акцептор для следующего оборота цикла.', energy:[{label:'+НАДН',cls:'nadh'}], quiz:{q:'Сколько оборотов цикла Кребса нужно на 1 молекулу глюкозы?', opts:['1','2','4','10'], ans:1} },
]
},
oxidation: {
name: 'β-Окисление',
color: '#fb923c',
colorRgb: '251,146,60',
desc: 'Повторяющиеся циклы окисления жирных кислот в митохондриях. Каждый цикл отщепляет 2C в виде ацетил-КоА и выделяет 1 НАДН + 1 ФАДН₂.',
stats: [
{ label: '+НАДН / цикл', cls: 'nadh' },
{ label: '+ФАДН₂', cls: 'nadh' },
{ label: '+Ацетил-КоА', cls: 'atp' },
],
legend: [
{ color: '#fb923c', type: 'circle', label: 'Промежуточный продукт' },
{ color: '#fb923c', type: 'line', label: 'Реакция β-окисления' },
],
nodes: [
{ id:'fac', label:'Жирная к-та',formula:'R-COOH', x:400, y:60, role:'substrate', desc:'Свободная жирная кислота (напр. пальмитиновая C₁₆). Активируется в ацил-КоА перед входом в митохондрии.', props:[] },
{ id:'acylcoa', label:'Ацил-КоА', formula:'R-CO-SCoA', x:400, y:150, role:'key', desc:'Активированная жирная кислота. Образуется при участии ацил-КоА-синтетазы за счёт АТФ (<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>АМФ+PPi). Не проходит через мембрану — транспортируется как карнитиновый эфир.', props:['-АТФ (<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>АМФ)'] },
{ id:'enoylcoa',label:'Транс-еноил-КоА',formula:'R-CH=CH-CO-SCoA',x:400,y:250,role:'inter',desc:'Транс-Δ²-еноил-КоА. Образуется при ФАД-зависимом окислении ацил-КоА ацил-КоА-дегидрогеназой.', props:['+ФАДН₂'] },
{ id:'hydroxy', label:'L-β-гидрокси-КоА',formula:'R-CHOH-CH₂-CO-SCoA',x:400,y:345,role:'inter',desc:'L-β-гидроксиацил-КоА. Образуется при гидратации двойной связи еноил-КоА гидратазой.', props:[] },
{ id:'ketoacoa',label:'β-кето-КоА', formula:'R-CO-CH₂-CO-SCoA',x:400,y:440,role:'inter',desc:'β-кетоацил-КоА. Образуется при НАД⁺-зависимом окислении L-β-гидроксиацил-КоА.', props:['+НАДН'] },
{ id:'newacyl', label:'Ацил-КоА (2C)',formula:'R\'—CO-SCoA', x:240, y:540, role:'inter', desc:'Укороченный на 2 углерода ацил-КоА. Возвращается на начало цикла β-окисления.', props:['Следующий цикл'] },
{ id:'acetcoa2',label:'Ацетил-КоА', formula:'CH₃CO-SCoA', x:560, y:540, role:'product', desc:'Ацетил-КоА — входит в цикл Кребса. Из пальмитиновой кислоты (C₁₆) образуется 8 ацетил-КоА за 7 циклов β-окисления.', props:['<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Цикл Кребса'] },
],
edges: [
{ from:'fac', to:'acylcoa', enzyme:'Ацил-КоА-синтетаза', co:'-АТФ', curveX:0 },
{ from:'acylcoa', to:'enoylcoa', enzyme:'Ацил-КоА-ДГ', co:'+ФАДН₂', curveX:0 },
{ from:'enoylcoa',to:'hydroxy', enzyme:'Еноил-КоА-гидратаза', curveX:0 },
{ from:'hydroxy', to:'ketoacoa', enzyme:'L-3-гидроксиацил-КоА-ДГ', co:'+НАДН', curveX:0 },
{ from:'ketoacoa',to:'newacyl', enzyme:'Тиолаза', curveX:0 },
{ from:'ketoacoa',to:'acetcoa2', enzyme:'Тиолаза', curveX:0 },
{ from:'newacyl', to:'acylcoa', enzyme:'Повтор цикла', curveX:-60 },
],
steps: [
{ title:'Активация жирной кислоты', mol:'acylcoa', desc:'Ацил-КоА-синтетаза присоединяет КоА к жирной кислоте, образуя ацил-КоА. Расходуется АТФ (<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>АМФ+PPi, что эквивалентно 2 АТФ). Это происходит в цитоплазме.', energy:[{label:'-2 АТФ',cls:'atp-used'}], quiz:{q:'Где происходит активация жирной кислоты в ацил-КоА?', opts:['В митохондриях','В ядре','В цитоплазме','В рибосомах'], ans:2} },
{ title:'ФАД-зависимое окисление', mol:'enoylcoa', desc:'Ацил-КоА-дегидрогеназа окисляет ацил-КоА, вводя двойную связь между α и β углеродами <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> транс-Δ²-еноил-КоА. ФАД восстанавливается до ФАДН₂.', energy:[{label:'+ФАДН₂',cls:'fadh2'}], quiz:{q:'Какой кофактор восстанавливается в первой реакции β-окисления?', opts:['НАД⁺','ФАД','ГТФ','КоА'], ans:1} },
{ title:'Гидратация двойной связи', mol:'hydroxy', desc:'Еноил-КоА-гидратаза присоединяет воду по двойной связи <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> L-β-гидроксиацил-КоА. Реакция стереоспецифична.', energy:[], quiz:{q:'Что присоединяется в реакции гидратации еноил-КоА?', opts:['CO₂','O₂','H₂O','НАД⁺'], ans:2} },
{ title:'НАД⁺-зависимое окисление', mol:'ketoacoa', desc:'L-3-гидроксиацил-КоА-дегидрогеназа окисляет гидроксильную группу <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> кетогруппу, восстанавливая НАД⁺ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> НАДН.', energy:[{label:'+НАДН',cls:'nadh'}], quiz:{q:'Какая группа окисляется в этой реакции?', opts:['Карбоксильная','Аминогруппа','Гидроксильная','Метильная'], ans:2} },
{ title:'Тиолитическое расщепление', mol:'acetcoa2', desc:'Тиолаза расщепляет β-кетоацил-КоА присоединением КоА <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ацетил-КоА (2C) + укороченный ацил-КоА. Цикл повторяется.', energy:[{label:'+Ацетил-КоА',cls:'atp-prod'}], quiz:{q:'Сколько ацетил-КоА образуется из пальмитиновой кислоты (C16)?', opts:['4','6','7','8'], ans:3} },
]
},
synthesis: {
name: 'Синтез белка',
color: '#a78bfa',
colorRgb: '167,139,250',
desc: 'Трансляция — считывание мРНК рибосомой и полимеризация аминокислот в полипептидную цепь.',
stats: [
{ label: '~2 ГТФ / аминокислота', cls: 'atp' },
{ label: 'мРНК <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> белок', cls: 'nadh' },
],
legend: [
{ color: '#a78bfa', type: 'circle', label: 'Участник трансляции' },
{ color: '#a78bfa', type: 'line', label: 'Этап синтеза' },
],
nodes: [
{ id:'mrna', label:'мРНК', formula:'5-AUG…-3', x:400, y:60, role:'substrate', desc:'Матричная РНК — несёт генетическую информацию от ДНК к рибосоме в виде кодонов (триплетов нуклеотидов).', props:['Матрица'] },
{ id:'ribosome',label:'Рибосома', formula:'60S+40S', x:400, y:160, role:'key', desc:'Эукариотическая рибосома (80S). Состоит из малой (40S) и большой (60S) субъединиц. Имеет 3 сайта: A (аминоацильный), P (пептидильный), E (выход).', props:['A-P-E сайты'] },
{ id:'trna', label:'аминоацил-тРНК', formula:'aa-tRNA', x:230, y:260, role:'inter', desc:'тРНК с присоединённой аминокислотой. Распознаёт кодон мРНК через антикодон. Доставляется в A-сайт в комплексе с EF-Tu·ГТФ.', props:['-2 ГТФ'] },
{ id:'peptide', label:'Растущая цепь', formula:'...aa-aa-aa', x:560, y:260, role:'inter', desc:'Нарастающая полипептидная цепь в P-сайте. Пептидилтрансфераза (23S rRNA) катализирует образование пептидной связи.', props:['P-сайт'] },
{ id:'peptbond',label:'Пептидная связь',formula:'—CO—NH—', x:400, y:360, role:'inter', desc:'Образование пептидной связи катализируется рибозимом (23S rRNA) — пептидилтрансферазой. Выделяется тРНК из P-сайта.', props:[] },
{ id:'translo', label:'Транслокация', formula:'EF-G·ГТФ', x:400, y:460, role:'inter', desc:'Фактор EF-G (с ГТФ) сдвигает рибосому на 1 кодон (3 нт) в направлении 5<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>3. Освобождается Е-сайт. Расходуется ГТФ.', props:['-1 ГТФ'] },
{ id:'protein', label:'Белок', formula:'[полипептид]', x:400, y:560, role:'product', desc:'Готовый полипептид. Освобождается при встрече со стоп-кодоном (UAA, UAG, UGA) при участии факторов высвобождения RF1/RF2.', props:['Готовый продукт'] },
],
edges: [
{ from:'mrna', to:'ribosome', enzyme:'Инициация (eIF)', curveX:0 },
{ from:'ribosome',to:'trna', enzyme:'Декодирование', curveX:0 },
{ from:'trna', to:'peptbond', enzyme:'Пептидилтрансфераза', curveX:0 },
{ from:'peptide', to:'peptbond', enzyme:'', curveX:0 },
{ from:'peptbond',to:'translo', enzyme:'EF-G·ГТФ', curveX:0 },
{ from:'translo', to:'protein', enzyme:'Терминация (RF)', curveX:0 },
{ from:'translo', to:'ribosome', enzyme:'Следующий кодон', curveX:-70 },
],
steps: [
{ title:'Инициация', mol:'ribosome', desc:'Малая субъединица рибосомы распознаёт 5′-кэп мРНК при помощи факторов инициации (eIF4E/4G). Инициаторная Met-тРНК занимает P-сайт. Присоединяется большая субъединица.', energy:[{label:'-3 ГТФ',cls:'atp-used'}], quiz:{q:'Какой сайт занимает инициаторная Met-тРНК?', opts:['A-сайт','P-сайт','E-сайт','Все три'], ans:1} },
{ title:'Элонгация — доставка аа-тРНК', mol:'trna', desc:'EF-Tu·ГТФ доставляет аминоацил-тРНК в A-сайт. При правильном спаривании кодон–антикодон ГТФ гидролизуется, EF-Tu·ГДФ уходит.', energy:[{label:'-1 ГТФ',cls:'atp-used'}], quiz:{q:'Какой фактор доставляет аа-тРНК в А-сайт?', opts:['EF-G','EF-Tu','eIF2','RF1'], ans:1} },
{ title:'Пептидная связь', mol:'peptbond', desc:'Пептидилтрансфераза переносит пептидильную группу с P-сайта на аминогруппу в A-сайте, образуя пептидную связь. Энергия — из гидролиза аминоацильной связи тРНК.', energy:[], quiz:{q:'Что катализирует образование пептидной связи?', opts:['Белковый фермент','23S rRNA (рибозим)','ДНК-полимераза','АТФ-синтаза'], ans:1} },
{ title:'Транслокация', mol:'translo', desc:'EF-G·ГТФ сдвигает рибосому на 3 нуклеотида по мРНК. Цепь с тРНК перемещается из A <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> P, пустая тРНК из P <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> E и уходит. Расходуется ГТФ.', energy:[{label:'-1 ГТФ',cls:'atp-used'}], quiz:{q:'На сколько нуклеотидов сдвигается рибосома при транслокации?', opts:['1','2','3','4'], ans:2} },
{ title:'Терминация и высвобождение', mol:'protein', desc:'Стоп-кодон (UAA/UAG/UGA) распознаётся факторами высвобождения RF1/RF2. Пептидилтрансфераза гидролизует связь пептид-тРНК <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> белок освобождается. Рибосома диссоциирует.', energy:[], quiz:{q:'Сколько стоп-кодонов существует?', opts:['1','2','3','4'], ans:2} },
]
}
};
// ═══════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════
let currentPath = 'glycolysis';
let animOn = true;
let activeNode = null;
let learnStep = 0;
let learnMode = false;
let particles = [];
let animFrame = null;
let svgTranslate = { x: 0, y: 0 };
let svgScale = 1;
let dragState = null;
// ═══════════════════════════════════════════════════════
// PATH SELECTION & RENDER
// ═══════════════════════════════════════════════════════
function selectPath(id) {
currentPath = id;
document.querySelectorAll('.path-chip').forEach(c => c.classList.toggle('active', c.dataset.path === id));
stopLearnMode();
renderPath();
renderPathInfo();
}
function renderPath() {
const pd = PATHWAYS[currentPath];
const area = document.getElementById('svg-area');
const W = area.clientWidth || 800;
const H = area.clientHeight || 600;
// auto-fit: find bounding box of nodes
const xs = pd.nodes.map(n => n.x), ys = pd.nodes.map(n => n.y);
const minX = Math.min(...xs) - 60, minY = Math.min(...ys) - 60;
const maxX = Math.max(...xs) + 60, maxY = Math.max(...ys) + 60;
const scaleX = W / (maxX - minX), scaleY = H / (maxY - minY);
svgScale = Math.min(scaleX, scaleY, 1.2) * 0.9;
svgTranslate.x = (W - (maxX - minX) * svgScale) / 2 - minX * svgScale;
svgTranslate.y = (H - (maxY - minY) * svgScale) / 2 - minY * svgScale;
applyTransform();
renderEdges(pd);
renderNodes(pd);
renderStats(pd);
renderLegend(pd);
stopParticles();
if (animOn) startParticles(pd);
}
function applyTransform() {
const g = document.getElementById('svg-root');
g.setAttribute('transform', `translate(${svgTranslate.x},${svgTranslate.y}) scale(${svgScale})`);
}
function renderEdges(pd) {
const layer = document.getElementById('edges-layer');
layer.innerHTML = '';
const color = pd.color;
const marker = `url(#arrowhead)`;
for (const e of pd.edges) {
const fn = pd.nodes.find(n => n.id === e.from);
const tn = pd.nodes.find(n => n.id === e.to);
if (!fn || !tn) continue;
const x1 = fn.x, y1 = fn.y, x2 = tn.x, y2 = tn.y;
const dx = x2 - x1, dy = y2 - y1;
const len = Math.hypot(dx, dy) || 1;
// shorten to edge of node circle (r=28)
const R = 28;
const sx = x1 + dx/len * R, sy = y1 + dy/len * R;
const ex = x2 - dx/len * R, ey = y2 - dy/len * R;
let pathD;
if (e.curveX && e.curveX !== 0) {
// quadratic curve
const mx = (x1+x2)/2 + e.curveX, my = (y1+y2)/2;
pathD = `M${sx},${sy} Q${mx},${my} ${ex},${ey}`;
} else {
pathD = `M${sx},${sy} L${ex},${ey}`;
}
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathD);
path.setAttribute('stroke', `rgba(${pd.colorRgb},0.45)`);
path.setAttribute('stroke-width', '1.8');
path.setAttribute('fill', 'none');
path.setAttribute('marker-end', marker);
path.setAttribute('data-edge-id', `${e.from}-${e.to}`);
layer.appendChild(path);
// enzyme label
if (e.enzyme) {
const midT = 0.5;
let lx, ly;
if (e.curveX) {
const mx = (x1+x2)/2 + e.curveX, my = (y1+y2)/2;
lx = (1-midT)*(1-midT)*sx + 2*(1-midT)*midT*mx + midT*midT*ex;
ly = (1-midT)*(1-midT)*sy + 2*(1-midT)*midT*my + midT*midT*ey - 12;
} else {
lx = (sx+ex)/2 + (-dy/len)*12;
ly = (sy+ey)/2 + (dx/len)*12;
}
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
txt.setAttribute('x', lx);
txt.setAttribute('y', ly);
txt.setAttribute('class', 'edge-enzyme');
txt.textContent = e.enzyme;
layer.appendChild(txt);
}
// co-factor label
if (e.co) {
const nx = -dy/len, ny = dx/len;
const lx = (sx+ex)/2 - nx*13, ly = (sy+ey)/2 - ny*13;
const txt2 = document.createElementNS('http://www.w3.org/2000/svg', 'text');
txt2.setAttribute('x', lx);
txt2.setAttribute('y', ly);
txt2.setAttribute('class', 'edge-label');
txt2.setAttribute('fill', `rgba(${pd.colorRgb},0.7)`);
txt2.textContent = e.co;
layer.appendChild(txt2);
}
}
}
function renderNodes(pd) {
const layer = document.getElementById('nodes-layer');
layer.innerHTML = '';
const roleColor = {
substrate: { fill: '#1a2e20', stroke: '#34d399' },
key: { fill: '#1e1a2e', stroke: '#a78bfa' },
inter: { fill: '#111128', stroke: `rgba(${pd.colorRgb},0.7)` },
product: { fill: '#1a2a20', stroke: '#4ade80' },
};
for (const n of pd.nodes) {
const rc = roleColor[n.role] || roleColor.inter;
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('class', 'mol-node' + (activeNode === n.id ? ' active' : ''));
g.setAttribute('data-node-id', n.id);
g.setAttribute('transform', `translate(${n.x},${n.y})`);
g.style.cursor = 'pointer';
g.addEventListener('click', () => clickNode(n.id));
// Outer glow circle
const glow = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
glow.setAttribute('r', '34');
glow.setAttribute('fill', 'none');
glow.setAttribute('stroke', rc.stroke);
glow.setAttribute('stroke-width', '0.5');
glow.setAttribute('opacity', '0.2');
g.appendChild(glow);
// Main circle
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('class', 'node-circle');
circle.setAttribute('r', n.role === 'key' ? '30' : '27');
circle.setAttribute('fill', rc.fill);
circle.setAttribute('stroke', rc.stroke);
circle.setAttribute('stroke-width', activeNode === n.id ? '2.5' : '1.8');
g.appendChild(circle);
// Label
const lines = n.label.split(' ');
if (lines.length === 1) {
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
t.setAttribute('class', 'node-label');
t.setAttribute('y', '0');
t.textContent = n.label;
g.appendChild(t);
// formula
const f = document.createElementNS('http://www.w3.org/2000/svg', 'text');
f.setAttribute('class', 'node-formula');
f.setAttribute('y', '13');
f.textContent = n.formula;
g.appendChild(f);
} else {
lines.forEach((ln, i) => {
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
t.setAttribute('class', 'node-label');
t.setAttribute('y', (i - (lines.length-1)/2) * 13 + '');
t.setAttribute('font-size', '9.5');
t.textContent = ln;
g.appendChild(t);
});
}
layer.appendChild(g);
}
}
function renderStats(pd) {
const el = document.getElementById('path-stats');
el.innerHTML = pd.stats.map(s => `<div class="pstat ${s.cls}">${s.label}</div>`).join('');
}
function renderLegend(pd) {
const el = document.getElementById('legend-items');
el.innerHTML = pd.legend.map(l => {
if (l.type === 'circle') return `<div class="legend-item"><div class="legend-dot" style="background:${l.color}"></div>${l.label}</div>`;
if (l.type === 'circle-sm') return `<div class="legend-item"><div class="legend-dot" style="background:${l.color};width:5px;height:5px;opacity:.6"></div>${l.label}</div>`;
return `<div class="legend-item"><div class="legend-line" style="background:${l.color}"></div>${l.label}</div>`;
}).join('');
}
function renderPathInfo() {
const pd = PATHWAYS[currentPath];
document.getElementById('path-info-content').innerHTML = `
<div style="font-family:'Unbounded',sans-serif;font-size:.85rem;font-weight:800;color:#e0e0f0;margin-bottom:8px">${pd.name}</div>
<div style="font-family:'Manrope',sans-serif;font-size:.8rem;color:#aaa;line-height:1.6;margin-bottom:14px">${pd.desc}</div>
<div style="font-family:'Manrope',sans-serif;font-size:.72rem;font-weight:700;color:#444;text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px">Выход энергии</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px">
${pd.stats.map(s=>`<span class="pstat ${s.cls}" style="position:static;font-size:.75rem">${s.label}</span>`).join('')}
</div>
<div style="font-family:'Manrope',sans-serif;font-size:.72rem;font-weight:700;color:#444;text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px">Ключевые молекулы (${pd.nodes.length})</div>
${pd.nodes.map(n=>`
<div style="display:flex;gap:8px;align-items:center;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer" onclick="clickNode('${n.id}');switchTab('mol')">
<div style="width:8px;height:8px;border-radius:50%;background:${pd.color};flex-shrink:0"></div>
<div>
<div style="font-family:'Manrope',sans-serif;font-size:.78rem;font-weight:700;color:#ccc">${n.label}</div>
<div style="font-family:'Manrope',sans-serif;font-size:.72rem;color:#555">${n.formula}</div>
</div>
</div>`).join('')}
`;
}
// ═══════════════════════════════════════════════════════
// PARTICLE ANIMATION
// ═══════════════════════════════════════════════════════
function startParticles(pd) {
const layer = document.getElementById('particles-layer');
layer.innerHTML = '';
particles = [];
for (const e of pd.edges) {
const fn = pd.nodes.find(n => n.id === e.from);
const tn = pd.nodes.find(n => n.id === e.to);
if (!fn || !tn) continue;
// stagger start
const delay = Math.random() * 2000;
setTimeout(() => spawnParticle(pd, e, fn, tn), delay);
}
}
function spawnParticle(pd, edge, fn, tn) {
if (!animOn) return;
const layer = document.getElementById('particles-layer');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('r', '4');
circle.setAttribute('fill', pd.color);
circle.setAttribute('opacity', '0.9');
circle.setAttribute('class', 'flow-particle');
layer.appendChild(circle);
const duration = 1400 + Math.random() * 600;
const start = performance.now();
const R = 28;
const dx0 = tn.x - fn.x, dy0 = tn.y - fn.y;
const len0 = Math.hypot(dx0, dy0) || 1;
const sx = fn.x + dx0/len0 * R, sy = fn.y + dy0/len0 * R;
const ex = tn.x - dx0/len0 * R, ey = tn.y - dy0/len0 * R;
function frame(now) {
if (!animOn) { circle.remove(); return; }
const t = Math.min((now - start) / duration, 1);
const ease = t < 0.5 ? 2*t*t : -1+(4-2*t)*t;
let px, py;
if (edge.curveX) {
const mx = (fn.x+tn.x)/2 + edge.curveX, my = (fn.y+tn.y)/2;
px = (1-ease)*(1-ease)*sx + 2*(1-ease)*ease*mx + ease*ease*ex;
py = (1-ease)*(1-ease)*sy + 2*(1-ease)*ease*my + ease*ease*ey;
} else {
px = sx + (ex-sx)*ease;
py = sy + (ey-sy)*ease;
}
circle.setAttribute('cx', px);
circle.setAttribute('cy', py);
circle.setAttribute('opacity', t < 0.1 ? t*9 : t > 0.85 ? (1-t)/0.15 : 0.9);
if (t < 1) {
requestAnimationFrame(frame);
} else {
circle.remove();
// respawn after pause
if (animOn) setTimeout(() => spawnParticle(pd, edge, fn, tn), 800 + Math.random() * 1200);
}
}
requestAnimationFrame(frame);
}
function stopParticles() {
animOn = false;
document.getElementById('particles-layer').innerHTML = '';
}
function toggleAnimation() {
animOn = !animOn;
const btn = document.getElementById('anim-toggle');
btn.classList.toggle('on', animOn);
btn.innerHTML = `<i data-lucide="${animOn ? 'play-circle' : 'pause-circle'}" style="width:14px;height:14px"></i> Анимація`;
btn.innerHTML = `<i data-lucide="${animOn ? 'play-circle' : 'pause-circle'}" style="width:14px;height:14px"></i> Анимация`;
if (window.lucide) lucide.createIcons();
if (animOn) startParticles(PATHWAYS[currentPath]);
else document.getElementById('particles-layer').innerHTML = '';
}
// ═══════════════════════════════════════════════════════
// NODE CLICK <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> SIDE PANEL
// ═══════════════════════════════════════════════════════
function clickNode(id) {
activeNode = id;
// re-render nodes to update active state
renderNodes(PATHWAYS[currentPath]);
const pd = PATHWAYS[currentPath];
const n = pd.nodes.find(x => x.id === id);
if (!n) return;
document.getElementById('mol-empty').style.display = 'none';
document.getElementById('mol-card').classList.add('show');
document.getElementById('mc-name').textContent = n.label;
document.getElementById('mc-formula').textContent = n.formula;
document.getElementById('mc-desc').textContent = n.desc;
document.getElementById('mc-props').innerHTML = (n.props||[]).map(p => {
let cls = 'key';
if (p.includes('АТФ') || p.includes('ГТФ')) cls = p.startsWith('+') ? 'atp' : 'atp';
if (p.includes('НАДН') || p.includes('ФАДН')) cls = 'nadh';
if (p.includes('CO₂')) cls = 'co2';
return `<span class="mol-prop ${cls}">${p}</span>`;
}).join('');
document.getElementById('mc-link').href = '/biochem-library';
document.getElementById('mc-link').innerHTML = 'Найти в библиотеке <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>';
switchTab('mol');
}
// ═══════════════════════════════════════════════════════
// LEARN MODE
// ═══════════════════════════════════════════════════════
function startLearn() {
learnMode = true;
learnStep = 0;
switchTab('learn');
document.getElementById('learn-inactive').style.display = 'none';
renderLearnStep();
}
function stopLearnMode() {
learnMode = false;
document.getElementById('learn-inactive').style.display = 'block';
document.getElementById('learn-active').innerHTML = '';
}
let quizAnswered = false;
function renderLearnStep() {
const pd = PATHWAYS[currentPath];
const steps = pd.steps;
if (learnStep >= steps.length) { renderLearnComplete(); return; }
const s = steps[learnStep];
const pct = Math.round((learnStep / steps.length) * 100);
quizAnswered = false;
// highlight node
activeNode = s.mol;
renderNodes(pd);
// scroll to node (pan)
const n = pd.nodes.find(x => x.id === s.mol);
if (n) {
const area = document.getElementById('svg-area');
const W = area.clientWidth, H = area.clientHeight;
svgTranslate.x = W/2 - n.x * svgScale;
svgTranslate.y = H/2 - n.y * svgScale;
applyTransform();
}
const html = `
<div class="learn-progress-bar"><div class="learn-progress-fill" style="width:${pct}%"></div></div>
<div class="step-counter">Шаг ${learnStep+1} из ${steps.length}</div>
<div class="step-title-row">
<span class="step-num">${learnStep+1}</span>
<span class="step-title">${s.title}</span>
</div>
<div class="step-desc">${s.desc}</div>
${s.energy.length ? `<div class="step-energy">${s.energy.map(e=>`<span class="energy-badge ${e.cls}">${e.label}</span>`).join('')}</div>` : ''}
<div class="mini-quiz">
<div class="quiz-q">${s.quiz.q}</div>
<div class="quiz-opts">
${s.quiz.opts.map((o,i)=>`<button class="quiz-opt" id="qopt-${i}" onclick="answerQuiz(${i},${s.quiz.ans})">${o}</button>`).join('')}
</div>
<div id="quiz-fb" style="display:none" class="quiz-feedback"></div>
</div>
<div class="step-nav">
<button class="step-btn prev" onclick="stepNav(-1)" ${learnStep===0?'disabled':''}><svg class="ic" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> Назад</button>
<button class="step-btn next" id="step-next-btn" onclick="stepNav(1)" disabled>
${learnStep===steps.length-1 ? 'Завершить <svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : 'Далее <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>'}
</button>
</div>
`;
document.getElementById('learn-active').innerHTML = html;
}
function answerQuiz(chosen, correct) {
if (quizAnswered) return;
quizAnswered = true;
const ok = chosen === correct;
const fb = document.getElementById('quiz-fb');
fb.style.display = '';
fb.className = 'quiz-feedback ' + (ok ? 'ok' : 'err');
fb.innerHTML = ok ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg> Верно!' : `<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Неверно. Правильный ответ: "${PATHWAYS[currentPath].steps[learnStep].quiz.opts[correct]}"`;
document.querySelectorAll('.quiz-opt').forEach((el, i) => {
el.disabled = true;
if (i === correct) el.classList.add('correct');
else if (i === chosen && !ok) el.classList.add('wrong');
});
document.getElementById('step-next-btn').disabled = false;
}
function stepNav(dir) {
learnStep += dir;
if (learnStep < 0) learnStep = 0;
renderLearnStep();
}
function renderLearnComplete() {
activeNode = null;
renderNodes(PATHWAYS[currentPath]);
document.getElementById('learn-active').innerHTML = `
<div class="learn-complete">
<div class="complete-icon"><svg class="ic" viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg></div>
<div class="complete-title">${PATHWAYS[currentPath].name} пройден!</div>
<div class="complete-text" style="margin-bottom:16px">Вы изучили все ${PATHWAYS[currentPath].steps.length} шагов пути.</div>
<button class="learn-btn" style="display:inline-flex;margin:0 auto" onclick="startLearn()">Повторить</button>
</div>
`;
}
// ═══════════════════════════════════════════════════════
// TABS
// ═══════════════════════════════════════════════════════
function switchTab(tab) {
['mol','learn','info'].forEach(t => {
document.getElementById('tab-'+t).classList.toggle('active', t===tab);
document.getElementById('panel-'+t).style.display = t===tab ? '' : 'none';
});
}
// ═══════════════════════════════════════════════════════
// PAN / ZOOM
// ═══════════════════════════════════════════════════════
function zoom(factor) {
svgScale *= factor;
applyTransform();
}
function resetView() {
renderPath();
}
const svgArea = document.getElementById('svg-area');
svgArea.addEventListener('mousedown', e => {
if (e.target.closest('.mol-node')) return;
dragState = { sx: e.clientX, sy: e.clientY, tx: svgTranslate.x, ty: svgTranslate.y };
});
window.addEventListener('mousemove', e => {
if (!dragState) return;
svgTranslate.x = dragState.tx + (e.clientX - dragState.sx);
svgTranslate.y = dragState.ty + (e.clientY - dragState.sy);
applyTransform();
});
window.addEventListener('mouseup', () => { dragState = null; });
svgArea.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.88 : 1.14;
const rect = svgArea.getBoundingClientRect();
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
svgTranslate.x = mx - (mx - svgTranslate.x) * delta;
svgTranslate.y = my - (my - svgTranslate.y) * delta;
svgScale *= delta;
applyTransform();
}, { passive: false });
// ═══════════════════════════════════════════════════════
// BOOT
// ═══════════════════════════════════════════════════════
async function init() {
try {
const user = await LS.getMe();
const initials = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
document.getElementById('nav-avatar').textContent = initials;
const nav2 = document.getElementById('nav-avatar2');
if (nav2) nav2.textContent = initials;
if (user?.role === 'admin') document.getElementById('btn-admin').style.display = '';
LS.applyRoleSidebar(user);
LS.showBoardIfAllowed();
if (user?.role !== 'student') {
document.getElementById('btn-classes').style.display = '';
}
document.getElementById('nav-user').textContent = user?.name || '—';
} catch(e) {}
// wait for layout
await new Promise(r => setTimeout(r, 60));
renderPath();
renderPathInfo();
if (window.lucide) lucide.createIcons();
LS.notif?.init();
LS.hideDisabledFeatures?.();
}
init();
</script>
<script src="/js/notifications.js"></script>
<script src="/js/mobile.js"></script>
</body>
</html>