DavidVivancos commited on
Commit
c80fbab
Β·
verified Β·
1 Parent(s): a6b461c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +2146 -19
index.html CHANGED
@@ -1,19 +1,2146 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sphero Γ— Neuraxon 2.0 β€” Bio-Inspired Neural Control</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
9
+
10
+ :root {
11
+ --bg-abyss: #030709;
12
+ --bg-deep: #060d14;
13
+ --bg-card: #0a1520;
14
+ --bg-raised: #0e1a28;
15
+ --bg-glass: rgba(10,21,32,0.75);
16
+ --border-faint: rgba(34,211,238,0.08);
17
+ --border-glow: rgba(34,211,238,0.2);
18
+ --border-active: rgba(34,211,238,0.5);
19
+ --txt-primary: #e0f2fe;
20
+ --txt-secondary: #7aa3c0;
21
+ --txt-dim: #3a5f7a;
22
+ --cyan: #22d3ee;
23
+ --cyan-bright: #67e8f9;
24
+ --green: #34d399;
25
+ --green-dim: rgba(52,211,153,0.15);
26
+ --red: #fb7185;
27
+ --red-dim: rgba(251,113,133,0.15);
28
+ --amber: #fbbf24;
29
+ --purple: #a78bfa;
30
+ --pink: #f472b6;
31
+ --blue: #60a5fa;
32
+ --excite: #22d3ee;
33
+ --neutral: #475569;
34
+ --inhibit: #f472b6;
35
+ --da-color: #fbbf24;
36
+ --sht-color: #a78bfa;
37
+ --ach-color: #34d399;
38
+ --na-color: #fb7185;
39
+ --mono: 'JetBrains Mono', monospace;
40
+ --sans: 'Outfit', system-ui, sans-serif;
41
+ }
42
+
43
+ * { box-sizing: border-box; margin: 0; padding: 0; }
44
+ html { height: 100%; }
45
+ body {
46
+ min-height: 100%; background: var(--bg-abyss); color: var(--txt-primary);
47
+ font-family: var(--sans); overflow-x: hidden;
48
+ background-image:
49
+ radial-gradient(ellipse 80% 50% at 20% 80%, rgba(34,211,238,0.03) 0%, transparent 60%),
50
+ radial-gradient(ellipse 60% 40% at 80% 20%, rgba(164,139,250,0.03) 0%, transparent 60%);
51
+ }
52
+
53
+ /* === HEADER === */
54
+ header {
55
+ position: sticky; top: 0; z-index: 100;
56
+ display: flex; justify-content: space-between; align-items: center;
57
+ padding: 12px 24px;
58
+ background: rgba(3,7,9,0.9); backdrop-filter: blur(20px) saturate(1.5);
59
+ border-bottom: 1px solid var(--border-faint);
60
+ }
61
+ .logo-area { display: flex; align-items: center; gap: 12px; }
62
+ .logo-area h1 {
63
+ font-size: 17px; font-weight: 700; letter-spacing: -0.02em;
64
+ background: linear-gradient(135deg, var(--cyan), var(--purple));
65
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
66
+ }
67
+ .logo-area .subtitle { font-size: 10px; color: var(--txt-dim); font-family: var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }
68
+ .header-right { display: flex; align-items: center; gap: 14px; }
69
+ .status-badge {
70
+ font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: 0.06em;
71
+ padding: 5px 14px; border-radius: 20px; border: 1px solid;
72
+ display: flex; align-items: center; gap: 6px;
73
+ }
74
+ .status-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; }
75
+ .status-badge.on { color: var(--green); border-color: rgba(52,211,153,0.3); }
76
+ .status-badge.on::before { background: var(--green); box-shadow: 0 0 8px var(--green); }
77
+ .status-badge.off { color: var(--red); border-color: rgba(251,113,133,0.2); }
78
+ .status-badge.off::before { background: var(--red); }
79
+ .mode-indicator {
80
+ font-family: var(--mono); font-size: 10px; font-weight: 500;
81
+ padding: 5px 12px; border-radius: 6px; letter-spacing: 0.05em;
82
+ background: rgba(34,211,238,0.08); color: var(--cyan); border: 1px solid rgba(34,211,238,0.15);
83
+ }
84
+
85
+ /* === LAYOUT === */
86
+ .app-grid {
87
+ display: grid;
88
+ grid-template-columns: 280px 1fr 260px;
89
+ grid-template-rows: auto 1fr auto;
90
+ gap: 0;
91
+ height: calc(100vh - 49px);
92
+ max-height: calc(100vh - 49px);
93
+ }
94
+ @media (max-width: 1024px) {
95
+ .app-grid { grid-template-columns: 1fr; grid-template-rows: auto; height: auto; }
96
+ .panel-left, .panel-right { border: none !important; }
97
+ }
98
+
99
+ /* === PANELS === */
100
+ .panel-left {
101
+ grid-row: 1 / -1;
102
+ border-right: 1px solid var(--border-faint);
103
+ background: var(--bg-deep);
104
+ overflow-y: auto; padding: 16px;
105
+ display: flex; flex-direction: column; gap: 14px;
106
+ }
107
+ .panel-center {
108
+ grid-row: 1 / -1;
109
+ display: flex; flex-direction: column;
110
+ position: relative; overflow: hidden;
111
+ cursor: grab;
112
+ }
113
+ .panel-center:active { cursor: grabbing; }
114
+ .panel-right {
115
+ grid-row: 1 / -1;
116
+ border-left: 1px solid var(--border-faint);
117
+ background: var(--bg-deep);
118
+ overflow-y: auto; padding: 16px;
119
+ display: flex; flex-direction: column; gap: 14px;
120
+ }
121
+
122
+ /* === CARDS === */
123
+ .card {
124
+ background: var(--bg-card); border: 1px solid var(--border-faint);
125
+ border-radius: 10px; padding: 14px; position: relative;
126
+ }
127
+ .card-title {
128
+ font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em;
129
+ color: var(--txt-dim); margin-bottom: 12px; display: flex; align-items: center; gap: 6px;
130
+ }
131
+ .card-title .dot { width: 5px; height: 5px; border-radius: 50%; }
132
+
133
+ /* === CONNECTION === */
134
+ .connect-row { display: flex; gap: 8px; }
135
+ .btn {
136
+ flex: 1; padding: 10px 16px; border: none; border-radius: 8px;
137
+ font-family: var(--sans); font-size: 12px; font-weight: 600;
138
+ cursor: pointer; transition: all 0.2s;
139
+ }
140
+ .btn:disabled { opacity: 0.35; cursor: not-allowed; }
141
+ .btn-primary {
142
+ background: linear-gradient(135deg, var(--cyan), #3b82f6);
143
+ color: #fff; box-shadow: 0 2px 12px rgba(34,211,238,0.2);
144
+ }
145
+ .btn-primary:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(34,211,238,0.3); }
146
+ .btn-secondary { background: var(--bg-raised); color: var(--txt-secondary); border: 1px solid var(--border-faint); }
147
+ .btn-secondary:hover:not(:disabled) { border-color: var(--border-glow); }
148
+
149
+ /* === MODE SELECTOR === */
150
+ .mode-group { display: flex; gap: 4px; background: var(--bg-abyss); border-radius: 8px; padding: 3px; }
151
+ .mode-btn {
152
+ flex: 1; padding: 8px 6px; border: none; border-radius: 6px; cursor: pointer;
153
+ font-family: var(--mono); font-size: 10px; font-weight: 500; letter-spacing: 0.03em;
154
+ background: transparent; color: var(--txt-dim); transition: all 0.2s;
155
+ }
156
+ .mode-btn.active { background: var(--bg-raised); color: var(--cyan); box-shadow: 0 0 12px rgba(34,211,238,0.1); }
157
+ .mode-btn:hover:not(.active) { color: var(--txt-secondary); }
158
+
159
+ /* === NEUROMOD SLIDERS === */
160
+ .neuromod-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
161
+ .neuromod-label { font-family: var(--mono); font-size: 10px; font-weight: 600; width: 28px; text-align: right; }
162
+ .neuromod-slider {
163
+ flex: 1; -webkit-appearance: none; appearance: none;
164
+ height: 4px; border-radius: 2px; outline: none; background: var(--bg-abyss);
165
+ }
166
+ .neuromod-slider::-webkit-slider-thumb {
167
+ -webkit-appearance: none; width: 14px; height: 14px;
168
+ border-radius: 50%; cursor: pointer; border: 2px solid;
169
+ }
170
+ .neuromod-val { font-family: var(--mono); font-size: 10px; width: 32px; color: var(--txt-dim); }
171
+ .nm-da .neuromod-label { color: var(--da-color); }
172
+ .nm-da .neuromod-slider::-webkit-slider-thumb { background: var(--da-color); border-color: var(--da-color); }
173
+ .nm-sht .neuromod-label { color: var(--sht-color); }
174
+ .nm-sht .neuromod-slider::-webkit-slider-thumb { background: var(--sht-color); border-color: var(--sht-color); }
175
+ .nm-ach .neuromod-label { color: var(--ach-color); }
176
+ .nm-ach .neuromod-slider::-webkit-slider-thumb { background: var(--ach-color); border-color: var(--ach-color); }
177
+ .nm-na .neuromod-label { color: var(--na-color); }
178
+ .nm-na .neuromod-slider::-webkit-slider-thumb { background: var(--na-color); border-color: var(--na-color); }
179
+
180
+ /* === METRICS === */
181
+ .metrics-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
182
+ .metric-box {
183
+ background: var(--bg-abyss); border-radius: 6px; padding: 8px 10px;
184
+ border: 1px solid var(--border-faint);
185
+ }
186
+ .metric-label { font-size: 9px; color: var(--txt-dim); font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }
187
+ .metric-value { font-size: 16px; font-weight: 700; margin-top: 2px; }
188
+ .metric-value.exc { color: var(--excite); }
189
+ .metric-value.inh { color: var(--inhibit); }
190
+ .metric-value.neu { color: var(--txt-dim); }
191
+
192
+ /* === D-PAD === */
193
+ .dpad-container { display: flex; justify-content: center; }
194
+ .dpad-grid {
195
+ display: grid; grid-template-columns: 54px 54px 54px; grid-template-rows: 54px 54px 54px;
196
+ gap: 5px;
197
+ }
198
+ .dpad-btn {
199
+ width: 54px; height: 54px; border-radius: 10px; border: 1.5px solid var(--border-glow);
200
+ background: var(--bg-raised); color: var(--txt-dim); font-size: 18px; font-weight: 700;
201
+ display: grid; place-items: center; cursor: pointer; transition: all 0.1s;
202
+ user-select: none; -webkit-user-select: none; font-family: var(--sans);
203
+ }
204
+ .dpad-btn:active, .dpad-btn.active {
205
+ background: rgba(34,211,238,0.15); border-color: var(--cyan); color: var(--cyan);
206
+ box-shadow: 0 0 14px rgba(34,211,238,0.25); transform: scale(0.94);
207
+ }
208
+ .dpad-btn.stop-btn { font-size: 9px; font-family: var(--mono); border-color: rgba(251,113,133,0.3); color: var(--red); font-weight: 600; }
209
+ .dpad-btn.stop-btn:active, .dpad-btn.stop-btn.active { background: rgba(251,113,133,0.15); box-shadow: 0 0 14px rgba(251,113,133,0.2); }
210
+ .dpad-hidden { visibility: hidden; }
211
+ .dpad-hint { text-align: center; font-size: 9px; color: var(--txt-dim); font-family: var(--mono); margin-top: 8px; }
212
+
213
+ /* === CANVAS === */
214
+ #neuralCanvas {
215
+ width: 100%; height: 100%; display: block;
216
+ background: transparent;
217
+ }
218
+ .canvas-overlay {
219
+ position: absolute; bottom: 12px; left: 12px; right: 12px;
220
+ display: flex; justify-content: space-between; align-items: flex-end;
221
+ pointer-events: none;
222
+ }
223
+ .overlay-chip {
224
+ font-family: var(--mono); font-size: 9px; letter-spacing: 0.05em;
225
+ padding: 4px 10px; border-radius: 4px;
226
+ background: rgba(3,7,9,0.8); color: var(--txt-dim);
227
+ border: 1px solid var(--border-faint); backdrop-filter: blur(8px);
228
+ }
229
+ .overlay-chip span { color: var(--cyan); }
230
+
231
+ /* === FOCUSED NODE HUD === */
232
+ .focus-hud {
233
+ position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
234
+ pointer-events: none; z-index: 10;
235
+ background: rgba(3,7,9,0.88); backdrop-filter: blur(16px) saturate(1.4);
236
+ border: 1px solid rgba(251,191,36,0.3); border-radius: 10px;
237
+ padding: 10px 18px; min-width: 220px;
238
+ font-family: var(--mono); font-size: 10px;
239
+ display: flex; gap: 14px; align-items: center;
240
+ opacity: 0; transition: opacity 0.25s ease;
241
+ box-shadow: 0 4px 30px rgba(0,0,0,0.5), 0 0 20px rgba(251,191,36,0.05);
242
+ }
243
+ .focus-hud.visible { opacity: 1; }
244
+ .focus-hud .node-id { font-size: 16px; font-weight: 800; min-width: 30px; text-align: center; }
245
+ .focus-hud .node-meta { display: flex; flex-direction: column; gap: 2px; }
246
+ .focus-hud .meta-row { display: flex; gap: 8px; }
247
+ .focus-hud .meta-label { color: var(--txt-dim); font-size: 9px; width: 50px; }
248
+ .focus-hud .meta-val { font-weight: 600; font-size: 10px; }
249
+
250
+ /* === INTERACTION HINT === */
251
+ .pan-hint {
252
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
253
+ pointer-events: none; z-index: 5;
254
+ font-family: var(--mono); font-size: 11px; color: rgba(224,242,254,0.25);
255
+ letter-spacing: 0.05em; text-align: center;
256
+ transition: opacity 1.2s ease;
257
+ }
258
+ .pan-hint.hidden { opacity: 0; }
259
+
260
+ /* === LEGEND === */
261
+ .legend-row { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
262
+ .legend-item { display: flex; align-items: center; gap: 5px; font-size: 9px; font-family: var(--mono); color: var(--txt-dim); }
263
+ .legend-dot { width: 8px; height: 8px; border-radius: 50%; }
264
+ .legend-dot.exc { background: var(--excite); box-shadow: 0 0 6px var(--excite); }
265
+ .legend-dot.inh { background: var(--inhibit); box-shadow: 0 0 6px var(--inhibit); }
266
+ .legend-dot.neu { background: var(--neutral); }
267
+
268
+ /* === LOG === */
269
+ .log-panel {
270
+ font-family: var(--mono); font-size: 10px; line-height: 1.6;
271
+ overflow-y: auto; padding: 10px; max-height: 160px;
272
+ background: rgba(0,0,0,0.4); border: 1px solid var(--border-faint);
273
+ border-radius: 8px; color: var(--txt-dim); white-space: pre-wrap;
274
+ }
275
+ .log-panel::-webkit-scrollbar { width: 4px; }
276
+ .log-panel::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
277
+
278
+ /* === MOTOR OUTPUT === */
279
+ .motor-display { display: flex; gap: 10px; align-items: center; justify-content: center; }
280
+ .motor-dial {
281
+ width: 80px; height: 80px; border-radius: 50%;
282
+ border: 2px solid var(--border-glow); background: var(--bg-abyss);
283
+ position: relative; display: flex; align-items: center; justify-content: center;
284
+ }
285
+ .motor-dial .needle {
286
+ width: 2px; height: 30px; background: var(--cyan);
287
+ position: absolute; bottom: 50%; left: calc(50% - 1px);
288
+ transform-origin: bottom center; transition: transform 0.15s;
289
+ border-radius: 1px; box-shadow: 0 0 6px var(--cyan);
290
+ }
291
+ .motor-dial .center-dot {
292
+ width: 8px; height: 8px; border-radius: 50%;
293
+ background: var(--cyan); position: absolute;
294
+ box-shadow: 0 0 10px var(--cyan);
295
+ }
296
+ .motor-label { font-family: var(--mono); font-size: 9px; color: var(--txt-dim); text-align: center; margin-top: 4px; }
297
+ .motor-val { font-family: var(--mono); font-size: 14px; font-weight: 700; color: var(--cyan); text-align: center; }
298
+
299
+ /* === ACTIVITY BAR === */
300
+ .activity-bar {
301
+ height: 40px; background: var(--bg-abyss); border-radius: 6px;
302
+ border: 1px solid var(--border-faint); overflow: hidden;
303
+ display: flex; position: relative;
304
+ }
305
+ .activity-bar canvas { width: 100%; height: 100%; }
306
+
307
+ /* === SCROLLBAR === */
308
+ .panel-left::-webkit-scrollbar, .panel-right::-webkit-scrollbar { width: 4px; }
309
+ .panel-left::-webkit-scrollbar-thumb, .panel-right::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
310
+
311
+ .controls-disabled { opacity: 0.3; pointer-events: none; }
312
+
313
+ /* === LEARNING PROGRESS BAR === */
314
+ .progress-track {
315
+ width: 100%; height: 18px; background: var(--bg-abyss);
316
+ border-radius: 9px; border: 1px solid var(--border-faint);
317
+ overflow: hidden; position: relative;
318
+ }
319
+ .progress-fill {
320
+ height: 100%; border-radius: 9px; transition: width 0.4s ease;
321
+ background: linear-gradient(90deg, rgba(251,191,36,0.4), rgba(52,211,153,0.8));
322
+ position: relative;
323
+ }
324
+ .progress-fill.learned {
325
+ background: linear-gradient(90deg, rgba(52,211,153,0.6), rgba(34,211,238,0.9));
326
+ }
327
+ .progress-text {
328
+ position: absolute; top: 0; left: 0; right: 0; bottom: 0;
329
+ display: flex; align-items: center; justify-content: center;
330
+ font-family: var(--mono); font-size: 9px; font-weight: 600;
331
+ color: var(--txt-primary); text-shadow: 0 1px 3px rgba(0,0,0,0.6);
332
+ pointer-events: none; z-index: 1;
333
+ }
334
+ .nxon-source-tag {
335
+ font-family: var(--mono); font-size: 9px; font-weight: 600;
336
+ padding: 3px 8px; border-radius: 4px; letter-spacing: 0.04em;
337
+ }
338
+ .nxon-source-tag.human { background: rgba(52,211,153,0.15); color: var(--green); }
339
+ .nxon-source-tag.nxon { background: rgba(164,139,250,0.15); color: var(--purple); }
340
+ .nxon-source-tag.learning { background: rgba(251,191,36,0.15); color: var(--amber); }
341
+ </style>
342
+ </head>
343
+ <body>
344
+
345
+ <header>
346
+ <div class="logo-area">
347
+ <div>
348
+ <h1> <a href="https://github.com/DavidVivancos/Neuraxon"> Neuraxon 2.0</a> <a href="https://sphero.com/collections/mini"> Sphero Mini<a> Control</h1>
349
+ <div class="subtitle">Bio-Inspired Neural Control By <a href="https://www.vivancos.com/">David Vivancos</a> & <a href="https://josesanchezgarcia.com/">Jose Sanchez</a> for <a href="https://qubic.org/">Qubic</a> Open Science </div>
350
+ </div>
351
+ </div>
352
+ <div class="header-right">
353
+ <div class="mode-indicator" id="modeLabel">MANUAL</div>
354
+ <div class="status-badge off" id="connStatus">OFFLINE</div>
355
+ </div>
356
+ </header>
357
+
358
+ <div class="app-grid">
359
+
360
+ <!-- =================== LEFT PANEL =================== -->
361
+ <div class="panel-left">
362
+
363
+ <div class="card">
364
+ <div class="card-title"><div class="dot" style="background:var(--cyan)"></div> SPHERO CONNECTION</div>
365
+ <div class="connect-row">
366
+ <button class="btn btn-primary" id="btnConnect">Connect</button>
367
+ <button class="btn btn-secondary" id="btnDisconnect" disabled>Disconnect</button>
368
+ </div>
369
+ </div>
370
+
371
+ <div class="card">
372
+ <div class="card-title"><div class="dot" style="background:var(--purple)"></div> CONTROL MODE</div>
373
+ <div class="mode-group">
374
+ <button class="mode-btn active" data-mode="manual" onclick="setMode('manual')">Manual</button>
375
+ <button class="mode-btn" data-mode="hybrid" onclick="setMode('hybrid')">Hybrid</button>
376
+ </div>
377
+ </div>
378
+
379
+ <div class="card" id="driveCard">
380
+ <div class="card-title"><div class="dot" style="background:var(--green)"></div> HUMAN DRIVE</div>
381
+ <div class="dpad-container">
382
+ <div class="dpad-grid">
383
+ <div class="dpad-hidden"></div>
384
+ <div class="dpad-btn" id="btnUp">W</div>
385
+ <div class="dpad-hidden"></div>
386
+ <div class="dpad-btn" id="btnLeft">A</div>
387
+ <div class="dpad-btn stop-btn" id="btnBrake">STOP</div>
388
+ <div class="dpad-btn" id="btnRight">D</div>
389
+ <div class="dpad-hidden"></div>
390
+ <div class="dpad-btn" id="btnDown">S</div>
391
+ <div class="dpad-hidden"></div>
392
+ </div>
393
+ </div>
394
+ <div class="dpad-hint">WASD / Arrows / Space</div>
395
+ </div>
396
+
397
+ <div class="card">
398
+ <div class="card-title"><div class="dot" style="background:var(--amber)"></div> MOTOR OUTPUT</div>
399
+ <div class="motor-display">
400
+ <div>
401
+ <div class="motor-dial" id="headingDial">
402
+ <div class="needle" id="headingNeedle"></div>
403
+ <div class="center-dot"></div>
404
+ </div>
405
+ <div class="motor-label">HEADING</div>
406
+ <div class="motor-val" id="headingVal">0Β°</div>
407
+ </div>
408
+ <div style="text-align:center;">
409
+ <div class="metric-box" style="width:80px;padding:12px;">
410
+ <div class="metric-label">SPEED</div>
411
+ <div class="metric-value exc" id="speedVal" style="font-size:22px;">0</div>
412
+ </div>
413
+ <div style="margin-top:6px;">
414
+ <div class="metric-label" style="font-size:8px;">BLEND</div>
415
+ <div class="motor-val" id="blendVal" style="font-size:11px;color:var(--purple);">H:100% N:0%</div>
416
+ </div>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ <div class="card" style="flex:1;min-height:100px;display:flex;flex-direction:column;">
422
+ <div class="card-title"><div class="dot" style="background:var(--txt-dim)"></div> SYSTEM LOG</div>
423
+ <div class="log-panel" id="logPanel" style="flex:1;max-height:none;"></div>
424
+ </div>
425
+
426
+ </div>
427
+
428
+ <!-- =================== CENTER - NEURAL VIS =================== -->
429
+ <div class="panel-center" id="panelCenter">
430
+ <canvas id="neuralCanvas"></canvas>
431
+
432
+ <!-- Focus HUD (shows details of nearest node) -->
433
+ <div class="focus-hud" id="focusHud">
434
+ <div class="node-id" id="focusNodeId">β€”</div>
435
+ <div class="node-meta">
436
+ <div class="meta-row"><span class="meta-label">TYPE</span><span class="meta-val" id="focusType">β€”</span></div>
437
+ <div class="meta-row"><span class="meta-label">STATE</span><span class="meta-val" id="focusState">β€”</span></div>
438
+ <div class="meta-row"><span class="meta-label">s(t)</span><span class="meta-val" id="focusS">β€”</span></div>
439
+ <div class="meta-row"><span class="meta-label">ADAPT</span><span class="meta-val" id="focusAdapt">β€”</span></div>
440
+ <div class="meta-row"><span class="meta-label">HEALTH</span><span class="meta-val" id="focusHealth">β€”</span></div>
441
+ <div class="meta-row"><span class="meta-label">SYNAPSES</span><span class="meta-val" id="focusSyn">β€”</span></div>
442
+ </div>
443
+ </div>
444
+
445
+ <!-- Interaction hint -->
446
+ <div class="pan-hint" id="panHint">⟐ CLICK & DRAG TO EXPLORE THE NEURAL GRAPH ⟐<br><span style="font-size:9px;opacity:0.5;">scroll to zoom · nearest node auto-focuses</span></div>
447
+
448
+ <div class="canvas-overlay">
449
+ <div class="overlay-chip">STEP <span id="stepCount">0</span></div>
450
+ <div class="legend-row">
451
+ <div class="legend-item"><div class="legend-dot exc"></div>Excite (+1)</div>
452
+ <div class="legend-item"><div class="legend-dot neu"></div>Neutral (0)</div>
453
+ <div class="legend-item"><div class="legend-dot inh"></div>Inhibit (-1)</div>
454
+ <div class="legend-item" style="opacity:0.6;">β—‹ Input</div>
455
+ <div class="legend-item" style="opacity:0.6;">β—‡ Hidden</div>
456
+ <div class="legend-item" style="opacity:0.6;">β–‘ Output</div>
457
+ </div>
458
+ <div class="overlay-chip">ENERGY <span id="energyVal">0.00</span></div>
459
+ </div>
460
+ </div>
461
+
462
+ <!-- =================== RIGHT PANEL =================== -->
463
+ <div class="panel-right">
464
+
465
+ <div class="card">
466
+ <div class="card-title"><div class="dot" style="background:var(--da-color)"></div> NEUROMODULATORS</div>
467
+ <div class="neuromod-row nm-da">
468
+ <span class="neuromod-label">DA</span>
469
+ <input type="range" class="neuromod-slider" id="sliderDA" min="0" max="100" value="50">
470
+ <span class="neuromod-val" id="valDA">0.50</span>
471
+ </div>
472
+ <div class="neuromod-row nm-sht">
473
+ <span class="neuromod-label">5HT</span>
474
+ <input type="range" class="neuromod-slider" id="slider5HT" min="0" max="100" value="50">
475
+ <span class="neuromod-val" id="val5HT">0.50</span>
476
+ </div>
477
+ <div class="neuromod-row nm-ach">
478
+ <span class="neuromod-label">ACh</span>
479
+ <input type="range" class="neuromod-slider" id="sliderACh" min="0" max="100" value="50">
480
+ <span class="neuromod-val" id="valACh">0.50</span>
481
+ </div>
482
+ <div class="neuromod-row nm-na">
483
+ <span class="neuromod-label">NA</span>
484
+ <input type="range" class="neuromod-slider" id="sliderNA" min="0" max="100" value="50">
485
+ <span class="neuromod-val" id="valNA">0.50</span>
486
+ </div>
487
+ </div>
488
+
489
+ <div class="card">
490
+ <div class="card-title"><div class="dot" style="background:var(--cyan)"></div> NETWORK STATS</div>
491
+ <div class="metrics-grid">
492
+ <div class="metric-box">
493
+ <div class="metric-label">Excitatory</div>
494
+ <div class="metric-value exc" id="statExc">0</div>
495
+ </div>
496
+ <div class="metric-box">
497
+ <div class="metric-label">Inhibitory</div>
498
+ <div class="metric-value inh" id="statInh">0</div>
499
+ </div>
500
+ <div class="metric-box">
501
+ <div class="metric-label">Neutral</div>
502
+ <div class="metric-value neu" id="statNeu">0</div>
503
+ </div>
504
+ <div class="metric-box">
505
+ <div class="metric-label">Synapses</div>
506
+ <div class="metric-value" style="color:var(--blue)" id="statSyn">0</div>
507
+ </div>
508
+ </div>
509
+ </div>
510
+
511
+ <div class="card">
512
+ <div class="card-title"><div class="dot" style="background:var(--sht-color)"></div> OSCILLATOR BANK</div>
513
+ <canvas id="oscCanvas" width="228" height="90" style="width:100%;height:90px;border-radius:6px;background:var(--bg-abyss);"></canvas>
514
+ </div>
515
+
516
+ <div class="card">
517
+ <div class="card-title"><div class="dot" style="background:var(--green)"></div> ACTIVITY TRACE</div>
518
+ <div class="activity-bar">
519
+ <canvas id="activityCanvas" width="228" height="40" style="width:100%;height:40px;"></canvas>
520
+ </div>
521
+ </div>
522
+
523
+ <div class="card">
524
+ <div class="card-title"><div class="dot" style="background:var(--pink)"></div> RECEPTOR ACTIVATIONS</div>
525
+ <div id="receptorBars" style="display:flex;flex-direction:column;gap:4px;"></div>
526
+ </div>
527
+
528
+ <div class="card">
529
+ <div class="card-title"><div class="dot" style="background:var(--amber)"></div> LEARNING PROGRESS</div>
530
+ <div style="margin-bottom:8px;">
531
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
532
+ <span style="font-family:var(--mono);font-size:9px;color:var(--txt-dim);">ACCURACY</span>
533
+ <span style="font-family:var(--mono);font-size:11px;font-weight:700;color:var(--green);" id="learnPct">0%</span>
534
+ </div>
535
+ <div class="progress-track">
536
+ <div class="progress-fill" id="learnBar" style="width:0%;"></div>
537
+ <div class="progress-text" id="learnBarText">Drive in MANUAL to teach</div>
538
+ </div>
539
+ </div>
540
+ <div class="metrics-grid">
541
+ <div class="metric-box">
542
+ <div class="metric-label">Teach Steps</div>
543
+ <div class="metric-value" style="color:var(--da-color);font-size:13px;" id="statLearn">0</div>
544
+ </div>
545
+ <div class="metric-box">
546
+ <div class="metric-label">Mean |Ξ”w|</div>
547
+ <div class="metric-value" style="color:var(--amber);font-size:13px;" id="statDw">0.000</div>
548
+ </div>
549
+ <div class="metric-box">
550
+ <div class="metric-label">Status</div>
551
+ <div class="metric-value" style="color:var(--green);font-size:11px;" id="statStruct">stable</div>
552
+ </div>
553
+ <div class="metric-box">
554
+ <div class="metric-label">Out Accum</div>
555
+ <div class="metric-value" style="color:var(--cyan);font-size:10px;font-family:var(--mono);" id="statAccum">0 0 0 0</div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+
560
+ <div class="card">
561
+ <div class="card-title"><div class="dot" style="background:var(--purple)"></div> MOTOR SOURCE</div>
562
+ <div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;" id="sourceTagArea">
563
+ <span class="nxon-source-tag human" id="tagHuman">HUMAN</span>
564
+ <span class="nxon-source-tag nxon" id="tagNxon" style="opacity:0.3;">NEURAXON</span>
565
+ <span class="nxon-source-tag learning" id="tagLearn" style="opacity:0.3;">LEARNING</span>
566
+ </div>
567
+ <div style="margin-top:8px;text-align:center;">
568
+ <span style="font-family:var(--mono);font-size:10px;color:var(--txt-dim);" id="sourceDetail">Manual control active</span>
569
+ </div>
570
+ </div>
571
+
572
+ </div>
573
+ </div>
574
+
575
+ <script>
576
+ // ═══════════════════════════════════════════════════════════════
577
+ // SPHERO MINI BLE PROTOCOL
578
+ // ═══════════════════════════════════════════════════════════════
579
+
580
+ const UUID_SPHERO_SERVICE = '00010001-574f-4f20-5370-6865726f2121';
581
+ const UUID_SPHERO_SERVICE_INIT = '00020001-574f-4f20-5370-6865726f2121';
582
+ const UUID_CHAR_HANDLE = '00010002-574f-4f20-5370-6865726f2121';
583
+ const UUID_CHAR_USETHEFORCE = '00020005-574f-4f20-5370-6865726f2121';
584
+ const UseTheForceBytes = new Uint8Array([0x75,0x73,0x65,0x74,0x68,0x65,0x66,0x6f,0x72,0x63,0x65,0x2e,0x2e,0x2e,0x62,0x61,0x6e,0x64]);
585
+ const API = { ESC: 0xAB, SOP: 0x8D, EOP: 0xD8, ESC_MASK: 0x88 };
586
+ const DeviceId = { powerInfo: 0x13, driving: 0x16, userIO: 0x1A };
587
+ const PowerCmd = { wake: 0x0D, sleep: 0x01 };
588
+ const DrivingCmd = { resetYaw: 0x06, driveWithHeading: 0x07 };
589
+ const UserIOCmd = { allLEDs: 0x0E };
590
+ const Flags = { requestsResponse: 2, requestsOnlyErrorResponse: 4, resetsInactivityTimeout: 8 };
591
+
592
+ class SpheroMiniBLE {
593
+ constructor() {
594
+ this.device = null; this.server = null; this.ch = new Map(); this.seq = 0;
595
+ this._chain = Promise.resolve(); this._qDepth = 0; this._closed = false;
596
+ this.onDisconnect = null;
597
+ }
598
+ _pushEscaped(out, b) {
599
+ if (b === API.SOP || b === API.EOP || b === API.ESC) { out.push(API.ESC); out.push(b & (~API.ESC_MASK)); }
600
+ else out.push(b);
601
+ }
602
+ _buildPacket(did, cid, dataBytes, cmdFlags) {
603
+ this.seq = (this.seq + 1) & 0xFF; let sum = 0; const out = [];
604
+ out.push(API.SOP); out.push(cmdFlags); sum += cmdFlags;
605
+ this._pushEscaped(out, did); sum += did;
606
+ this._pushEscaped(out, cid); sum += cid;
607
+ this._pushEscaped(out, this.seq); sum += this.seq;
608
+ for (const b of dataBytes) { this._pushEscaped(out, b); sum += b; }
609
+ const chk = (~sum) & 0xFF; this._pushEscaped(out, chk); out.push(API.EOP);
610
+ return new Uint8Array(out);
611
+ }
612
+ _enqueueWrite(fn) {
613
+ if (this._closed) return Promise.reject(new Error('BLE closed'));
614
+ this._qDepth++;
615
+ const run = async () => { try { return await fn(); } finally { this._qDepth = Math.max(0, this._qDepth - 1); } };
616
+ this._chain = this._chain.then(run, run); return this._chain;
617
+ }
618
+ async connect() {
619
+ sysLog('Requesting Bluetooth device...');
620
+ this.device = await navigator.bluetooth.requestDevice({
621
+ filters: [{ services: [UUID_SPHERO_SERVICE] }],
622
+ optionalServices: [UUID_SPHERO_SERVICE_INIT]
623
+ });
624
+ this.device.addEventListener('gattserverdisconnected', () => {
625
+ sysLog('BLE disconnected');
626
+ if (this.onDisconnect) this.onDisconnect();
627
+ });
628
+ sysLog(`Connecting to: ${this.device.name || 'Sphero Mini'}`);
629
+ this.server = await this.device.gatt.connect();
630
+ const svcCmd = await this.server.getPrimaryService(UUID_SPHERO_SERVICE);
631
+ const svcInit = await this.server.getPrimaryService(UUID_SPHERO_SERVICE_INIT);
632
+ const chHandle = await svcCmd.getCharacteristic(UUID_CHAR_HANDLE);
633
+ const chForce = await svcInit.getCharacteristic(UUID_CHAR_USETHEFORCE);
634
+ this.ch.set('handle', chHandle);
635
+ sysLog('Waking Sphero...');
636
+ await this._enqueueWrite(() => chForce.writeValue(UseTheForceBytes));
637
+ await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' });
638
+ await sleep(200);
639
+ await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' });
640
+ }
641
+ async disconnect() {
642
+ this._closed = true;
643
+ if (this.device && this.device.gatt.connected) this.device.gatt.disconnect();
644
+ }
645
+ async send(did, cid, data, opts = { response: 'errorOnly' }) {
646
+ const chHandle = this.ch.get('handle'); if (!chHandle) throw new Error('Handle missing');
647
+ let cmdFlags = Flags.resetsInactivityTimeout;
648
+ if (opts.response === 'full') cmdFlags |= Flags.requestsResponse;
649
+ else if (opts.response === 'errorOnly') cmdFlags |= Flags.requestsOnlyErrorResponse;
650
+ const pkt = this._buildPacket(did, cid, data, cmdFlags);
651
+ return this._enqueueWrite(async () => {
652
+ if (chHandle.writeValueWithoutResponse && opts.response !== 'full') await chHandle.writeValueWithoutResponse(pkt);
653
+ else await chHandle.writeValue(pkt);
654
+ return this.seq;
655
+ });
656
+ }
657
+ async setMainLED(r, g, b) { return this.send(DeviceId.userIO, UserIOCmd.allLEDs, [0x00, 0x70, r & 255, g & 255, b & 255], { response: 'none' }); }
658
+ async roll(speed, headingDeg) {
659
+ const head = ((headingDeg % 360) + 360) % 360;
660
+ return this.send(DeviceId.driving, DrivingCmd.driveWithHeading, [speed & 255, (head >> 8) & 0xFF, head & 0xFF, 0x01], { response: 'none' });
661
+ }
662
+ async resetYaw() { return this.send(DeviceId.driving, DrivingCmd.resetYaw, [], { response: 'full' }); }
663
+ async stop() { return this.roll(0, 0); }
664
+ }
665
+
666
+
667
+ // ═══════════════════════════════════════════════════════════════
668
+ // NEURAXON 2.0 β€” JavaScript Implementation
669
+ // Based on the architecture by David Vivancos & Jose Sanchez
670
+ // ═══════════════════════════════════════════════════════════════
671
+
672
+ class ReceptorSubtype {
673
+ constructor(name, threshold, gain, isTonic) {
674
+ this.name = name; this.threshold = threshold;
675
+ this.gain = gain; this.isTonic = isTonic; this.activation = 0;
676
+ }
677
+ computeActivation(concentration) {
678
+ const k = this.isTonic ? 20 : 10;
679
+ this.activation = this.gain / (1 + Math.exp(-k * (concentration - this.threshold)));
680
+ return this.activation;
681
+ }
682
+ }
683
+
684
+ class OscillatorBank {
685
+ constructor() {
686
+ this.bands = [
687
+ { name: 'infraslow', freq: 0.05, phase: Math.random() * Math.PI * 2, amplitude: 0.15 },
688
+ { name: 'slow', freq: 0.5, phase: Math.random() * Math.PI * 2, amplitude: 0.2 },
689
+ { name: 'theta', freq: 6, phase: Math.random() * Math.PI * 2, amplitude: 0.3 },
690
+ { name: 'alpha', freq: 10, phase: Math.random() * Math.PI * 2, amplitude: 0.15 },
691
+ { name: 'gamma', freq: 40, phase: Math.random() * Math.PI * 2, amplitude: 0.25 },
692
+ ];
693
+ this.coupling = 0.3;
694
+ }
695
+ update(dt) {
696
+ for (const b of this.bands) {
697
+ b.phase += 2 * Math.PI * b.freq * dt / 1000;
698
+ b.phase %= (2 * Math.PI);
699
+ }
700
+ }
701
+ getDrive(neuronId, N) {
702
+ const phi = 2 * Math.PI * neuronId / N;
703
+ const theta = this.bands[2];
704
+ const gamma = this.bands[4];
705
+ const slow = this.bands[1];
706
+ const infra = this.bands[0];
707
+ const gateTheta = Math.max(0, Math.cos(theta.phase + phi));
708
+ const gammaSig = gamma.amplitude * gateTheta * Math.sin(gamma.phase + 2 * phi);
709
+ const slowSig = slow.amplitude * Math.sin(slow.phase + 0.3 * phi);
710
+ const infraSig = infra.amplitude * Math.sin(infra.phase);
711
+ return this.coupling * (gammaSig + 0.5 * slowSig + 0.3 * infraSig);
712
+ }
713
+ }
714
+
715
+ class NeuromodulatorSystem {
716
+ constructor() {
717
+ this.modulators = {
718
+ DA: { tonic: 0.5, phasic: 0, tauTonic: 5000, tauPhasic: 200, releaseRate: 0.3 },
719
+ SHT: { tonic: 0.5, phasic: 0, tauTonic: 8000, tauPhasic: 500, releaseRate: 0.2 },
720
+ ACh: { tonic: 0.5, phasic: 0, tauTonic: 4000, tauPhasic: 150, releaseRate: 0.25 },
721
+ NA: { tonic: 0.5, phasic: 0, tauTonic: 6000, tauPhasic: 300, releaseRate: 0.2 },
722
+ };
723
+ this.receptors = {
724
+ D1: new ReceptorSubtype('D1', 0.4, 1.0, false),
725
+ D2: new ReceptorSubtype('D2', 0.6, 0.8, true),
726
+ SHT1A: new ReceptorSubtype('5HT1A', 0.3, 1.0, true),
727
+ SHT2A: new ReceptorSubtype('5HT2A', 0.5, 0.9, false),
728
+ SHT4: new ReceptorSubtype('5HT4', 0.4, 0.7, false),
729
+ M1: new ReceptorSubtype('M1', 0.35, 1.0, false),
730
+ M2: new ReceptorSubtype('M2', 0.5, 0.6, true),
731
+ B1: new ReceptorSubtype('Ξ²1', 0.4, 0.9, false),
732
+ A2: new ReceptorSubtype('Ξ±2', 0.3, 0.7, true),
733
+ };
734
+ this.externalOverrides = { DA: null, SHT: null, ACh: null, NA: null };
735
+ }
736
+ update(activity, dt) {
737
+ const { excFrac, meanAct, changeRate } = activity;
738
+ for (const [key, m] of Object.entries(this.modulators)) {
739
+ m.tonic += (0.5 - m.tonic) * dt / m.tauTonic;
740
+ m.phasic *= Math.exp(-dt / m.tauPhasic);
741
+ }
742
+ this.modulators.DA.phasic += this.modulators.DA.releaseRate * changeRate * dt / 1000;
743
+ this.modulators.SHT.tonic += this.modulators.SHT.releaseRate * meanAct * dt / 5000;
744
+ this.modulators.ACh.phasic += this.modulators.ACh.releaseRate * excFrac * dt / 1000;
745
+ this.modulators.NA.phasic += this.modulators.NA.releaseRate * changeRate * dt / 1000;
746
+ this.modulators.ACh.phasic *= (1 - 0.1 * this.modulators.DA.phasic);
747
+ this.modulators.SHT.tonic += 0.02 * (this.modulators.NA.tonic + this.modulators.NA.phasic) * dt / 1000;
748
+ for (const m of Object.values(this.modulators)) {
749
+ m.tonic = clamp(m.tonic, 0, 1);
750
+ m.phasic = clamp(m.phasic, 0, 1);
751
+ }
752
+ for (const [key, val] of Object.entries(this.externalOverrides)) {
753
+ if (val !== null) this.modulators[key].tonic = val;
754
+ }
755
+ }
756
+ computeReceptorActivations() {
757
+ const R = {};
758
+ const getConc = (modKey, isTonic) => {
759
+ const m = this.modulators[modKey];
760
+ return isTonic ? m.tonic : m.tonic + m.phasic;
761
+ };
762
+ R.D1 = this.receptors.D1.computeActivation(getConc('DA', false));
763
+ R.D2 = this.receptors.D2.computeActivation(getConc('DA', true));
764
+ R.SHT1A = this.receptors.SHT1A.computeActivation(getConc('SHT', true));
765
+ R.SHT2A = this.receptors.SHT2A.computeActivation(getConc('SHT', false));
766
+ R.SHT4 = this.receptors.SHT4.computeActivation(getConc('SHT', false));
767
+ R.M1 = this.receptors.M1.computeActivation(getConc('ACh', false));
768
+ R.M2 = this.receptors.M2.computeActivation(getConc('ACh', true));
769
+ R.B1 = this.receptors.B1.computeActivation(getConc('NA', false));
770
+ R.A2 = this.receptors.A2.computeActivation(getConc('NA', true));
771
+ return R;
772
+ }
773
+ }
774
+
775
+ class NeuraxonSynapse {
776
+ constructor(preId, postId, branchId) {
777
+ this.preId = preId; this.postId = postId; this.branchId = branchId;
778
+ this.wFast = (Math.random() - 0.5) * 0.6;
779
+ this.wSlow = (Math.random() - 0.5) * 0.3;
780
+ this.wMeta = (Math.random() - 0.5) * 0.1;
781
+ this.tauFast = 50; this.tauSlow = 500; this.tauMeta = 5000;
782
+ this.preTrace = 0; this.postTrace = 0;
783
+ this.recentDw = 0; this.integrity = 1.0;
784
+ this.silent = Math.random() < 0.05;
785
+ }
786
+ computeInput(preState) {
787
+ if (this.silent) return 0;
788
+ return (this.wFast + this.wSlow) * preState;
789
+ }
790
+ getModulatoryEffect() { return this.wMeta; }
791
+ update(preState, postState, R, neighborDws, dt) {
792
+ const tauSTDP = 150;
793
+ this.preTrace += (-this.preTrace / tauSTDP + (preState === 1 ? 1 : 0)) * dt / 1000;
794
+ this.postTrace += (-this.postTrace / tauSTDP + (postState === 1 ? 1 : 0)) * dt / 1000;
795
+ const Aplus = this.preTrace * (postState === 1 ? 1 : 0);
796
+ const Aminus = this.postTrace * (preState === 1 ? 1 : 0);
797
+ // Base learning rate β€” slight boost for turn output neurons to compensate for rarity
798
+ const isTurnTarget = (this.postId === 32 || this.postId === 33);
799
+ const eta = isTurnTarget ? 0.06 : 0.05;
800
+ const d1 = R.D1 || 0.5;
801
+ const d2 = R.D2 || 0.5;
802
+ let dw = eta * Aplus * d1 - eta * 0.6 * Aminus * d2;
803
+ if (preState === 1 && postState === 1) dw += eta * 0.3 * d1;
804
+ if (preState === 1 && postState === -1) dw -= eta * 0.2 * d2;
805
+ if (postState === 0) dw *= 0.1;
806
+ if (neighborDws.length > 0) {
807
+ let assoc = 0;
808
+ for (const nd of neighborDws) assoc += nd / (1 + Math.random());
809
+ dw += 0.01 * assoc;
810
+ }
811
+ this.recentDw = dw;
812
+ this.wFast += (dt / this.tauFast) * (-this.wFast * 0.0005 + 0.5 * dw);
813
+ const slowRate = isTurnTarget ? 0.18 : 0.15;
814
+ this.wSlow += (dt / this.tauSlow) * (-this.wSlow * 0.0005 + slowRate * dw);
815
+ this.wFast = clamp(this.wFast, -1, 1);
816
+ this.wSlow = clamp(this.wSlow, -1, 1);
817
+ const shtFactor = 0.5 * (R.SHT2A || 0.5) + 0.1 * (1 - (R.SHT1A || 0.5));
818
+ this.wMeta += (dt / this.tauMeta) * (-this.wMeta * 0.0005 + 0.05 * dw * shtFactor);
819
+ this.wMeta = clamp(this.wMeta, -0.5, 0.5);
820
+ const activityBonus = (Math.abs(preState) + Math.abs(postState)) * 0.00005;
821
+ this.integrity += (-0.00001 + activityBonus) * dt;
822
+ this.integrity = clamp(this.integrity, 0, 1);
823
+ if (this.silent && Math.abs(preState) === 1 && Math.abs(postState) === 1 && Math.random() < 0.02) {
824
+ this.silent = false;
825
+ }
826
+ }
827
+ }
828
+
829
+ class Neuraxon {
830
+ constructor(id, type) {
831
+ this.id = id; this.type = type;
832
+ this.s = 0; this.state = 0;
833
+ this.theta1 = 0.5; this.theta2 = -0.5;
834
+ this.adaptation = 0; this.autoReceptor = 0;
835
+ this.rBar = 0; this.targetRate = 0.3;
836
+ this.health = 1.0; this.baseRate = 0.05;
837
+ this.tau = 80 + Math.random() * 60;
838
+ this.branches = 3;
839
+ this.x = 0; this.y = 0;
840
+ this.prevState = 0;
841
+ }
842
+ update(branchInputs, modInputs, Iext, oscDrive, R, dt) {
843
+ const alpha = 0.001;
844
+ this.rBar += alpha * (Math.abs(this.state) - this.rBar) * dt / 1000;
845
+ const thetaDend = 0.8;
846
+ let D = 0;
847
+ for (let b = 0; b < this.branches; b++) {
848
+ const inputs = branchInputs[b] || [];
849
+ let sigma = 0;
850
+ for (const inp of inputs) sigma += inp;
851
+ if (Math.abs(sigma) > thetaDend) {
852
+ D += Math.sign(sigma) * Math.pow(Math.abs(sigma), 1.3);
853
+ } else {
854
+ D += sigma;
855
+ }
856
+ }
857
+ const gNA = 1 + 0.5 * (R.B1 || 0.5) + 0.2 * (R.A2 || 0.5);
858
+ const spont = (this.baseRate + 0.3 * (R.A2 || 0.3)) * (Math.random() - 0.4);
859
+ this.s += (dt / this.tau) * (-this.s + gNA * D + Iext + oscDrive - this.adaptation + spont);
860
+ let rawMod = 0;
861
+ for (const m of (modInputs || [])) rawMod += m;
862
+ rawMod += 0.3 * (R.M1 || 0.5) - 0.2 * (R.M2 || 0.3);
863
+ const dThetaMeta = 0.3 * Math.tanh(rawMod);
864
+ const etaH = 0.01;
865
+ const dThetaHomeo = etaH * (this.rBar - this.targetRate);
866
+ const theta1Eff = this.theta1 - dThetaMeta + dThetaHomeo - 0.1 * this.autoReceptor;
867
+ const theta2Eff = this.theta2 - dThetaMeta + dThetaHomeo + 0.1 * this.autoReceptor;
868
+ this.prevState = this.state;
869
+ if (this.s > theta1Eff) this.state = 1;
870
+ else if (this.s < theta2Eff) this.state = -1;
871
+ else this.state = 0;
872
+ const tauA = 300, tauR = 1000;
873
+ this.adaptation += (dt / tauA) * (-this.adaptation + 0.1 * Math.abs(this.state));
874
+ this.autoReceptor += (dt / tauR) * (-this.autoReceptor + 0.2 * this.state);
875
+ this.health -= 0.00001 * (1 - Math.abs(this.state)) * dt / 1000;
876
+ this.health = clamp(this.health, 0.1, 1);
877
+ }
878
+ }
879
+
880
+ class NeuraxonNetwork {
881
+ constructor(nInput, nHidden, nOutput) {
882
+ this.neurons = [];
883
+ this.synapses = [];
884
+ this.neuromod = new NeuromodulatorSystem();
885
+ this.oscillators = new OscillatorBank();
886
+ this.energy = 0;
887
+ this.step = 0;
888
+ this.meanDw = 0;
889
+ this.structEvent = '';
890
+ for (let i = 0; i < nInput; i++) this.neurons.push(new Neuraxon(i, 'input'));
891
+ for (let i = 0; i < nHidden; i++) this.neurons.push(new Neuraxon(nInput + i, 'hidden'));
892
+ for (let i = 0; i < nOutput; i++) this.neurons.push(new Neuraxon(nInput + nHidden + i, 'output'));
893
+ this.N = this.neurons.length;
894
+ this.nInput = nInput; this.nHidden = nHidden; this.nOutput = nOutput;
895
+ this._buildSmallWorld(6, 0.2);
896
+ this._addMotorPathways();
897
+ this._layoutNeurons();
898
+ }
899
+ _addMotorPathways() {
900
+ const outStart = this.nInput + this.nHidden;
901
+ // Direct input→output: turn pathways get a slight head start
902
+ const directMap = [[0, 0], [1, 2], [2, 3], [3, 1]];
903
+ const turnOutputs = new Set([2, 3]);
904
+ for (const [inp, out] of directMap) {
905
+ const syn = new NeuraxonSynapse(inp, outStart + out, 0);
906
+ const isTurn = turnOutputs.has(out);
907
+ syn.wFast = isTurn ? 0.7 + Math.random() * 0.15 : 0.6 + Math.random() * 0.2;
908
+ syn.wSlow = isTurn ? 0.35 + Math.random() * 0.1 : 0.3 + Math.random() * 0.1;
909
+ syn.integrity = 1.0;
910
+ syn.silent = false;
911
+ this.synapses.push(syn);
912
+ }
913
+ for (let h = this.nInput; h < outStart; h++) {
914
+ for (let o = outStart; o < this.N; o++) {
915
+ const isTurnOut = (o === outStart + 2 || o === outStart + 3);
916
+ const connProb = isTurnOut ? 0.45 : 0.4;
917
+ if (Math.random() < connProb) {
918
+ const syn = new NeuraxonSynapse(h, o, Math.floor(Math.random() * 3));
919
+ syn.wFast = (Math.random() - 0.5) * 0.4;
920
+ syn.wSlow = (Math.random() - 0.5) * 0.2;
921
+ this.synapses.push(syn);
922
+ }
923
+ }
924
+ }
925
+ for (let i = 0; i < this.nInput; i++) {
926
+ for (let h = this.nInput; h < outStart; h++) {
927
+ if (Math.random() < 0.35) {
928
+ const syn = new NeuraxonSynapse(i, h, Math.floor(Math.random() * 3));
929
+ syn.wFast = (Math.random() - 0.5) * 0.5;
930
+ syn.wSlow = (Math.random() - 0.5) * 0.2;
931
+ this.synapses.push(syn);
932
+ }
933
+ }
934
+ }
935
+ for (let i = 4; i <= 5; i++) {
936
+ for (let h = this.nInput; h < outStart; h++) {
937
+ if (Math.random() < 0.5) {
938
+ const syn = new NeuraxonSynapse(i, h, Math.floor(Math.random() * 3));
939
+ syn.wFast = (Math.random() - 0.3) * 0.3;
940
+ this.synapses.push(syn);
941
+ }
942
+ }
943
+ }
944
+ }
945
+ _buildSmallWorld(k, beta) {
946
+ const N = this.N;
947
+ const halfK = Math.floor(k / 2);
948
+ const edges = new Set();
949
+ for (let i = 0; i < N; i++) {
950
+ for (let j = 1; j <= halfK; j++) {
951
+ const target = (i + j) % N;
952
+ const key = Math.min(i, target) + '-' + Math.max(i, target);
953
+ if (!edges.has(key)) {
954
+ edges.add(key);
955
+ const branchId = Math.floor(Math.random() * 3);
956
+ this.synapses.push(new NeuraxonSynapse(i, target, branchId));
957
+ }
958
+ }
959
+ }
960
+ const synCopy = [...this.synapses];
961
+ for (const syn of synCopy) {
962
+ if (Math.random() < beta) {
963
+ let newTarget;
964
+ do { newTarget = Math.floor(Math.random() * N); } while (newTarget === syn.preId);
965
+ syn.postId = newTarget;
966
+ syn.branchId = Math.floor(Math.random() * 3);
967
+ }
968
+ }
969
+ }
970
+ _layoutNeurons() {
971
+ // ═══ ENHANCED POLAR / RADIAL LAYOUT ═══
972
+ // Spread neurons in polar coordinates with staggered radii for visual depth
973
+ const cx = 0, cy = 0; // world-space center
974
+
975
+ // Input neurons: inner ring with slight jitter
976
+ for (let i = 0; i < this.nInput; i++) {
977
+ const angle = (2 * Math.PI * i / this.nInput) - Math.PI / 2;
978
+ const r = 80 + (i % 2) * 12;
979
+ this.neurons[i].x = cx + r * Math.cos(angle);
980
+ this.neurons[i].y = cy + r * Math.sin(angle);
981
+ }
982
+ // Hidden neurons: two staggered concentric bands
983
+ const hidStart = this.nInput;
984
+ for (let i = 0; i < this.nHidden; i++) {
985
+ const angle = (2 * Math.PI * i / this.nHidden) - Math.PI / 2;
986
+ const band = (i % 3 === 0) ? 190 : (i % 3 === 1) ? 230 : 260;
987
+ const jitter = (Math.sin(i * 2.7) * 15);
988
+ this.neurons[hidStart + i].x = cx + (band + jitter) * Math.cos(angle);
989
+ this.neurons[hidStart + i].y = cy + (band + jitter) * Math.sin(angle);
990
+ }
991
+ // Output neurons: outer ring
992
+ const outStart = this.nInput + this.nHidden;
993
+ for (let i = 0; i < this.nOutput; i++) {
994
+ const angle = (2 * Math.PI * i / this.nOutput) - Math.PI / 2;
995
+ const r = 350;
996
+ this.neurons[outStart + i].x = cx + r * Math.cos(angle);
997
+ this.neurons[outStart + i].y = cy + r * Math.sin(angle);
998
+ }
999
+ }
1000
+ setInputs(values) {
1001
+ for (let i = 0; i < Math.min(values.length, this.nInput); i++) {
1002
+ const n = this.neurons[i];
1003
+ n.s = values[i] * 2.0;
1004
+ n.state = values[i] > 0.2 ? 1 : values[i] < -0.2 ? -1 : 0;
1005
+ n.adaptation = 0;
1006
+ n.autoReceptor = 0;
1007
+ }
1008
+ }
1009
+ getOutputStates() {
1010
+ const start = this.nInput + this.nHidden;
1011
+ return this.neurons.slice(start).map(n => n.state);
1012
+ }
1013
+ getOutputValues() {
1014
+ const start = this.nInput + this.nHidden;
1015
+ return this.neurons.slice(start).map(n => n.s);
1016
+ }
1017
+ simulateStep(dt) {
1018
+ this.step++;
1019
+ let excCount = 0, inhCount = 0, neuCount = 0, totalAbs = 0, changes = 0;
1020
+ for (const n of this.neurons) {
1021
+ if (n.state === 1) excCount++;
1022
+ else if (n.state === -1) inhCount++;
1023
+ else neuCount++;
1024
+ totalAbs += Math.abs(n.state);
1025
+ if (n.state !== n.prevState) changes++;
1026
+ }
1027
+ const activity = {
1028
+ excFrac: excCount / this.N,
1029
+ meanAct: totalAbs / this.N,
1030
+ changeRate: changes / this.N,
1031
+ };
1032
+ this.neuromod.update(activity, dt);
1033
+ this.oscillators.update(dt);
1034
+ const R = this.neuromod.computeReceptorActivations();
1035
+ const branchInputs = new Array(this.N);
1036
+ const modInputs = new Array(this.N);
1037
+ for (let i = 0; i < this.N; i++) {
1038
+ branchInputs[i] = [[], [], []];
1039
+ modInputs[i] = [];
1040
+ }
1041
+ for (const syn of this.synapses) {
1042
+ const preState = this.neurons[syn.preId].state;
1043
+ const input = syn.computeInput(preState);
1044
+ if (branchInputs[syn.postId] && branchInputs[syn.postId][syn.branchId]) {
1045
+ branchInputs[syn.postId][syn.branchId].push(input);
1046
+ }
1047
+ if (modInputs[syn.postId]) {
1048
+ modInputs[syn.postId].push(syn.getModulatoryEffect());
1049
+ }
1050
+ }
1051
+ for (const n of this.neurons) {
1052
+ if (n.type === 'input') continue;
1053
+ const osc = this.oscillators.getDrive(n.id, this.N);
1054
+ n.update(branchInputs[n.id], modInputs[n.id], 0, osc, R, dt);
1055
+ }
1056
+ let totalDw = 0;
1057
+ for (const syn of this.synapses) {
1058
+ const pre = this.neurons[syn.preId];
1059
+ const post = this.neurons[syn.postId];
1060
+ const neighbors = this.synapses
1061
+ .filter(s => s.postId === syn.postId && s !== syn && s.branchId === syn.branchId)
1062
+ .slice(0, 3)
1063
+ .map(s => s.recentDw);
1064
+ syn.update(pre.state, post.state, R, neighbors, dt);
1065
+ totalDw += Math.abs(syn.recentDw);
1066
+ }
1067
+ this.meanDw = this.synapses.length > 0 ? totalDw / this.synapses.length : 0;
1068
+ this.structEvent = '';
1069
+ const toRemove = [];
1070
+ for (let i = this.synapses.length - 1; i >= 0; i--) {
1071
+ if (this.synapses[i].integrity < 0.1) { toRemove.push(i); }
1072
+ }
1073
+ for (const idx of toRemove) { this.synapses.splice(idx, 1); this.structEvent = 'pruned'; }
1074
+ if (Math.random() < 0.002 && this.synapses.length < this.N * 8) {
1075
+ const a = Math.floor(Math.random() * this.N);
1076
+ const b = Math.floor(Math.random() * this.N);
1077
+ if (a !== b) {
1078
+ this.synapses.push(new NeuraxonSynapse(a, b, Math.floor(Math.random() * 3)));
1079
+ this.structEvent = 'formed';
1080
+ }
1081
+ }
1082
+ this.energy += 0.01 * excCount * dt / 1000;
1083
+ return { excCount, inhCount, neuCount, R, activity };
1084
+ }
1085
+ }
1086
+
1087
+
1088
+ // ═══════════════════════════════════════════════════════════════
1089
+ // APPLICATION LAYER
1090
+ // ═══════════��═══════════════════════════════════════════════════
1091
+
1092
+ const $ = id => document.getElementById(id);
1093
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
1094
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
1095
+
1096
+ function sysLog(msg) {
1097
+ const el = $('logPanel');
1098
+ const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
1099
+ el.textContent += `[${ts}] ${msg}\n`;
1100
+ el.scrollTop = el.scrollHeight;
1101
+ }
1102
+
1103
+ let sphero = null;
1104
+ let isConnected = false;
1105
+ let controlMode = 'manual';
1106
+ let heading = 0;
1107
+ const BASE_SPEED = 90;
1108
+ const TURN_DEG = 8;
1109
+ const activeKeys = new Set();
1110
+ let simRunning = true;
1111
+
1112
+ const outputAccum = [0, 0, 0, 0];
1113
+ const OUTPUT_DECAY = 0.92;
1114
+ const OUTPUT_GAIN = 0.5;
1115
+
1116
+ let teachingActive = false;
1117
+ let teachingTarget = [0, 0, 0, 0];
1118
+ let learnCount = 0;
1119
+ let lastLearnLog = 0;
1120
+
1121
+ // ── STOP learning: track idle to teach "all-off" as a real behavior ──
1122
+ let idleTicks = 0;
1123
+ const IDLE_TEACH_DELAY = 25; // ~0.4s no keys β†’ teach "stop"
1124
+
1125
+ // ── Movement replay: remember human sequences for Neuraxon mode ──
1126
+ const replayBuffer = []; // { inputs: [...], target: [...] }
1127
+ const REPLAY_MAX = 200;
1128
+ let replayIdx = 0;
1129
+ let isReplaying = false;
1130
+
1131
+ let learnHits = 0;
1132
+ let learnTrials = 0;
1133
+ let learnAccuracy = 0;
1134
+ const LEARN_WINDOW = 300;
1135
+ const learnHistory = [];
1136
+ let nxonContributing = false;
1137
+
1138
+ let explorePhase = 0;
1139
+ let exploreBehavior = 0;
1140
+ let exploreTimer = 0;
1141
+ const EXPLORE_DURATION = 160; // ticks per behavior (~2.6s)
1142
+
1143
+ const network = new NeuraxonNetwork(6, 24, 4);
1144
+
1145
+ const activityHistory = [];
1146
+ const MAX_HIST = 200;
1147
+
1148
+ // --- Build receptor bars UI ---
1149
+ function buildReceptorUI() {
1150
+ const container = $('receptorBars');
1151
+ const receptorNames = ['D1','D2','5HT1A','5HT2A','5HT4','M1','M2','Ξ²1','Ξ±2'];
1152
+ const colors = {
1153
+ D1: 'var(--da-color)', D2: 'var(--da-color)',
1154
+ '5HT1A': 'var(--sht-color)', '5HT2A': 'var(--sht-color)', '5HT4': 'var(--sht-color)',
1155
+ M1: 'var(--ach-color)', M2: 'var(--ach-color)',
1156
+ 'Ξ²1': 'var(--na-color)', 'Ξ±2': 'var(--na-color)',
1157
+ };
1158
+ container.innerHTML = '';
1159
+ for (const name of receptorNames) {
1160
+ const row = document.createElement('div');
1161
+ row.style.cssText = 'display:flex;align-items:center;gap:6px;';
1162
+ row.innerHTML = `
1163
+ <span style="font-family:var(--mono);font-size:9px;width:38px;text-align:right;color:${colors[name]};font-weight:600;">${name}</span>
1164
+ <div style="flex:1;height:4px;background:var(--bg-abyss);border-radius:2px;overflow:hidden;">
1165
+ <div id="rbar_${name.replace(/[^a-zA-Z0-9]/g,'')}" style="height:100%;width:0%;background:${colors[name]};border-radius:2px;transition:width 0.15s;"></div>
1166
+ </div>
1167
+ `;
1168
+ container.appendChild(row);
1169
+ }
1170
+ }
1171
+ buildReceptorUI();
1172
+
1173
+ ['DA','5HT','ACh','NA'].forEach(key => {
1174
+ const sliderId = key === '5HT' ? 'slider5HT' : `slider${key}`;
1175
+ const valId = key === '5HT' ? 'val5HT' : `val${key}`;
1176
+ const modKey = key === '5HT' ? 'SHT' : key;
1177
+ $(sliderId).addEventListener('input', e => {
1178
+ const v = e.target.value / 100;
1179
+ $(valId).textContent = v.toFixed(2);
1180
+ network.neuromod.externalOverrides[modKey] = v;
1181
+ });
1182
+ });
1183
+
1184
+ function setMode(mode) {
1185
+ controlMode = mode;
1186
+ document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === mode));
1187
+ $('modeLabel').textContent = mode.toUpperCase();
1188
+ const driveCard = $('driveCard');
1189
+ if (mode === 'neuraxon') {
1190
+ driveCard.classList.add('controls-disabled');
1191
+ } else {
1192
+ driveCard.classList.remove('controls-disabled');
1193
+ }
1194
+ sysLog(`Mode: ${mode.toUpperCase()}`);
1195
+ }
1196
+
1197
+ function getHumanAction() {
1198
+ let speed = 0, turn = 0;
1199
+ if (activeKeys.has('up')) speed = BASE_SPEED;
1200
+ if (activeKeys.has('down')) speed = -BASE_SPEED;
1201
+ if (activeKeys.has('left')) turn = -TURN_DEG;
1202
+ if (activeKeys.has('right')) turn = TURN_DEG;
1203
+ if (activeKeys.has('up') && activeKeys.has('left')) { speed = BASE_SPEED * 0.7; turn = -TURN_DEG; }
1204
+ if (activeKeys.has('up') && activeKeys.has('right')) { speed = BASE_SPEED * 0.7; turn = TURN_DEG; }
1205
+ return { speed, turn };
1206
+ }
1207
+
1208
+ function applyTeachingSignal() {
1209
+ const outStart = network.nInput + network.nHidden;
1210
+ teachingTarget = [0, 0, 0, 0];
1211
+ teachingActive = false;
1212
+
1213
+ if (activeKeys.has('up')) { teachingTarget[0] = 1; teachingActive = true; }
1214
+ if (activeKeys.has('down')) { teachingTarget[1] = 1; teachingActive = true; }
1215
+ if (activeKeys.has('left')) { teachingTarget[2] = 1; teachingActive = true; }
1216
+ if (activeKeys.has('right')) { teachingTarget[3] = 1; teachingActive = true; }
1217
+
1218
+ // ── STOP TEACHING: when human releases keys, teach "no output" ──
1219
+ if (!teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) {
1220
+ idleTicks++;
1221
+ // After a brief pause, actively teach the network to be quiet
1222
+ if (idleTicks > IDLE_TEACH_DELAY && idleTicks < IDLE_TEACH_DELAY + 60) {
1223
+ teachingActive = true;
1224
+ // All targets stay 0 β€” this IS the stop signal
1225
+ for (let i = 0; i < 4; i++) {
1226
+ const n = network.neurons[outStart + i];
1227
+ n.s = -0.3;
1228
+ n.state = 0;
1229
+ }
1230
+ // Mild DA for the stop-learning (smaller than move-learning)
1231
+ network.neuromod.modulators.DA.phasic = clamp(
1232
+ network.neuromod.modulators.DA.phasic + 0.08, 0, 1
1233
+ );
1234
+ learnCount++;
1235
+ return; // don't also run the movement clamp below
1236
+ }
1237
+ } else {
1238
+ idleTicks = 0;
1239
+ }
1240
+
1241
+ if (teachingActive && (controlMode === 'manual' || controlMode === 'hybrid')) {
1242
+ const isTurnTeach = (teachingTarget[2] === 1 || teachingTarget[3] === 1);
1243
+
1244
+ // ── Record to replay buffer (raw inputβ†’target pair) ──
1245
+ const currentInputs = computeSensoryInputs();
1246
+ replayBuffer.push({ inputs: [...currentInputs], target: [...teachingTarget] });
1247
+ if (replayBuffer.length > REPLAY_MAX) replayBuffer.shift();
1248
+
1249
+ // ── MEASURE ACCURACY BEFORE CLAMPING ──
1250
+ let hits = 0, evaluated = 0;
1251
+ for (let i = 0; i < 4; i++) {
1252
+ const n = network.neurons[outStart + i];
1253
+ const naturalState = n.state;
1254
+ const desired = teachingTarget[i];
1255
+ if (desired === 1) {
1256
+ evaluated++;
1257
+ if (naturalState === 1) hits++;
1258
+ } else {
1259
+ evaluated++;
1260
+ if (naturalState <= 0) hits++;
1261
+ }
1262
+ }
1263
+ const hitRatio = evaluated > 0 ? hits / evaluated : 0;
1264
+ learnHistory.push(hitRatio);
1265
+ if (learnHistory.length > LEARN_WINDOW) learnHistory.shift();
1266
+ if (learnHistory.length > 10) {
1267
+ const sum = learnHistory.reduce((a, b) => a + b, 0);
1268
+ learnAccuracy = Math.round((sum / learnHistory.length) * 100);
1269
+ }
1270
+
1271
+ // ── CLAMP OUTPUT NEURONS ──
1272
+ for (let i = 0; i < 4; i++) {
1273
+ const n = network.neurons[outStart + i];
1274
+ if (teachingTarget[i] > 0) {
1275
+ n.s = n.theta1 + 0.8;
1276
+ n.state = 1;
1277
+ } else {
1278
+ n.s = -0.2;
1279
+ n.state = 0;
1280
+ }
1281
+ }
1282
+
1283
+ // ── NEUROMODULATOR BURSTS β€” moderate turn bonus ──
1284
+ const daBoost = isTurnTeach ? 0.25 : 0.2;
1285
+ network.neuromod.modulators.DA.phasic = clamp(
1286
+ network.neuromod.modulators.DA.phasic + daBoost, 0, 1
1287
+ );
1288
+ const achBase = 0.12 * (1 - learnAccuracy / 150);
1289
+ const achBoost = isTurnTeach ? Math.max(0.05, achBase) : Math.max(0.03, achBase);
1290
+ network.neuromod.modulators.ACh.phasic = clamp(
1291
+ network.neuromod.modulators.ACh.phasic + achBoost, 0, 1
1292
+ );
1293
+ network.neuromod.modulators.NA.phasic = clamp(
1294
+ network.neuromod.modulators.NA.phasic + 0.05, 0, 1
1295
+ );
1296
+
1297
+ learnCount++;
1298
+ if (learnCount - lastLearnLog >= 200) {
1299
+ sysLog(`Learn: ${learnCount} steps | acc: ${learnAccuracy}% | ${isTurnTeach ? '↻ turn' : '↑ fwd'}`);
1300
+ lastLearnLog = learnCount;
1301
+ }
1302
+ }
1303
+ }
1304
+
1305
+ function getNeuraxonAction() {
1306
+ const outStart = network.nInput + network.nHidden;
1307
+ for (let i = 0; i < 4; i++) {
1308
+ const state = network.neurons[outStart + i].state;
1309
+ outputAccum[i] = outputAccum[i] * OUTPUT_DECAY + state * OUTPUT_GAIN;
1310
+ outputAccum[i] = clamp(outputAccum[i], -2.0, 2.0);
1311
+ }
1312
+ const fwd = Math.max(0, outputAccum[0]);
1313
+ const back = Math.max(0, outputAccum[1]);
1314
+ const left = Math.max(0, outputAccum[2]);
1315
+ const right = Math.max(0, outputAccum[3]);
1316
+ const rawSpeed = fwd - back * 0.5;
1317
+ const speed = clamp(Math.round(rawSpeed * BASE_SPEED * 0.9), 0, 180);
1318
+ const turnRaw = (right - left);
1319
+ const turn = turnRaw * TURN_DEG * 2.8;
1320
+ nxonContributing = (speed > 12 || Math.abs(turn) > 2);
1321
+ return { speed, turn };
1322
+ }
1323
+
1324
+ function computeSensoryInputs() {
1325
+ const inputs = new Array(6).fill(0);
1326
+ if (controlMode === 'manual' || controlMode === 'hybrid') {
1327
+ if (activeKeys.has('up')) inputs[0] = 1.0;
1328
+ if (activeKeys.has('left')) inputs[1] = 1.0;
1329
+ if (activeKeys.has('right')) inputs[2] = 1.0;
1330
+ if (activeKeys.has('down')) inputs[3] = 1.0;
1331
+ inputs[4] = Math.sin(Date.now() / 4000) * 0.2;
1332
+ inputs[5] = Math.cos(Date.now() / 5000) * 0.15;
1333
+ }
1334
+ if (controlMode === 'hybrid') {
1335
+ inputs[4] += Math.sin(Date.now() / 2000) * 0.25;
1336
+ inputs[5] += Math.cos(Date.now() / 2500) * 0.2;
1337
+ if (nxonContributing) {
1338
+ inputs[4] += outputAccum[0] * 0.15;
1339
+ inputs[5] += (outputAccum[3] - outputAccum[2]) * 0.1;
1340
+ }
1341
+ }
1342
+ if (controlMode === 'neuraxon') {
1343
+ // ── REPLAY-FIRST STRATEGY: replay what the human actually did ──
1344
+ // If we have recorded moves, prefer replaying them over random exploration
1345
+ if (replayBuffer.length > 20 && Math.random() < 0.6) {
1346
+ // Replay a stored input pattern β€” cycles through the buffer
1347
+ isReplaying = true;
1348
+ const frame = replayBuffer[replayIdx % replayBuffer.length];
1349
+ replayIdx++;
1350
+ // Feed the stored sensory inputs (first 4 channels = directional)
1351
+ for (let i = 0; i < 4; i++) {
1352
+ inputs[i] = frame.inputs[i] * 0.8; // slightly attenuated replay
1353
+ }
1354
+ } else {
1355
+ // ── EXPLORATION: balanced patterns including STOP ──
1356
+ isReplaying = false;
1357
+ exploreTimer++;
1358
+ if (exploreTimer >= EXPLORE_DURATION) {
1359
+ exploreTimer = 0;
1360
+ exploreBehavior = (exploreBehavior + 1) % 8;
1361
+ }
1362
+ const t = exploreTimer / EXPLORE_DURATION;
1363
+ const ramp = t < 0.15 ? t / 0.15 : t > 0.85 ? (1 - t) / 0.15 : 1.0;
1364
+ const intensity = (0.85 + 0.15 * Math.sin(Date.now() / 800)) * ramp;
1365
+ switch (exploreBehavior) {
1366
+ case 0: inputs[0] = intensity; break; // forward
1367
+ case 1: inputs[1] = intensity; break; // left
1368
+ case 2: inputs[2] = intensity; break; // right
1369
+ case 3: inputs[0] = intensity; inputs[2] = intensity * 0.6; break; // fwd + right
1370
+ case 4: inputs[0] = intensity; inputs[1] = intensity * 0.6; break; // fwd + left
1371
+ case 5: inputs[3] = intensity * 0.5; break; // back
1372
+ case 6: break; // STOP β€” all zero
1373
+ case 7: break; // STOP β€” all zero (2nd pause)
1374
+ }
1375
+ }
1376
+
1377
+ // Self-reinforcement: feed output accumulators back evenly
1378
+ const selfGain = 0.25;
1379
+ inputs[0] += clamp(outputAccum[0] * selfGain, 0, 0.3);
1380
+ inputs[1] += clamp(outputAccum[2] * selfGain, 0, 0.3);
1381
+ inputs[2] += clamp(outputAccum[3] * selfGain, 0, 0.3);
1382
+ inputs[3] += clamp(outputAccum[1] * selfGain, 0, 0.3);
1383
+
1384
+ // Rhythmic drive
1385
+ inputs[4] = Math.sin(Date.now() / 1000) * 0.5 + Math.sin(Date.now() / 3300) * 0.2;
1386
+ inputs[5] = Math.cos(Date.now() / 1400) * 0.4 + Math.cos(Date.now() / 4500) * 0.2;
1387
+ }
1388
+ for (let i = 0; i < 6; i++) inputs[i] = clamp(inputs[i], -1.5, 1.5);
1389
+ return inputs;
1390
+ }
1391
+
1392
+ // --- Drive loop ---
1393
+ let driveInterval = null;
1394
+ function startDriveLoop() {
1395
+ if (driveInterval) clearInterval(driveInterval);
1396
+ driveInterval = setInterval(() => {
1397
+ if (!isConnected || !sphero) return;
1398
+ const human = getHumanAction();
1399
+ const ai = getNeuraxonAction();
1400
+ let finalSpeed, finalTurn;
1401
+ if (controlMode === 'manual') {
1402
+ finalSpeed = Math.abs(human.speed);
1403
+ finalTurn = human.turn;
1404
+ if (human.speed < 0) finalTurn = 180;
1405
+ } else if (controlMode === 'hybrid') {
1406
+ const hSpd = Math.abs(human.speed);
1407
+ finalSpeed = Math.round(hSpd * 0.75 + ai.speed * 0.25);
1408
+ finalTurn = human.turn * 0.75 + ai.turn * 0.25;
1409
+ } else {
1410
+ finalSpeed = ai.speed;
1411
+ finalTurn = ai.turn;
1412
+ }
1413
+ heading = (heading + finalTurn + 360) % 360;
1414
+ sphero.roll(clamp(Math.round(finalSpeed), 0, 255), Math.round(heading)).catch(() => {});
1415
+
1416
+ if (controlMode === 'manual') {
1417
+ if (teachingActive && idleTicks > IDLE_TEACH_DELAY) {
1418
+ // Red-orange = teaching STOP
1419
+ sphero.setMainLED(200, 80, 50).catch(() => {});
1420
+ } else if (teachingActive) {
1421
+ sphero.setMainLED(250, 180, 30).catch(() => {});
1422
+ } else {
1423
+ sphero.setMainLED(40, 200, 140).catch(() => {});
1424
+ }
1425
+ } else if (controlMode === 'hybrid') {
1426
+ if (teachingActive && nxonContributing) sphero.setMainLED(220, 180, 255).catch(() => {});
1427
+ else if (teachingActive) sphero.setMainLED(250, 180, 30).catch(() => {});
1428
+ else if (nxonContributing) sphero.setMainLED(160, 100, 255).catch(() => {});
1429
+ else sphero.setMainLED(40, 140, 160).catch(() => {});
1430
+ } else {
1431
+ if (nxonContributing) sphero.setMainLED(140, 80, 255).catch(() => {});
1432
+ else sphero.setMainLED(30, 180, 220).catch(() => {});
1433
+ }
1434
+
1435
+ $('speedVal').textContent = Math.round(finalSpeed);
1436
+ $('headingVal').textContent = Math.round(heading) + 'Β°';
1437
+ $('headingNeedle').style.transform = `rotate(${heading}deg)`;
1438
+ }, 60);
1439
+ }
1440
+
1441
+ function stopDriveLoop() {
1442
+ if (driveInterval) { clearInterval(driveInterval); driveInterval = null; }
1443
+ if (sphero && isConnected) sphero.stop().catch(() => {});
1444
+ }
1445
+
1446
+ $('btnConnect').onclick = async () => {
1447
+ try {
1448
+ $('btnConnect').disabled = true;
1449
+ sphero = new SpheroMiniBLE();
1450
+ sphero.onDisconnect = () => {
1451
+ sysLog('Disconnected from Sphero.');
1452
+ stopDriveLoop();
1453
+ updateConnectionUI(false);
1454
+ sphero = null;
1455
+ };
1456
+ await sphero.connect();
1457
+ sysLog('Setting up...');
1458
+ await sphero.setMainLED(34, 211, 238);
1459
+ await sphero.resetYaw();
1460
+ heading = 0;
1461
+ updateConnectionUI(true);
1462
+ startDriveLoop();
1463
+ sysLog('Connected! Neuraxon brain online.');
1464
+ } catch (e) {
1465
+ sysLog(`Connection failed: ${e.message}`);
1466
+ updateConnectionUI(false);
1467
+ }
1468
+ };
1469
+
1470
+ $('btnDisconnect').onclick = async () => {
1471
+ sysLog('Disconnecting...');
1472
+ stopDriveLoop();
1473
+ if (sphero) {
1474
+ try { await sphero.setMainLED(0, 0, 0); } catch (e) {}
1475
+ await sphero.disconnect();
1476
+ }
1477
+ };
1478
+
1479
+ function updateConnectionUI(connected) {
1480
+ isConnected = connected;
1481
+ const s = $('connStatus');
1482
+ s.textContent = connected ? 'CONNECTED' : 'OFFLINE';
1483
+ s.className = 'status-badge ' + (connected ? 'on' : 'off');
1484
+ $('btnConnect').disabled = connected;
1485
+ $('btnDisconnect').disabled = !connected;
1486
+ }
1487
+
1488
+ // --- Controls ---
1489
+ function bindControls() {
1490
+ const dirs = { Up: 'up', Down: 'down', Left: 'left', Right: 'right' };
1491
+ for (const [btnSuffix, dir] of Object.entries(dirs)) {
1492
+ const btn = $('btn' + btnSuffix);
1493
+ const down = () => { activeKeys.add(dir); btn.classList.add('active'); };
1494
+ const up = () => { activeKeys.delete(dir); btn.classList.remove('active'); };
1495
+ btn.addEventListener('mousedown', down);
1496
+ btn.addEventListener('mouseup', up);
1497
+ btn.addEventListener('mouseleave', up);
1498
+ btn.addEventListener('touchstart', e => { e.preventDefault(); down(); });
1499
+ btn.addEventListener('touchend', e => { e.preventDefault(); up(); });
1500
+ }
1501
+ $('btnBrake').addEventListener('mousedown', () => activeKeys.clear());
1502
+ $('btnBrake').addEventListener('touchstart', e => { e.preventDefault(); activeKeys.clear(); });
1503
+
1504
+ document.addEventListener('keydown', e => {
1505
+ if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' ','w','a','s','d','W','A','S','D'].includes(e.key)) e.preventDefault();
1506
+ let dir = null;
1507
+ if (e.key === 'ArrowUp' || e.key.toLowerCase() === 'w') dir = 'up';
1508
+ else if (e.key === 'ArrowDown' || e.key.toLowerCase() === 's') dir = 'down';
1509
+ else if (e.key === 'ArrowLeft' || e.key.toLowerCase() === 'a') dir = 'left';
1510
+ else if (e.key === 'ArrowRight' || e.key.toLowerCase() === 'd') dir = 'right';
1511
+ else if (e.key === ' ') { activeKeys.clear(); return; }
1512
+ if (dir && !activeKeys.has(dir)) {
1513
+ activeKeys.add(dir);
1514
+ const btn = $('btn' + dir.charAt(0).toUpperCase() + dir.slice(1));
1515
+ if (btn) btn.classList.add('active');
1516
+ }
1517
+ });
1518
+ document.addEventListener('keyup', e => {
1519
+ let dir = null;
1520
+ if (e.key === 'ArrowUp' || e.key.toLowerCase() === 'w') dir = 'up';
1521
+ else if (e.key === 'ArrowDown' || e.key.toLowerCase() === 's') dir = 'down';
1522
+ else if (e.key === 'ArrowLeft' || e.key.toLowerCase() === 'a') dir = 'left';
1523
+ else if (e.key === 'ArrowRight' || e.key.toLowerCase() === 'd') dir = 'right';
1524
+ if (dir) {
1525
+ activeKeys.delete(dir);
1526
+ const btn = $('btn' + dir.charAt(0).toUpperCase() + dir.slice(1));
1527
+ if (btn) btn.classList.remove('active');
1528
+ }
1529
+ });
1530
+ }
1531
+ bindControls();
1532
+
1533
+
1534
+ // ═══════════════════════════════════════════════════════════════
1535
+ // ENHANCED INTERACTIVE VISUALIZATION
1536
+ // β€” Continuous Panning, Proximity Focus, Polar Layout
1537
+ // ═══════════════════════════════════════════════════════════════
1538
+
1539
+ const canvas = $('neuralCanvas');
1540
+ const ctx = canvas.getContext('2d');
1541
+ const oscCanvas = $('oscCanvas');
1542
+ const oscCtx = oscCanvas.getContext('2d');
1543
+ const actCanvas = $('activityCanvas');
1544
+ const actCtx = actCanvas.getContext('2d');
1545
+
1546
+ // ── Camera / Pan State ──
1547
+ let camX = 0, camY = 0; // world offset (panning)
1548
+ let camZoom = 1.0;
1549
+ let isDragging = false;
1550
+ let dragStartX = 0, dragStartY = 0;
1551
+ let dragStartCamX = 0, dragStartCamY = 0;
1552
+ let hasDragged = false; // track if user has interacted
1553
+
1554
+ // ── Focused Node State ──
1555
+ let focusedNodeId = -1;
1556
+ let focusAnimT = 0; // animation interpolant
1557
+
1558
+ // ── Synapse Pulse Particles ──
1559
+ const pulseParticles = [];
1560
+ const MAX_PARTICLES = 80;
1561
+
1562
+ function resizeCanvas() {
1563
+ const rect = canvas.parentElement.getBoundingClientRect();
1564
+ canvas.width = rect.width * window.devicePixelRatio;
1565
+ canvas.height = rect.height * window.devicePixelRatio;
1566
+ ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
1567
+ }
1568
+ window.addEventListener('resize', resizeCanvas);
1569
+ resizeCanvas();
1570
+
1571
+ // ── Mouse / Touch Handlers for Panning ──
1572
+ const panelCenter = $('panelCenter');
1573
+
1574
+ panelCenter.addEventListener('mousedown', e => {
1575
+ if (e.target !== canvas) return;
1576
+ isDragging = true;
1577
+ dragStartX = e.clientX;
1578
+ dragStartY = e.clientY;
1579
+ dragStartCamX = camX;
1580
+ dragStartCamY = camY;
1581
+ });
1582
+
1583
+ window.addEventListener('mousemove', e => {
1584
+ if (!isDragging) return;
1585
+ const dx = e.clientX - dragStartX;
1586
+ const dy = e.clientY - dragStartY;
1587
+ camX = dragStartCamX + dx / camZoom;
1588
+ camY = dragStartCamY + dy / camZoom;
1589
+ if (!hasDragged && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
1590
+ hasDragged = true;
1591
+ $('panHint').classList.add('hidden');
1592
+ }
1593
+ });
1594
+
1595
+ window.addEventListener('mouseup', () => { isDragging = false; });
1596
+
1597
+ // Zoom with scroll wheel
1598
+ panelCenter.addEventListener('wheel', e => {
1599
+ e.preventDefault();
1600
+ const delta = e.deltaY > 0 ? 0.92 : 1.08;
1601
+ camZoom = clamp(camZoom * delta, 0.3, 4.0);
1602
+ if (!hasDragged) { hasDragged = true; $('panHint').classList.add('hidden'); }
1603
+ }, { passive: false });
1604
+
1605
+ // Touch support for mobile
1606
+ let touchStartX = 0, touchStartY = 0, touchStartCamX = 0, touchStartCamY = 0;
1607
+ panelCenter.addEventListener('touchstart', e => {
1608
+ if (e.touches.length === 1) {
1609
+ isDragging = true;
1610
+ touchStartX = e.touches[0].clientX;
1611
+ touchStartY = e.touches[0].clientY;
1612
+ touchStartCamX = camX;
1613
+ touchStartCamY = camY;
1614
+ }
1615
+ }, { passive: true });
1616
+ panelCenter.addEventListener('touchmove', e => {
1617
+ if (!isDragging || e.touches.length !== 1) return;
1618
+ const dx = e.touches[0].clientX - touchStartX;
1619
+ const dy = e.touches[0].clientY - touchStartY;
1620
+ camX = touchStartCamX + dx / camZoom;
1621
+ camY = touchStartCamY + dy / camZoom;
1622
+ if (!hasDragged) { hasDragged = true; $('panHint').classList.add('hidden'); }
1623
+ }, { passive: true });
1624
+ panelCenter.addEventListener('touchend', () => { isDragging = false; });
1625
+
1626
+
1627
+ // ── World-to-Screen coordinate transforms ──
1628
+ function worldToScreen(wx, wy) {
1629
+ const w = canvas.width / window.devicePixelRatio;
1630
+ const h = canvas.height / window.devicePixelRatio;
1631
+ return {
1632
+ x: w / 2 + (wx + camX) * camZoom,
1633
+ y: h / 2 + (wy + camY) * camZoom,
1634
+ };
1635
+ }
1636
+
1637
+ function screenToWorld(sx, sy) {
1638
+ const w = canvas.width / window.devicePixelRatio;
1639
+ const h = canvas.height / window.devicePixelRatio;
1640
+ return {
1641
+ x: (sx - w / 2) / camZoom - camX,
1642
+ y: (sy - h / 2) / camZoom - camY,
1643
+ };
1644
+ }
1645
+
1646
+ // ── Find the nearest node to the screen center (focal point) ──
1647
+ function findFocalNode() {
1648
+ const w = canvas.width / window.devicePixelRatio;
1649
+ const h = canvas.height / window.devicePixelRatio;
1650
+ // Focal point is screen center
1651
+ const focal = screenToWorld(w / 2, h / 2);
1652
+ let minDist = Infinity;
1653
+ let nearestId = -1;
1654
+ for (const n of network.neurons) {
1655
+ const dx = n.x - focal.x;
1656
+ const dy = n.y - focal.y;
1657
+ const d = dx * dx + dy * dy;
1658
+ if (d < minDist) { minDist = d; nearestId = n.id; }
1659
+ }
1660
+ return nearestId;
1661
+ }
1662
+
1663
+ // ── Update Focus HUD ──
1664
+ function updateFocusHUD(nodeId) {
1665
+ const hud = $('focusHud');
1666
+ if (nodeId < 0) { hud.classList.remove('visible'); return; }
1667
+ hud.classList.add('visible');
1668
+ const n = network.neurons[nodeId];
1669
+ $('focusNodeId').textContent = '#' + n.id;
1670
+ $('focusNodeId').style.color = n.state === 1 ? '#22d3ee' : n.state === -1 ? '#f472b6' : '#475569';
1671
+ $('focusType').textContent = n.type.toUpperCase();
1672
+ $('focusType').style.color = n.type === 'input' ? '#fbbf24' : n.type === 'output' ? '#34d399' : '#7aa3c0';
1673
+ const stateLabel = n.state === 1 ? '+1 EXCITE' : n.state === -1 ? '-1 INHIBIT' : ' 0 NEUTRAL';
1674
+ $('focusState').textContent = stateLabel;
1675
+ $('focusState').style.color = n.state === 1 ? '#22d3ee' : n.state === -1 ? '#f472b6' : '#475569';
1676
+ $('focusS').textContent = n.s.toFixed(3);
1677
+ $('focusAdapt').textContent = n.adaptation.toFixed(4);
1678
+ $('focusHealth').textContent = (n.health * 100).toFixed(1) + '%';
1679
+ // Count synapses
1680
+ let synIn = 0, synOut = 0;
1681
+ for (const syn of network.synapses) {
1682
+ if (syn.postId === nodeId) synIn++;
1683
+ if (syn.preId === nodeId) synOut++;
1684
+ }
1685
+ $('focusSyn').textContent = `in:${synIn} out:${synOut}`;
1686
+ }
1687
+
1688
+
1689
+ // ═══════════════════════════════════════════════════════════════
1690
+ // DRAW NETWORK β€” Enhanced Interactive Polar Visualization
1691
+ // ═══════════════════════════════════════════════════════════════
1692
+
1693
+ const INPUT_LABELS = ['FWD', 'LEFT', 'RIGHT', 'BACK', 'EXPLORE', 'RHYTHM'];
1694
+ const OUTPUT_LABELS = ['SPD+', 'SPD-', 'TRN←', 'TRNβ†’'];
1695
+
1696
+ function drawNetwork(stats) {
1697
+ const w = canvas.width / window.devicePixelRatio;
1698
+ const h = canvas.height / window.devicePixelRatio;
1699
+ ctx.clearRect(0, 0, w, h);
1700
+
1701
+ const time = Date.now() / 1000;
1702
+
1703
+ // ── Background: Concentric polar grid rings ──
1704
+ ctx.save();
1705
+ const center = worldToScreen(0, 0);
1706
+ const ringRadii = [80, 190, 230, 260, 350];
1707
+ for (const r of ringRadii) {
1708
+ const sr = r * camZoom;
1709
+ ctx.beginPath();
1710
+ ctx.arc(center.x, center.y, sr, 0, Math.PI * 2);
1711
+ ctx.strokeStyle = 'rgba(34,211,238,0.04)';
1712
+ ctx.lineWidth = 1;
1713
+ ctx.stroke();
1714
+ }
1715
+ // Radial spokes
1716
+ for (let i = 0; i < 12; i++) {
1717
+ const angle = (Math.PI * 2 * i) / 12;
1718
+ const outer = 400 * camZoom;
1719
+ ctx.beginPath();
1720
+ ctx.moveTo(center.x, center.y);
1721
+ ctx.lineTo(center.x + outer * Math.cos(angle), center.y + outer * Math.sin(angle));
1722
+ ctx.strokeStyle = 'rgba(34,211,238,0.02)';
1723
+ ctx.lineWidth = 0.5;
1724
+ ctx.stroke();
1725
+ }
1726
+ ctx.restore();
1727
+
1728
+ // ── Find and update focal node ──
1729
+ const newFocusId = findFocalNode();
1730
+ if (newFocusId !== focusedNodeId) {
1731
+ focusedNodeId = newFocusId;
1732
+ focusAnimT = 0;
1733
+ }
1734
+ focusAnimT = Math.min(1, focusAnimT + 0.08);
1735
+ updateFocusHUD(focusedNodeId);
1736
+
1737
+ // ── Spawn pulse particles on active synapses ──
1738
+ if (pulseParticles.length < MAX_PARTICLES && Math.random() < 0.3) {
1739
+ for (const syn of network.synapses) {
1740
+ if (syn.silent) continue;
1741
+ const pre = network.neurons[syn.preId];
1742
+ if (pre.state !== 0 && Math.random() < 0.008) {
1743
+ pulseParticles.push({
1744
+ preId: syn.preId, postId: syn.postId,
1745
+ t: 0, speed: 0.015 + Math.random() * 0.02,
1746
+ color: pre.state === 1 ? [34,211,238] : [244,114,182],
1747
+ });
1748
+ if (pulseParticles.length >= MAX_PARTICLES) break;
1749
+ }
1750
+ }
1751
+ }
1752
+
1753
+ // ── Draw Synapses ──
1754
+ ctx.lineWidth = 0.5;
1755
+ for (const syn of network.synapses) {
1756
+ if (syn.silent) continue;
1757
+ const pre = network.neurons[syn.preId];
1758
+ const post = network.neurons[syn.postId];
1759
+ const p1 = worldToScreen(pre.x, pre.y);
1760
+ const p2 = worldToScreen(post.x, post.y);
1761
+ const strength = Math.abs(syn.wFast + syn.wSlow);
1762
+
1763
+ // Highlight synapses connected to focused node
1764
+ const isFocusConn = (syn.preId === focusedNodeId || syn.postId === focusedNodeId);
1765
+ const alpha = isFocusConn ? 0.15 + strength * 0.4 : 0.02 + strength * 0.08;
1766
+ const lw = isFocusConn ? 1.2 : 0.5;
1767
+
1768
+ if (syn.wFast + syn.wSlow > 0) {
1769
+ ctx.strokeStyle = `rgba(34,211,238,${alpha})`;
1770
+ } else {
1771
+ ctx.strokeStyle = `rgba(244,114,182,${alpha})`;
1772
+ }
1773
+ ctx.lineWidth = lw;
1774
+
1775
+ const mx = (p1.x + p2.x) / 2 + (p2.y - p1.y) * 0.12;
1776
+ const my = (p1.y + p2.y) / 2 - (p2.x - p1.x) * 0.12;
1777
+ ctx.beginPath();
1778
+ ctx.moveTo(p1.x, p1.y);
1779
+ ctx.quadraticCurveTo(mx, my, p2.x, p2.y);
1780
+ ctx.stroke();
1781
+ }
1782
+
1783
+ // ── Draw Pulse Particles ──
1784
+ for (let i = pulseParticles.length - 1; i >= 0; i--) {
1785
+ const p = pulseParticles[i];
1786
+ p.t += p.speed;
1787
+ if (p.t >= 1) { pulseParticles.splice(i, 1); continue; }
1788
+ const pre = network.neurons[p.preId];
1789
+ const post = network.neurons[p.postId];
1790
+ const p1 = worldToScreen(pre.x, pre.y);
1791
+ const p2 = worldToScreen(post.x, post.y);
1792
+ const mx = (p1.x + p2.x) / 2 + (p2.y - p1.y) * 0.12;
1793
+ const my = (p1.y + p2.y) / 2 - (p2.x - p1.x) * 0.12;
1794
+ // Quadratic bezier interpolation
1795
+ const t = p.t;
1796
+ const u = 1 - t;
1797
+ const px = u * u * p1.x + 2 * u * t * mx + t * t * p2.x;
1798
+ const py = u * u * p1.y + 2 * u * t * my + t * t * p2.y;
1799
+ const [cr, cg, cb] = p.color;
1800
+ const pAlpha = Math.sin(t * Math.PI) * 0.8;
1801
+ ctx.beginPath();
1802
+ ctx.arc(px, py, 2 * camZoom, 0, Math.PI * 2);
1803
+ ctx.fillStyle = `rgba(${cr},${cg},${cb},${pAlpha})`;
1804
+ ctx.fill();
1805
+ }
1806
+
1807
+ // ── Draw Neurons ──
1808
+ for (const n of network.neurons) {
1809
+ const pos = worldToScreen(n.x, n.y);
1810
+ const isFocused = (n.id === focusedNodeId);
1811
+ const easeT = isFocused ? focusAnimT : 0;
1812
+
1813
+ // Base styling
1814
+ let color, glowColor;
1815
+ if (n.state === 1) {
1816
+ color = '#22d3ee'; glowColor = 'rgba(34,211,238,0.5)';
1817
+ } else if (n.state === -1) {
1818
+ color = '#f472b6'; glowColor = 'rgba(244,114,182,0.5)';
1819
+ } else {
1820
+ color = '#475569'; glowColor = 'rgba(71,85,105,0.15)';
1821
+ }
1822
+
1823
+ // Radius: focused nodes are much larger
1824
+ let baseR = n.type === 'input' ? 6 : n.type === 'output' ? 8 : 4.5;
1825
+ if (n.state !== 0) baseR += 1.5;
1826
+ const focusR = baseR * 2.5;
1827
+ const radius = (baseR + (focusR - baseR) * easeT) * camZoom;
1828
+
1829
+ // Glow effect
1830
+ if (n.state !== 0 || isFocused) {
1831
+ const glowR = radius * (isFocused ? 5 : 3.5);
1832
+ ctx.beginPath();
1833
+ const grad = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, glowR);
1834
+ grad.addColorStop(0, isFocused ? 'rgba(251,191,36,0.2)' : glowColor);
1835
+ grad.addColorStop(1, 'transparent');
1836
+ ctx.fillStyle = grad;
1837
+ ctx.arc(pos.x, pos.y, glowR, 0, Math.PI * 2);
1838
+ ctx.fill();
1839
+ }
1840
+
1841
+ // Pulsing ring for active nodes
1842
+ if (n.state !== 0) {
1843
+ const pulseR = radius + Math.sin(time * 4 + n.id) * 2 * camZoom;
1844
+ ctx.beginPath();
1845
+ ctx.arc(pos.x, pos.y, pulseR + 3 * camZoom, 0, Math.PI * 2);
1846
+ ctx.strokeStyle = n.state === 1 ? 'rgba(34,211,238,0.15)' : 'rgba(244,114,182,0.15)';
1847
+ ctx.lineWidth = 1;
1848
+ ctx.stroke();
1849
+ }
1850
+
1851
+ // Body
1852
+ ctx.beginPath();
1853
+ ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
1854
+ ctx.fillStyle = color;
1855
+ ctx.fill();
1856
+
1857
+ // ── FOCUSED STATE: yellow dashed border + metadata ──
1858
+ if (isFocused) {
1859
+ ctx.save();
1860
+ ctx.beginPath();
1861
+ ctx.arc(pos.x, pos.y, radius + 4 * camZoom, 0, Math.PI * 2);
1862
+ ctx.setLineDash([4, 4]);
1863
+ ctx.strokeStyle = `rgba(251,191,36,${0.6 + 0.3 * Math.sin(time * 3)})`;
1864
+ ctx.lineWidth = 2;
1865
+ ctx.stroke();
1866
+ ctx.setLineDash([]);
1867
+ ctx.restore();
1868
+
1869
+ // Show full label near the node
1870
+ ctx.save();
1871
+ ctx.font = `bold ${11 * camZoom}px 'JetBrains Mono'`;
1872
+ ctx.textAlign = 'center';
1873
+ ctx.fillStyle = 'rgba(251,191,36,0.9)';
1874
+ let label = `#${n.id}`;
1875
+ if (n.type === 'input' && n.id < INPUT_LABELS.length) label = INPUT_LABELS[n.id];
1876
+ else if (n.type === 'output') label = OUTPUT_LABELS[n.id - network.nInput - network.nHidden] || `OUT${n.id}`;
1877
+ ctx.fillText(label, pos.x, pos.y - radius - 8 * camZoom);
1878
+ // State below
1879
+ ctx.font = `${9 * camZoom}px 'JetBrains Mono'`;
1880
+ ctx.fillStyle = color;
1881
+ const stateStr = n.state === 1 ? '+1' : n.state === -1 ? '-1' : '0';
1882
+ ctx.fillText(`s=${n.s.toFixed(2)} [${stateStr}]`, pos.x, pos.y + radius + 14 * camZoom);
1883
+ ctx.restore();
1884
+ } else {
1885
+ // ── CONTEXT STATE: minimal ID label ──
1886
+ // Type indicator ring
1887
+ if (n.type === 'input') {
1888
+ ctx.beginPath();
1889
+ ctx.arc(pos.x, pos.y, radius + 2.5 * camZoom, 0, Math.PI * 2);
1890
+ ctx.strokeStyle = 'rgba(251,191,36,0.4)';
1891
+ ctx.lineWidth = 1;
1892
+ ctx.stroke();
1893
+ } else if (n.type === 'output') {
1894
+ ctx.beginPath();
1895
+ ctx.arc(pos.x, pos.y, radius + 2.5 * camZoom, 0, Math.PI * 2);
1896
+ ctx.strokeStyle = 'rgba(52,211,153,0.4)';
1897
+ ctx.lineWidth = 1.5;
1898
+ ctx.stroke();
1899
+ }
1900
+
1901
+ // Small ID label only when zoomed in enough
1902
+ if (camZoom > 0.7) {
1903
+ ctx.font = `${Math.max(7, 8 * camZoom)}px 'JetBrains Mono'`;
1904
+ ctx.textAlign = 'center';
1905
+ ctx.textBaseline = 'middle';
1906
+ ctx.fillStyle = 'rgba(224,242,254,0.35)';
1907
+ ctx.fillText(n.id, pos.x, pos.y);
1908
+ }
1909
+ }
1910
+ }
1911
+
1912
+ // ── Ring labels ──
1913
+ ctx.save();
1914
+ ctx.font = `${9 * camZoom}px 'JetBrains Mono'`;
1915
+ ctx.textAlign = 'center';
1916
+ ctx.fillStyle = 'rgba(251,191,36,0.35)';
1917
+ const lbl1 = worldToScreen(0, -55);
1918
+ ctx.fillText('INPUT', lbl1.x, lbl1.y);
1919
+ ctx.fillStyle = 'rgba(148,163,184,0.2)';
1920
+ const lbl2 = worldToScreen(0, -165);
1921
+ ctx.fillText('HIDDEN', lbl2.x, lbl2.y);
1922
+ ctx.fillStyle = 'rgba(52,211,153,0.3)';
1923
+ const lbl3 = worldToScreen(0, -380);
1924
+ ctx.fillText('OUTPUT', lbl3.x, lbl3.y);
1925
+ ctx.restore();
1926
+
1927
+ // ── Focal Crosshair / Reticle ──
1928
+ const cx = w / 2, cy = h / 2;
1929
+ ctx.save();
1930
+ ctx.strokeStyle = `rgba(251,191,36,${0.25 + 0.1 * Math.sin(time * 2)})`;
1931
+ ctx.lineWidth = 1;
1932
+ const crossLen = 12;
1933
+ const gap = 6;
1934
+ // Four short lines forming a crosshair
1935
+ ctx.beginPath();
1936
+ ctx.moveTo(cx - crossLen - gap, cy); ctx.lineTo(cx - gap, cy);
1937
+ ctx.moveTo(cx + gap, cy); ctx.lineTo(cx + crossLen + gap, cy);
1938
+ ctx.moveTo(cx, cy - crossLen - gap); ctx.lineTo(cx, cy - gap);
1939
+ ctx.moveTo(cx, cy + gap); ctx.lineTo(cx, cy + crossLen + gap);
1940
+ ctx.stroke();
1941
+ // Small rotating diamond
1942
+ ctx.translate(cx, cy);
1943
+ ctx.rotate(time * 0.5);
1944
+ ctx.strokeStyle = `rgba(251,191,36,${0.15 + 0.08 * Math.sin(time * 3)})`;
1945
+ ctx.beginPath();
1946
+ const d = 20;
1947
+ ctx.moveTo(0, -d); ctx.lineTo(d, 0); ctx.lineTo(0, d); ctx.lineTo(-d, 0); ctx.closePath();
1948
+ ctx.stroke();
1949
+ ctx.restore();
1950
+ }
1951
+
1952
+
1953
+ // ═══════════════════════════════════════════════════════════════
1954
+ // OSCILLATOR + ACTIVITY TRACE (unchanged)
1955
+ // ═══════════════════════════════════════════════════════════════
1956
+
1957
+ function drawOscillators() {
1958
+ const w = oscCanvas.width, h = oscCanvas.height;
1959
+ oscCtx.clearRect(0, 0, w, h);
1960
+ const bands = network.oscillators.bands;
1961
+ const colors = ['#3a5f7a', '#60a5fa', '#a78bfa', '#22d3ee', '#fbbf24'];
1962
+ const bandH = h / bands.length;
1963
+ for (let i = 0; i < bands.length; i++) {
1964
+ const b = bands[i];
1965
+ const y0 = i * bandH + bandH / 2;
1966
+ oscCtx.beginPath();
1967
+ oscCtx.strokeStyle = colors[i];
1968
+ oscCtx.lineWidth = 1.5;
1969
+ for (let x = 0; x < w; x++) {
1970
+ const t = x / w * Math.PI * 4;
1971
+ const val = Math.sin(b.phase + t) * b.amplitude * (bandH * 0.35);
1972
+ if (x === 0) oscCtx.moveTo(x, y0 + val);
1973
+ else oscCtx.lineTo(x, y0 + val);
1974
+ }
1975
+ oscCtx.stroke();
1976
+ oscCtx.font = '8px JetBrains Mono';
1977
+ oscCtx.fillStyle = colors[i];
1978
+ oscCtx.textAlign = 'left';
1979
+ oscCtx.fillText(b.name, 3, i * bandH + 10);
1980
+ }
1981
+ }
1982
+
1983
+ function drawActivityTrace() {
1984
+ const w = actCanvas.width, h = actCanvas.height;
1985
+ actCtx.clearRect(0, 0, w, h);
1986
+ if (activityHistory.length < 2) return;
1987
+ const step = w / MAX_HIST;
1988
+ actCtx.beginPath();
1989
+ actCtx.strokeStyle = 'rgba(34,211,238,0.7)';
1990
+ actCtx.lineWidth = 1;
1991
+ for (let i = 0; i < activityHistory.length; i++) {
1992
+ const x = i * step;
1993
+ const y = h - activityHistory[i].excFrac * h;
1994
+ if (i === 0) actCtx.moveTo(x, y); else actCtx.lineTo(x, y);
1995
+ }
1996
+ actCtx.stroke();
1997
+ actCtx.beginPath();
1998
+ actCtx.strokeStyle = 'rgba(244,114,182,0.7)';
1999
+ for (let i = 0; i < activityHistory.length; i++) {
2000
+ const x = i * step;
2001
+ const y = h - activityHistory[i].inhFrac * h;
2002
+ if (i === 0) actCtx.moveTo(x, y); else actCtx.lineTo(x, y);
2003
+ }
2004
+ actCtx.stroke();
2005
+ }
2006
+
2007
+ function updateReceptorBars(R) {
2008
+ const mapping = {
2009
+ D1: 'D1', D2: 'D2', SHT1A: '5HT1A', SHT2A: '5HT2A', SHT4: '5HT4',
2010
+ M1: 'M1', M2: 'M2', B1: '1', A2: '2',
2011
+ };
2012
+ for (const [key, name] of Object.entries(mapping)) {
2013
+ const val = R[key] || 0;
2014
+ const barId = 'rbar_' + name.replace(/[^a-zA-Z0-9]/g, '');
2015
+ const el = document.getElementById(barId);
2016
+ if (el) el.style.width = (val * 100) + '%';
2017
+ }
2018
+ }
2019
+
2020
+
2021
+ // ════════════════════════��══════════════════════════════════════
2022
+ // MAIN SIMULATION LOOP
2023
+ // ═══════════════════════════════════════════════════════════════
2024
+
2025
+ const SIM_DT = 16;
2026
+
2027
+ function simulationTick() {
2028
+ if (!simRunning) { requestAnimationFrame(simulationTick); return; }
2029
+
2030
+ const inputs = computeSensoryInputs();
2031
+ network.setInputs(inputs);
2032
+ const stats = network.simulateStep(SIM_DT);
2033
+ applyTeachingSignal();
2034
+
2035
+ activityHistory.push({
2036
+ excFrac: stats.excCount / network.N,
2037
+ inhFrac: stats.inhCount / network.N,
2038
+ });
2039
+ if (activityHistory.length > MAX_HIST) activityHistory.shift();
2040
+
2041
+ // Update stats UI
2042
+ $('statExc').textContent = stats.excCount;
2043
+ $('statInh').textContent = stats.inhCount;
2044
+ $('statNeu').textContent = stats.neuCount;
2045
+ $('statSyn').textContent = network.synapses.length;
2046
+ $('stepCount').textContent = network.step;
2047
+ $('energyVal').textContent = network.energy.toFixed(2);
2048
+ $('statDw').textContent = network.meanDw.toFixed(4);
2049
+ $('statStruct').textContent = network.structEvent || (teachingActive ? (idleTicks > IDLE_TEACH_DELAY ? 'STOP-LEARN' : 'LEARNING') : 'stable');
2050
+ $('statStruct').style.color = teachingActive ? (idleTicks > IDLE_TEACH_DELAY ? 'var(--red)' : 'var(--da-color)') : 'var(--green)';
2051
+ $('statLearn').textContent = learnCount;
2052
+ $('statAccum').textContent = outputAccum.map(v => v.toFixed(1)).join(' ');
2053
+
2054
+ const pct = clamp(learnAccuracy, 0, 100);
2055
+ $('learnPct').textContent = pct + '%';
2056
+ $('learnBar').style.width = pct + '%';
2057
+ $('learnBar').className = 'progress-fill' + (pct > 60 ? ' learned' : '');
2058
+ if (learnCount === 0) {
2059
+ $('learnBarText').textContent = 'Drive in MANUAL to teach';
2060
+ } else if (pct < 30) {
2061
+ $('learnBarText').textContent = 'Learning... drive, turn & stop!';
2062
+ } else if (pct < 60) {
2063
+ $('learnBarText').textContent = 'Getting better β€” try HYBRID';
2064
+ } else {
2065
+ $('learnBarText').textContent = 'Well trained! Neuraxon ready';
2066
+ }
2067
+
2068
+ // Source tags
2069
+ const tagH = $('tagHuman');
2070
+ const tagN = $('tagNxon');
2071
+ const tagL = $('tagLearn');
2072
+ const srcDetail = $('sourceDetail');
2073
+
2074
+ if (controlMode === 'manual') {
2075
+ tagH.style.opacity = activeKeys.size > 0 ? '1' : '0.5';
2076
+ tagN.style.opacity = '0.2';
2077
+ tagL.style.opacity = teachingActive ? '1' : '0.2';
2078
+ srcDetail.textContent = teachingActive ? 'Teaching network from your input' : 'Manual control β€” press keys to teach';
2079
+ $('blendVal').textContent = 'H:100% N:0%';
2080
+ } else if (controlMode === 'hybrid') {
2081
+ tagH.style.opacity = activeKeys.size > 0 ? '1' : '0.4';
2082
+ tagN.style.opacity = nxonContributing ? '1' : '0.3';
2083
+ tagL.style.opacity = teachingActive ? '1' : '0.2';
2084
+ srcDetail.textContent = nxonContributing
2085
+ ? (teachingActive ? 'Both active β€” learning + Neuraxon assisting' : 'Neuraxon gently assisting (25%)')
2086
+ : (teachingActive ? 'Teaching from your input' : 'Hybrid idle β€” you lead, Neuraxon assists');
2087
+ $('blendVal').textContent = 'H:75% N:25%';
2088
+ }
2089
+
2090
+ // Motor display
2091
+ const ai = getNeuraxonAction();
2092
+ const human = getHumanAction();
2093
+ let dispSpeed, dispTurn;
2094
+ if (controlMode === 'manual') {
2095
+ dispSpeed = Math.abs(human.speed);
2096
+ dispTurn = human.turn;
2097
+ } else if (controlMode === 'hybrid') {
2098
+ dispSpeed = Math.round(Math.abs(human.speed) * 0.75 + ai.speed * 0.25);
2099
+ dispTurn = human.turn * 0.75 + ai.turn * 0.25;
2100
+ } else {
2101
+ dispSpeed = ai.speed;
2102
+ dispTurn = ai.turn;
2103
+ }
2104
+ if (!isConnected) {
2105
+ heading = (heading + dispTurn + 360) % 360;
2106
+ }
2107
+ $('speedVal').textContent = Math.round(dispSpeed);
2108
+ $('headingVal').textContent = Math.round(heading) + 'Β°';
2109
+ $('headingNeedle').style.transform = `rotate(${heading}deg)`;
2110
+
2111
+ // Draw visuals
2112
+ drawNetwork(stats);
2113
+ drawOscillators();
2114
+ drawActivityTrace();
2115
+ updateReceptorBars(stats.R);
2116
+
2117
+ requestAnimationFrame(simulationTick);
2118
+ }
2119
+
2120
+ // Start
2121
+ sysLog('Neuraxon 2.0 brain initialized.');
2122
+ sysLog('6 input β†’ 24 hidden β†’ 4 output neurons');
2123
+ sysLog(`${network.synapses.length} synapses (small-world + motor pathways)`);
2124
+ sysLog('DA-gated STDP learning with 9 receptor subtypes');
2125
+ sysLog('Learns: forward, turns, AND stopping');
2126
+ sysLog('Replay buffer: Neuraxon replays your moves');
2127
+ sysLog('═══════════════════════════════════════');
2128
+ sysLog('HOW TO USE:');
2129
+ sysLog('1. MANUAL mode β€” drive with WASD/arrows');
2130
+ sysLog(' β†’ Network learns from every keypress');
2131
+ sysLog(' β†’ Watch accuracy % rise in LEARNING panel');
2132
+ sysLog(' β†’ Gold LED = learning in progress');
2133
+ sysLog('2. HYBRID mode β€” 75% you + 25% Neuraxon');
2134
+ sysLog(' β†’ Neuraxon gently assists, you lead');
2135
+ sysLog(' β†’ Still learns when you press keys');
2136
+ sysLog('TIP: Drive fwd, turn, AND STOP to teach');
2137
+ sysLog(' the full repertoire of behaviors');
2138
+ sysLog('════════════��══════════════════════════');
2139
+ sysLog('NEURAL GRAPH: Drag to pan, scroll to zoom');
2140
+ sysLog('Nearest node auto-focuses with metadata');
2141
+ sysLog('═══════════════════════════════════════');
2142
+ requestAnimationFrame(simulationTick);
2143
+
2144
+ </script>
2145
+ </body>
2146
+ </html>