salomonsky commited on
Commit
e229d32
·
verified ·
1 Parent(s): 7a7ae4e

Upload 10 files

Browse files
Files changed (10) hide show
  1. index.html +73 -1536
  2. src/config.js +4 -0
  3. src/db.js +6 -0
  4. src/firebase.js +19 -0
  5. src/gemini.js +3 -0
  6. src/main.js +52 -0
  7. src/scene.js +33 -0
  8. src/ui.js +10 -0
  9. src/utils.js +4 -0
  10. styles.css +1 -74
index.html CHANGED
@@ -1,1551 +1,88 @@
1
  <!DOCTYPE html>
2
  <html lang="es">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
6
- <title>ECOTAGS 3D - SISTEMA NEURONAL</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&family=Rajdhani:wght@300;500;700&display=swap" rel="stylesheet">
9
- <style>
10
- :root {
11
- --color-bg: #030508;
12
- --color-primary: #00f3ff;
13
- --color-secondary: #0066ff;
14
- --color-danger: #ff003c;
15
- --color-success: #00ff9d;
16
- --color-text: #e0f7ff;
17
- --color-panel: rgba(5, 10, 20, 0.85);
18
- --border-glow: 0 0 10px rgba(0, 243, 255, 0.3);
19
- --font-main: 'Orbitron', sans-serif;
20
- --font-body: 'Rajdhani', sans-serif;
21
- }
22
-
23
- * {
24
- box-sizing: border-box;
25
- user-select: none;
26
- -webkit-user-drag: none;
27
- }
28
-
29
- body {
30
- margin: 0;
31
- overflow: hidden;
32
- background-color: var(--color-bg);
33
- font-family: var(--font-main);
34
- color: var(--color-text);
35
- height: 100vh;
36
- width: 100vw;
37
- }
38
-
39
- #canvas-container {
40
- position: fixed;
41
- top: 0;
42
- left: 0;
43
- width: 100%;
44
- height: 100%;
45
- z-index: 0;
46
- }
47
-
48
- .hud-panel {
49
- background: var(--color-panel);
50
- border: 1px solid rgba(0, 243, 255, 0.2);
51
- backdrop-filter: blur(12px);
52
- -webkit-backdrop-filter: blur(12px);
53
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
54
- border-radius: 4px;
55
- position: relative;
56
- overflow: hidden;
57
- }
58
-
59
- .hud-panel::before {
60
- content: '';
61
- position: absolute;
62
- top: 0;
63
- left: 0;
64
- width: 100%;
65
- height: 2px;
66
- background: linear-gradient(90deg, transparent, var(--color-primary), transparent);
67
- opacity: 0.5;
68
- }
69
-
70
- .hud-panel::after {
71
- content: '';
72
- position: absolute;
73
- bottom: 0;
74
- right: 0;
75
- width: 10px;
76
- height: 10px;
77
- border-bottom: 2px solid var(--color-primary);
78
- border-right: 2px solid var(--color-primary);
79
- }
80
-
81
- #ui-sidebar {
82
- position: fixed;
83
- top: 20px;
84
- left: 20px;
85
- width: 340px;
86
- max-height: calc(100vh - 40px);
87
- display: flex;
88
- flex-direction: column;
89
- z-index: 100;
90
- padding: 24px;
91
- gap: 20px;
92
- transition: transform 0.3s ease;
93
- }
94
-
95
- .logo-section {
96
- display: flex;
97
- justify-content: space-between;
98
- align-items: center;
99
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
100
- padding-bottom: 15px;
101
- }
102
-
103
- .logo-text {
104
- font-size: 24px;
105
- font-weight: 900;
106
- letter-spacing: 2px;
107
- background: linear-gradient(180deg, #fff, var(--color-primary));
108
- -webkit-background-clip: text;
109
- -webkit-text-fill-color: transparent;
110
- text-shadow: 0 0 10px rgba(0, 243, 255, 0.5);
111
- }
112
-
113
- .version-tag {
114
- font-family: var(--font-body);
115
- font-size: 12px;
116
- color: var(--color-secondary);
117
- border: 1px solid var(--color-secondary);
118
- padding: 2px 6px;
119
- border-radius: 2px;
120
- }
121
-
122
- .input-group {
123
- position: relative;
124
- margin-bottom: 10px;
125
- }
126
-
127
- .cyber-input {
128
- width: 100%;
129
- background: rgba(0, 0, 0, 0.3);
130
- border: 1px solid rgba(255, 255, 255, 0.1);
131
- color: var(--color-primary);
132
- padding: 12px 15px;
133
- font-family: var(--font-body);
134
- font-size: 16px;
135
- font-weight: 500;
136
- outline: none;
137
- transition: all 0.3s ease;
138
- text-transform: uppercase;
139
- }
140
-
141
- .cyber-input:focus {
142
- border-color: var(--color-primary);
143
- box-shadow: inset 0 0 10px rgba(0, 243, 255, 0.1);
144
- }
145
-
146
- .cyber-input::placeholder {
147
- color: rgba(255, 255, 255, 0.3);
148
- }
149
-
150
- .slider-container {
151
- margin-bottom: 15px;
152
- }
153
-
154
- .slider-header {
155
- display: flex;
156
- justify-content: space-between;
157
- font-size: 12px;
158
- color: rgba(255, 255, 255, 0.6);
159
- margin-bottom: 5px;
160
- font-family: var(--font-body);
161
- text-transform: uppercase;
162
- letter-spacing: 1px;
163
- }
164
-
165
- .cyber-slider {
166
- -webkit-appearance: none;
167
- width: 100%;
168
- height: 4px;
169
- background: rgba(255, 255, 255, 0.1);
170
- outline: none;
171
- border-radius: 2px;
172
- }
173
-
174
- .cyber-slider::-webkit-slider-thumb {
175
- -webkit-appearance: none;
176
- width: 12px;
177
- height: 20px;
178
- background: var(--color-primary);
179
- border: 1px solid #fff;
180
- cursor: pointer;
181
- box-shadow: 0 0 10px var(--color-primary);
182
- }
183
-
184
- .cyber-button {
185
- width: 100%;
186
- background: linear-gradient(45deg, rgba(0, 102, 255, 0.2), rgba(0, 243, 255, 0.2));
187
- border: 1px solid var(--color-secondary);
188
- color: var(--color-primary);
189
- padding: 15px;
190
- font-family: var(--font-main);
191
- font-weight: 700;
192
- font-size: 14px;
193
- letter-spacing: 2px;
194
- cursor: pointer;
195
- transition: all 0.3s ease;
196
- text-transform: uppercase;
197
- position: relative;
198
- overflow: hidden;
199
- display: flex;
200
- align-items: center;
201
- justify-content: center;
202
- gap: 10px;
203
- }
204
-
205
- .cyber-button:hover {
206
- background: linear-gradient(45deg, var(--color-secondary), var(--color-primary));
207
- color: #000;
208
- box-shadow: 0 0 20px rgba(0, 243, 255, 0.4);
209
- }
210
-
211
- .cyber-button:disabled {
212
- opacity: 0.5;
213
- cursor: not-allowed;
214
- filter: grayscale(1);
215
- }
216
-
217
- .cyber-button-danger {
218
- border-color: var(--color-danger);
219
- color: var(--color-danger);
220
- background: rgba(255, 0, 60, 0.1);
221
- font-size: 10px;
222
- padding: 5px 10px;
223
- width: auto;
224
- }
225
-
226
- .cyber-button-danger:hover {
227
- background: var(--color-danger);
228
- color: #fff;
229
- box-shadow: 0 0 15px rgba(255, 0, 60, 0.5);
230
- }
231
-
232
- #minimap-container {
233
- margin-top: auto;
234
- border: 1px solid rgba(255, 255, 255, 0.1);
235
- background: rgba(0, 0, 0, 0.5);
236
- position: relative;
237
- }
238
-
239
- #minimap-header {
240
- position: absolute;
241
- top: 5px;
242
- left: 5px;
243
- font-size: 10px;
244
- color: var(--color-primary);
245
- pointer-events: none;
246
- }
247
-
248
- #minimap-controls {
249
- position: absolute;
250
- top: 5px;
251
- right: 5px;
252
- display: flex;
253
- gap: 2px;
254
- }
255
-
256
- .mini-btn {
257
- width: 16px;
258
- height: 16px;
259
- background: rgba(0, 0, 0, 0.8);
260
- border: 1px solid var(--color-secondary);
261
- color: #fff;
262
- display: flex;
263
- align-items: center;
264
- justify-content: center;
265
- font-size: 10px;
266
- cursor: pointer;
267
- }
268
-
269
- .mini-btn:hover {
270
- background: var(--color-secondary);
271
- }
272
-
273
- #users-list {
274
- margin-top: 10px;
275
- max-height: 150px;
276
- overflow-y: auto;
277
- font-family: var(--font-body);
278
- }
279
-
280
- .user-item {
281
- display: flex;
282
- justify-content: space-between;
283
- padding: 8px;
284
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
285
- cursor: pointer;
286
- transition: background 0.2s;
287
- font-size: 14px;
288
- }
289
-
290
- .user-item:hover {
291
- background: rgba(0, 243, 255, 0.1);
292
- color: var(--color-primary);
293
- }
294
-
295
- .user-count-badge {
296
- background: rgba(255, 255, 255, 0.1);
297
- padding: 0 6px;
298
- border-radius: 10px;
299
- font-size: 12px;
300
- }
301
-
302
- #progress-bar {
303
- position: absolute;
304
- bottom: 0;
305
- left: 0;
306
- height: 2px;
307
- background: var(--color-success);
308
- width: 0%;
309
- transition: width 0.3s;
310
- }
311
-
312
- #login-overlay {
313
- position: fixed;
314
- top: 0;
315
- left: 0;
316
- width: 100vw;
317
- height: 100vh;
318
- background: rgba(0, 3, 10, 0.95);
319
- z-index: 200;
320
- display: flex;
321
- align-items: center;
322
- justify-content: center;
323
- backdrop-filter: blur(20px);
324
- transition: opacity 0.6s ease;
325
- }
326
-
327
- .login-card {
328
- width: 100%;
329
- max-width: 450px;
330
- padding: 40px;
331
- background: rgba(10, 15, 30, 0.6);
332
- border: 1px solid var(--color-primary);
333
- box-shadow: 0 0 50px rgba(0, 243, 255, 0.1);
334
- position: relative;
335
- }
336
-
337
- .login-card::before {
338
- content: '';
339
- position: absolute;
340
- top: -2px;
341
- left: -2px;
342
- width: 20px;
343
- height: 20px;
344
- border-top: 2px solid var(--color-primary);
345
- border-left: 2px solid var(--color-primary);
346
- }
347
-
348
- .login-card::after {
349
- content: '';
350
- position: absolute;
351
- bottom: -2px;
352
- right: -2px;
353
- width: 20px;
354
- height: 20px;
355
- border-bottom: 2px solid var(--color-primary);
356
- border-right: 2px solid var(--color-primary);
357
- }
358
-
359
- .scan-line {
360
- position: absolute;
361
- top: 0;
362
- left: 0;
363
- width: 100%;
364
- height: 2px;
365
- background: rgba(0, 243, 255, 0.5);
366
- animation: scan 3s linear infinite;
367
- opacity: 0.3;
368
- pointer-events: none;
369
- }
370
-
371
- @keyframes scan {
372
- 0% { top: 0%; }
373
- 100% { top: 100%; }
374
- }
375
-
376
- .tooltip-box {
377
- position: absolute;
378
- background: rgba(5, 10, 15, 0.95);
379
- border: 1px solid var(--color-success);
380
- padding: 10px 15px;
381
- pointer-events: none;
382
- display: none;
383
- z-index: 150;
384
- box-shadow: 0 0 15px rgba(0, 255, 157, 0.2);
385
- max-width: 250px;
386
- }
387
-
388
- .tooltip-title {
389
- color: var(--color-success);
390
- font-weight: 700;
391
- font-size: 14px;
392
- margin-bottom: 4px;
393
- border-bottom: 1px solid rgba(0, 255, 157, 0.3);
394
- padding-bottom: 4px;
395
- text-transform: uppercase;
396
- }
397
-
398
- .tooltip-meta {
399
- color: rgba(255, 255, 255, 0.7);
400
- font-size: 11px;
401
- font-family: var(--font-body);
402
- }
403
-
404
- #error-console {
405
- position: fixed;
406
- bottom: 0;
407
- left: 0;
408
- width: 100%;
409
- height: 150px;
410
- background: rgba(20, 0, 0, 0.9);
411
- border-top: 2px solid var(--color-danger);
412
- color: var(--color-danger);
413
- font-family: monospace;
414
- padding: 10px;
415
- z-index: 9999;
416
- overflow-y: auto;
417
- display: none;
418
- font-size: 12px;
419
- }
420
-
421
- ::-webkit-scrollbar {
422
- width: 4px;
423
- }
424
-
425
- ::-webkit-scrollbar-track {
426
- background: rgba(0, 0, 0, 0.2);
427
- }
428
-
429
- ::-webkit-scrollbar-thumb {
430
- background: var(--color-secondary);
431
- }
432
-
433
- .hidden {
434
- display: none !important;
435
- }
436
-
437
- .fade-out {
438
- opacity: 0;
439
- pointer-events: none;
440
- }
441
-
442
- .loading-pulse {
443
- animation: pulse 1.5s infinite;
444
- color: var(--color-primary);
445
- font-weight: bold;
446
- text-align: center;
447
- margin-top: 20px;
448
- font-family: var(--font-body);
449
- letter-spacing: 2px;
450
- }
451
-
452
- @keyframes pulse {
453
- 0% { opacity: 0.5; }
454
- 50% { opacity: 1; text-shadow: 0 0 10px var(--color-primary); }
455
- 100% { opacity: 0.5; }
456
- }
457
- </style>
458
  </head>
459
  <body>
460
-
461
- <div id="error-console">
462
- <strong>SISTEMA DE DIAGNOSTICO - ERRORES DETECTADOS:</strong><br>
463
- <div id="error-list"></div>
464
- </div>
465
-
466
- <div id="login-overlay">
467
- <div class="login-card hud-panel">
468
- <div class="scan-line"></div>
469
- <div class="text-center mb-8">
470
- <h1 class="logo-text" style="font-size: 32px;">ECOTAGS <span style="font-size: 16px; color: var(--color-primary);">SYSTEM</span></h1>
471
- <p style="font-family: var(--font-body); color: rgba(255,255,255,0.6); margin-top: 5px;">INTERFAZ NEURONAL DE DATOS</p>
472
- </div>
473
- <div class="loading-pulse" id="loading-text">
474
- INICIALIZANDO SISTEMA...
475
- </div>
476
- <div style="text-align: center; margin-top: 10px; color: #666; font-size: 10px;">
477
- ASIGNANDO IDENTIFICADOR AUTOMÁTICO
478
- </div>
479
  </div>
480
- </div>
481
-
482
- <div id="canvas-container"></div>
483
- <div id="tooltip" class="tooltip-box"></div>
484
-
485
- <aside id="ui-sidebar" class="hud-panel">
486
- <div id="progress-bar"></div>
487
-
488
- <div class="logo-section">
489
- <div class="logo-text">ECOTAGS</div>
490
- <div style="display: flex; gap: 10px; align-items: center;">
491
- <div class="version-tag">V3.1</div>
492
- <button id="btn-logout" class="cyber-button cyber-button-danger">REINICIAR</button>
493
  </div>
494
- </div>
495
-
496
- <div style="display: flex; flex-direction: column; gap: 15px;">
497
- <div class="input-group">
498
- <input type="text" id="seed-input" class="cyber-input" placeholder="SEMILLA NEURONAL...">
499
  </div>
500
-
501
- <div class="slider-container">
502
- <div class="slider-header">
503
- <span>DENSIDAD PRIMARIA</span>
504
- <span id="val-l1" style="color: var(--color-primary)">6</span>
505
- </div>
506
- <input type="range" id="range-l1" class="cyber-slider" min="1" max="15" value="6">
507
  </div>
508
-
509
- <div class="slider-container">
510
- <div class="slider-header">
511
- <span>RAMIFICACIÓN SECUNDARIA</span>
512
- <span id="val-l2" style="color: var(--color-primary)">4</span>
513
- </div>
514
- <input type="range" id="range-l2" class="cyber-slider" min="1" max="10" value="4">
 
 
 
 
 
 
 
 
 
 
515
  </div>
516
-
517
- <div class="slider-container">
518
- <div class="slider-header">
519
- <span>PROFUNDIDAD TERCIARIA</span>
520
- <span id="val-l3" style="color: var(--color-primary)">2</span>
521
- </div>
522
- <input type="range" id="range-l3" class="cyber-slider" min="0" max="5" value="2">
 
 
 
 
 
523
  </div>
524
-
525
- <button id="btn-visualize" class="cyber-button">
526
- <span>SEMBRAR EN LA RED</span>
527
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
528
- <circle cx="12" cy="12" r="10"></circle>
529
- <line x1="12" y1="8" x2="12" y2="16"></line>
530
- <line x1="8" y1="12" x2="16" y2="12"></line>
531
- </svg>
532
- </button>
533
  </div>
 
534
 
535
- <div style="flex: 1; display: flex; flex-direction: column; min-height: 0; gap: 10px;">
536
- <div style="font-size: 12px; color: var(--color-secondary); letter-spacing: 2px; font-weight: 700;">RED GLOBAL</div>
537
-
538
- <div id="minimap-container" style="height: 180px; width: 100%;">
539
- <canvas id="minimap-canvas" width="290" height="180"></canvas>
540
- <div id="minimap-header">RADAR</div>
541
- <div id="minimap-controls">
542
- <div class="mini-btn" id="map-zoom-out">-</div>
543
- <div class="mini-btn" id="map-zoom-in">+</div>
544
- </div>
545
- </div>
546
-
547
- <div id="users-list"></div>
548
- </div>
549
- </aside>
550
-
551
- <script>
552
- // Sistema global de diagnóstico de errores
553
- window.onerror = function(msg, url, lineNo, columnNo, error) {
554
- const consoleDiv = document.getElementById('error-console');
555
- const list = document.getElementById('error-list');
556
- if (consoleDiv && list) {
557
- consoleDiv.style.display = 'block';
558
- const item = document.createElement('div');
559
- item.innerHTML = `[CRITICO] ${msg} <br><span style="color:#aaa; font-size:10px">En: ${url}:${lineNo}</span>`;
560
- item.style.borderBottom = '1px solid #400';
561
- item.style.padding = '5px 0';
562
- list.appendChild(item);
563
- }
564
- return false;
565
- };
566
-
567
- window.addEventListener('unhandledrejection', function(event) {
568
- const consoleDiv = document.getElementById('error-console');
569
- const list = document.getElementById('error-list');
570
- if (consoleDiv && list) {
571
- consoleDiv.style.display = 'block';
572
- const item = document.createElement('div');
573
- // Detectar error común de Firebase Domain
574
- let msg = event.reason ? event.reason.message : 'Error desconocido';
575
- if (msg.includes('auth/unauthorized-domain')) {
576
- msg = "ERROR DE DOMINIO: Este dominio no está autorizado en Firebase Console -> Authentication -> Settings.";
577
- }
578
- item.innerHTML = `[PROMESA] ${msg}`;
579
- item.style.borderBottom = '1px solid #400';
580
- item.style.padding = '5px 0';
581
- list.appendChild(item);
582
- }
583
- });
584
- </script>
585
-
586
- <script type="module">
587
- import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
588
- import { getAuth, signInAnonymously, signOut, onAuthStateChanged, setPersistence, browserSessionPersistence } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
589
- import { getFirestore, doc, setDoc, getDoc, addDoc, collection, onSnapshot, query, orderBy, serverTimestamp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
590
- import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.module.js";
591
- import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/controls/OrbitControls.js";
592
- import { FontLoader } from "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/loaders/FontLoader.js";
593
- import { TextGeometry } from "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/geometries/TextGeometry.js";
594
- import TWEEN from "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@18.6.4/dist/tween.esm.js";
595
-
596
- class Config {
597
- static get FIREBASE() {
598
- return {
599
- apiKey: "AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",
600
- authDomain: "neuronal-1f3b9.firebaseapp.com",
601
- projectId: "neuronal-1f3b9",
602
- storageBucket: "neuronal-1f3b9.firebasestorage.app",
603
- messagingSenderId: "208887839866",
604
- appId: "1:208887839866:web:adbb697dd0b63195b10fc3"
605
- };
606
- }
607
-
608
- static get GEMINI_API_KEY() {
609
- return localStorage.getItem('GEMINI_API_KEY') || 'AIzaSyDmoQNpzgzW21f_WFCU9YbaAeI1fdOJMlo';
610
- }
611
-
612
- static get COLORS() {
613
- return {
614
- bg: 0x030508,
615
- primary: 0x00f3ff,
616
- secondary: 0x0066ff,
617
- text: 0xe0f7ff,
618
- grid: 0x1a2b4a
619
- };
620
- }
621
-
622
- static get CONSTANTS() {
623
- return {
624
- GALAXY_SPACING: 400,
625
- USER_SPHERE_RADIUS: 5,
626
- SPIRAL_TIGHTNESS: 0.15,
627
- SPIRAL_VERTICAL_STEP: 2.5,
628
- CAMERA_FOV: 60,
629
- CAMERA_FAR: 5000
630
- };
631
- }
632
- }
633
-
634
- class Utils {
635
- static normalizeString(str) {
636
- return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase();
637
- }
638
-
639
- static stringToColor(str) {
640
- let hash = 0;
641
- for (let i = 0; i < str.length; i++) {
642
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
643
- }
644
- const h = Math.abs(hash % 360);
645
- return new THREE.Color(`hsl(${h}, 80%, 60%)`);
646
- }
647
-
648
- static stringToHexColor(str) {
649
- let hash = 0;
650
- for (let i = 0; i < str.length; i++) {
651
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
652
- }
653
- const h = Math.abs(hash % 360);
654
- return `hsl(${h}, 80%, 60%)`;
655
- }
656
-
657
- static delay(ms) {
658
- return new Promise(resolve => setTimeout(resolve, ms));
659
- }
660
-
661
- static randomRange(min, max) {
662
- return Math.random() * (max - min) + min;
663
- }
664
-
665
- static generateCode() {
666
- return Math.floor(1000 + Math.random() * 9000).toString();
667
- }
668
- }
669
-
670
- class AuthManager {
671
- constructor(appInstance) {
672
- this.app = appInstance;
673
- this.firebaseApp = initializeApp(Config.FIREBASE);
674
- this.auth = getAuth(this.firebaseApp);
675
- this.db = getFirestore(this.firebaseApp);
676
- this.currentUser = null;
677
- this.currentUsername = null;
678
- }
679
-
680
- async init() {
681
- await setPersistence(this.auth, browserSessionPersistence);
682
-
683
- onAuthStateChanged(this.auth, async (user) => {
684
- if (user) {
685
- this.currentUser = user;
686
- const loader = document.getElementById('loading-text');
687
- if (loader) loader.innerText = "CONECTANDO A BASE DE DATOS...";
688
- await this.checkUserProfile();
689
- } else {
690
- const loader = document.getElementById('loading-text');
691
- if (loader) loader.innerText = "GENERANDO CREDENCIALES...";
692
- try {
693
- await signInAnonymously(this.auth);
694
- } catch (e) {
695
- console.error("Auth Error:", e);
696
- if (loader) loader.innerText = "ERROR DE AUTENTICACION (VER CONSOLA)";
697
- }
698
- }
699
- });
700
-
701
- const logoutBtn = document.getElementById('btn-logout');
702
- if (logoutBtn) logoutBtn.addEventListener('click', () => this.handleLogout());
703
- }
704
-
705
- async handleLogout() {
706
- try {
707
- await signOut(this.auth);
708
- window.location.reload();
709
- } catch (error) {
710
- console.error("Logout Error:", error);
711
- }
712
- }
713
-
714
- async createUserProfile(username) {
715
- if (!this.currentUser) return;
716
- const uid = this.currentUser.uid;
717
- this.currentUsername = username;
718
-
719
- const userRef = doc(this.db, 'artifacts', 'neuronal-1f3b9', 'users', uid, 'user_data', 'profile');
720
- await setDoc(userRef, {
721
- username: username,
722
- createdAt: serverTimestamp(),
723
- lastLogin: serverTimestamp(),
724
- platform: navigator.platform
725
- }, { merge: true });
726
- }
727
-
728
- async checkUserProfile() {
729
- if (!this.currentUser) return;
730
- const uid = this.currentUser.uid;
731
-
732
- try {
733
- const userRef = doc(this.db, 'artifacts', 'neuronal-1f3b9', 'users', uid, 'user_data', 'profile');
734
- const snap = await getDoc(userRef);
735
-
736
- if (snap.exists()) {
737
- this.currentUsername = snap.data().username;
738
- } else {
739
- const randomName = `PILOTO-${Utils.generateCode()}`;
740
- await this.createUserProfile(randomName);
741
- }
742
-
743
- const loader = document.getElementById('loading-text');
744
- if (loader) loader.innerText = "ACCESO CONCEDIDO";
745
- setTimeout(() => {
746
- this.app.ui.hideLogin();
747
- this.app.data.startListening();
748
- }, 800);
749
-
750
- } catch (error) {
751
- console.error("Profile Check Error:", error);
752
- const loader = document.getElementById('loading-text');
753
- if (loader) loader.innerText = "ERROR DE CONEXION";
754
- }
755
- }
756
- }
757
-
758
- class DataManager {
759
- constructor(appInstance) {
760
- this.app = appInstance;
761
- this.db = appInstance.auth.db;
762
- this.maps = [];
763
- this.users = new Map();
764
- this.listeners = [];
765
- }
766
-
767
- startListening() {
768
- const mapsRef = collection(this.db, 'artifacts', 'neuronal-1f3b9', 'public', 'data', 'maps');
769
- const q = query(mapsRef, orderBy('createdAt', 'asc'));
770
-
771
- const unsubscribe = onSnapshot(q, (snapshot) => {
772
- this.maps = [];
773
- snapshot.forEach(doc => {
774
- this.maps.push({
775
- id: doc.id,
776
- ...doc.data()
777
- });
778
- });
779
- this.processData();
780
- }, (error) => {
781
- console.error("Snapshot error:", error);
782
- });
783
- this.listeners.push(unsubscribe);
784
- }
785
-
786
- async processData() {
787
- const uniqueUserIds = [...new Set(this.maps.map(m => m.userId))];
788
-
789
- if (this.app.auth.currentUser && !uniqueUserIds.includes(this.app.auth.currentUser.uid)) {
790
- uniqueUserIds.push(this.app.auth.currentUser.uid);
791
- }
792
-
793
- for (const uid of uniqueUserIds) {
794
- if (!this.users.has(uid)) {
795
- let username = "DESCONOCIDO";
796
- if (uid === this.app.auth.currentUser?.uid) {
797
- username = this.app.auth.currentUsername;
798
- } else {
799
- try {
800
- const ref = doc(this.db, 'artifacts', 'neuronal-1f3b9', 'users', uid, 'user_data', 'profile');
801
- const snap = await getDoc(ref);
802
- if (snap.exists()) username = snap.data().username;
803
- } catch (e) {
804
- console.warn(`Error fetching user ${uid}`, e);
805
- }
806
- }
807
- this.users.set(uid, { username, id: uid, maps: [] });
808
- }
809
- }
810
-
811
- this.users.forEach(u => u.maps = []);
812
- this.maps.forEach(map => {
813
- if (this.users.has(map.userId)) {
814
- this.users.get(map.userId).maps.push(map);
815
- }
816
- });
817
-
818
- this.app.world.galaxySystem.rebuild(this.users);
819
- this.app.ui.updateUserList(this.users);
820
- }
821
-
822
- async saveNeuron(topic, dataStructure) {
823
- if (!this.app.auth.currentUser) return;
824
-
825
- const colRef = collection(this.db, 'artifacts', 'neuronal-1f3b9', 'public', 'data', 'maps');
826
- await addDoc(colRef, {
827
- topic: topic,
828
- data: JSON.stringify(dataStructure),
829
- userId: this.app.auth.currentUser.uid,
830
- createdAt: serverTimestamp(),
831
- origin: { x: 0, y: 0, z: 0 }
832
- });
833
- }
834
- }
835
-
836
- class AIManager {
837
- constructor(appInstance) {
838
- this.app = appInstance;
839
- }
840
-
841
- async generate(topic, levels) {
842
- const prompt = `
843
- Genera un objeto JSON puro (sin markdown) basado en el tema: "${topic}".
844
- Asegúrate de que no haya acentos ni caracteres especiales.
845
- Estructura:
846
- {
847
- "analisis": "Breve descripción de 15 palabras",
848
- "lista_palabras": [
849
- {
850
- "palabra_principal": "Concepto1",
851
- "variantes": [
852
- {
853
- "palabra_variante": "Subconcepto1",
854
- "sub_variantes": ["Detalle1", "Detalle2"]
855
- }
856
- ]
857
- }
858
- ]
859
- }
860
- Requisitos de cantidad:
861
- - ${levels.l1} elementos principales en lista_palabras.
862
- - ${levels.l2} variantes por cada principal.
863
- - ${levels.l3} sub_variantes por cada variante.
864
- IMPORTANTE: Responde SOLO con el JSON.
865
- `;
866
-
867
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${Config.GEMINI_API_KEY}`;
868
-
869
- try {
870
- const response = await fetch(url, {
871
- method: 'POST',
872
- headers: { 'Content-Type': 'application/json' },
873
- body: JSON.stringify({
874
- contents: [{ parts: [{ text: prompt }] }],
875
- generationConfig: { responseMimeType: "application/json" }
876
- })
877
- });
878
-
879
- if (!response.ok) throw new Error("API Error");
880
-
881
- const data = await response.json();
882
- const text = data.candidates[0].content.parts[0].text;
883
- return JSON.parse(text);
884
-
885
- } catch (error) {
886
- console.error("AI Error:", error);
887
- throw error;
888
- }
889
- }
890
- }
891
-
892
- class Comet {
893
- constructor(scene, font) {
894
- this.scene = scene;
895
- this.font = font;
896
- this.angle = 0;
897
- this.radius = 600;
898
- this.speed = 0.003;
899
- this.group = new THREE.Group();
900
- this.tailPositions = [];
901
- this.wordTimer = 0;
902
- this.currentWordMesh = null;
903
- this.words = ["DATOS", "FLUJO", "RED", "NODO", "ENLACE", "CORE", "SYSTEM", "LINK", "SIGNAL", "PULSE"];
904
-
905
- this.initVisuals();
906
- this.scene.add(this.group);
907
- }
908
-
909
- initVisuals() {
910
- const headGeo = new THREE.SphereGeometry(2, 16, 16);
911
- const headMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
912
- this.head = new THREE.Mesh(headGeo, headMat);
913
- this.group.add(this.head);
914
-
915
- const glowGeo = new THREE.SpriteMaterial({
916
- map: new THREE.CanvasTexture(this.generateGlowTexture()),
917
- color: 0x00ffff,
918
- transparent: true,
919
- opacity: 0.8,
920
- blending: THREE.AdditiveBlending
921
- });
922
- const glow = new THREE.Sprite(glowGeo);
923
- glow.scale.set(15, 15, 1);
924
- this.head.add(glow);
925
-
926
- const lineGeo = new THREE.BufferGeometry();
927
- const positions = new Float32Array(50 * 3);
928
- lineGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
929
- const lineMat = new THREE.LineBasicMaterial({
930
- color: 0x00ffff,
931
- transparent: true,
932
- opacity: 0.4,
933
- blending: THREE.AdditiveBlending
934
- });
935
- this.tail = new THREE.Line(lineGeo, lineMat);
936
- this.scene.add(this.tail);
937
- }
938
-
939
- generateGlowTexture() {
940
- const canvas = document.createElement('canvas');
941
- canvas.width = 32;
942
- canvas.height = 32;
943
- const context = canvas.getContext('2d');
944
- const gradient = context.createRadialGradient(16, 16, 0, 16, 16, 16);
945
- gradient.addColorStop(0, 'rgba(255,255,255,1)');
946
- gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
947
- gradient.addColorStop(0.5, 'rgba(0,64,255,0.2)');
948
- gradient.addColorStop(1, 'rgba(0,0,0,0)');
949
- context.fillStyle = gradient;
950
- context.fillRect(0, 0, 32, 32);
951
- return canvas;
952
- }
953
-
954
- update(camera) {
955
- this.angle += this.speed;
956
-
957
- const x = Math.cos(this.angle) * this.radius;
958
- const z = Math.sin(this.angle * 0.7) * this.radius;
959
- const y = Math.sin(this.angle * 0.3) * 100;
960
-
961
- this.group.position.set(x, y, z);
962
-
963
- this.tailPositions.unshift(new THREE.Vector3(x, y, z));
964
- if (this.tailPositions.length > 50) this.tailPositions.pop();
965
-
966
- const positions = this.tail.geometry.attributes.position.array;
967
- for (let i = 0; i < this.tailPositions.length; i++) {
968
- const p = this.tailPositions[i];
969
- positions[i * 3] = p.x;
970
- positions[i * 3 + 1] = p.y;
971
- positions[i * 3 + 2] = p.z;
972
- }
973
- this.tail.geometry.attributes.position.needsUpdate = true;
974
-
975
- this.wordTimer++;
976
- if (this.wordTimer > 300) {
977
- this.spawnWord();
978
- this.wordTimer = 0;
979
- }
980
-
981
- if (this.currentWordMesh) {
982
- this.currentWordMesh.lookAt(camera.position);
983
- }
984
- }
985
-
986
- spawnWord() {
987
- if (this.currentWordMesh) {
988
- this.group.remove(this.currentWordMesh);
989
- this.currentWordMesh.geometry.dispose();
990
- }
991
-
992
- const word = this.words[Math.floor(Math.random() * this.words.length)];
993
- const geo = new TextGeometry(word, {
994
- font: this.font,
995
- size: 4,
996
- height: 0.2
997
- });
998
- geo.center();
999
- const mat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
1000
- this.currentWordMesh = new THREE.Mesh(geo, mat);
1001
- this.currentWordMesh.position.y = 8;
1002
- this.group.add(this.currentWordMesh);
1003
- }
1004
- }
1005
-
1006
- class GalaxySystem {
1007
- constructor(appInstance) {
1008
- this.app = appInstance;
1009
- this.scene = appInstance.world.scene;
1010
- this.container = new THREE.Group();
1011
- this.scene.add(this.container);
1012
- this.userNodes = new Map();
1013
- }
1014
-
1015
- rebuild(usersMap) {
1016
- this.clear();
1017
- const users = Array.from(usersMap.values());
1018
-
1019
- const userIndex = users.findIndex(u => u.id === this.app.auth.currentUser?.uid);
1020
- if (userIndex === -1 && this.app.auth.currentUser) {
1021
- // Si el usuario actual no tiene mapas, agrégalo virtualmente al final
1022
- users.push({
1023
- id: this.app.auth.currentUser.uid,
1024
- username: this.app.auth.currentUsername,
1025
- maps: []
1026
- });
1027
- }
1028
-
1029
- users.forEach((user, index) => {
1030
- const angle = index * 2.5;
1031
- const radius = Math.sqrt(index) * Config.CONSTANTS.GALAXY_SPACING;
1032
- const x = Math.cos(angle) * radius;
1033
- const z = Math.sin(angle) * radius;
1034
- const origin = new THREE.Vector3(x, 0, z);
1035
-
1036
- this.createUserGalaxy(user, origin);
1037
- });
1038
- }
1039
-
1040
- createUserGalaxy(user, origin) {
1041
- const isMe = user.id === this.app.auth.currentUser?.uid;
1042
-
1043
- const coreGeo = new THREE.IcosahedronGeometry(Config.CONSTANTS.USER_SPHERE_RADIUS, 1);
1044
- const coreMat = new THREE.MeshStandardMaterial({
1045
- color: isMe ? Config.COLORS.primary : Config.COLORS.secondary,
1046
- emissive: isMe ? 0x0044aa : 0x001133,
1047
- emissiveIntensity: 0.8,
1048
- wireframe: true
1049
- });
1050
- const core = new THREE.Mesh(coreGeo, coreMat);
1051
- core.position.copy(origin);
1052
- core.userData = { type: 'user', id: user.id, username: user.username };
1053
- this.container.add(core);
1054
-
1055
- // Anillos rotatorios decorativos
1056
- const ringGeo = new THREE.TorusGeometry(Config.CONSTANTS.USER_SPHERE_RADIUS * 1.5, 0.1, 8, 32);
1057
- const ringMat = new THREE.MeshBasicMaterial({ color: isMe ? 0xffffff : 0x555555, transparent: true, opacity: 0.3 });
1058
- const ring = new THREE.Mesh(ringGeo, ringMat);
1059
- ring.position.copy(origin);
1060
- ring.rotation.x = Math.PI / 2;
1061
- this.container.add(ring);
1062
-
1063
- // Animación del anillo guardada en userData para el loop
1064
- ring.userData = { animate: true, speed: 0.01 };
1065
-
1066
- if (this.app.world.font) {
1067
- const textGeo = new TextGeometry(user.username, {
1068
- font: this.app.world.font,
1069
- size: 2.5,
1070
- height: 0.1
1071
- });
1072
- textGeo.center();
1073
- const textMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
1074
- const text = new THREE.Mesh(textGeo, textMat);
1075
- text.position.copy(origin);
1076
- text.position.y += 10;
1077
- text.userData = { isBillboard: true };
1078
- this.container.add(text);
1079
- }
1080
-
1081
- this.buildNeurons(user.maps, origin);
1082
- this.userNodes.set(user.id, { origin, mesh: core });
1083
- }
1084
-
1085
- buildNeurons(maps, origin) {
1086
- maps.forEach((map, index) => {
1087
- // Cálculo de espiral esférica
1088
- const phi = index * 0.5; // Espaciado angular
1089
- const height = (index * Config.CONSTANTS.SPIRAL_VERTICAL_STEP) - (maps.length * Config.CONSTANTS.SPIRAL_VERTICAL_STEP / 2);
1090
- const r = 20 + (index * 1.2);
1091
-
1092
- const x = r * Math.cos(phi);
1093
- const z = r * Math.sin(phi);
1094
- const y = height;
1095
-
1096
- const pos = new THREE.Vector3(x, y, z).add(origin);
1097
- const color = Utils.stringToColor(map.topic);
1098
-
1099
- // Línea conector al centro
1100
- const points = [origin, pos];
1101
- const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
1102
- const lineMat = new THREE.LineBasicMaterial({
1103
- color: color,
1104
- transparent: true,
1105
- opacity: 0.2
1106
- });
1107
- this.container.add(new THREE.Line(lineGeo, lineMat));
1108
-
1109
- try {
1110
- const data = JSON.parse(map.data);
1111
- this.buildTreeRecursive(data.lista_palabras, pos, 1, color);
1112
-
1113
- // Título del mapa
1114
- if (this.app.world.font) {
1115
- const labelGeo = new TextGeometry(map.topic, {
1116
- font: this.app.world.font,
1117
- size: 1.2,
1118
- height: 0.05
1119
- });
1120
- const labelMat = new THREE.MeshBasicMaterial({ color: color });
1121
- const label = new THREE.Mesh(labelGeo, labelMat);
1122
- label.position.copy(pos).add(new THREE.Vector3(0, 2, 0));
1123
- label.userData = { isBillboard: true };
1124
- this.container.add(label);
1125
- }
1126
- } catch (e) {
1127
- console.error("Error parsing map data", e);
1128
- }
1129
- });
1130
- }
1131
-
1132
- buildTreeRecursive(items, parentPos, level, color) {
1133
- if (!items || level > 3) return;
1134
-
1135
- const radius = 10 / level;
1136
-
1137
- items.forEach((item, i) => {
1138
- let text = item.palabra_principal || item.palabra_variante || item;
1139
- let children = item.variantes || item.sub_variantes || [];
1140
-
1141
- // Distribución aleatoria esférica local
1142
- const u = Math.random();
1143
- const v = Math.random();
1144
- const theta = 2 * Math.PI * u;
1145
- const phi = Math.acos(2 * v - 1);
1146
-
1147
- const lx = radius * Math.sin(phi) * Math.cos(theta);
1148
- const ly = radius * Math.sin(phi) * Math.sin(theta);
1149
- const lz = radius * Math.cos(phi);
1150
-
1151
- const pos = new THREE.Vector3(lx, ly, lz).add(parentPos);
1152
-
1153
- const nodeSize = 0.5 / level;
1154
- const geo = new THREE.SphereGeometry(nodeSize, 8, 8);
1155
- const mat = new THREE.MeshBasicMaterial({ color: color });
1156
- const mesh = new THREE.Mesh(geo, mat);
1157
- mesh.position.copy(pos);
1158
- mesh.userData = { type: 'neuron', text: text, level: level };
1159
- this.container.add(mesh);
1160
-
1161
- const lineGeo = new THREE.BufferGeometry().setFromPoints([parentPos, pos]);
1162
- const lineMat = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.15 });
1163
- this.container.add(new THREE.Line(lineGeo, lineMat));
1164
-
1165
- this.buildTreeRecursive(children, pos, level + 1, color);
1166
- });
1167
- }
1168
-
1169
- clear() {
1170
- while(this.container.children.length > 0){
1171
- const obj = this.container.children[0];
1172
- this.container.remove(obj);
1173
- if(obj.geometry) obj.geometry.dispose();
1174
- if(obj.material) obj.material.dispose();
1175
- }
1176
- this.userNodes.clear();
1177
- }
1178
-
1179
- update() {
1180
- this.container.children.forEach(child => {
1181
- if (child.userData.animate) {
1182
- child.rotation.z += child.userData.speed;
1183
- }
1184
- if (child.userData.isBillboard) {
1185
- child.lookAt(this.app.world.camera.position);
1186
- }
1187
- });
1188
- }
1189
- }
1190
-
1191
- class Minimap {
1192
- constructor(appInstance) {
1193
- this.app = appInstance;
1194
- this.canvas = document.getElementById('minimap-canvas');
1195
- this.ctx = this.canvas.getContext('2d');
1196
- this.scale = 0.05;
1197
- this.width = this.canvas.width;
1198
- this.height = this.canvas.height;
1199
- this.cx = this.width / 2;
1200
- this.cy = this.height / 2;
1201
-
1202
- this.setupInteractions();
1203
- }
1204
-
1205
- setupInteractions() {
1206
- document.getElementById('map-zoom-in').onclick = () => { this.scale *= 1.2; this.draw(); };
1207
- document.getElementById('map-zoom-out').onclick = () => { this.scale /= 1.2; this.draw(); };
1208
-
1209
- this.canvas.addEventListener('click', (e) => {
1210
- const rect = this.canvas.getBoundingClientRect();
1211
- const x = e.clientX - rect.left;
1212
- const y = e.clientY - rect.top;
1213
- this.handleClick(x, y);
1214
- });
1215
- }
1216
-
1217
- draw() {
1218
- this.ctx.fillStyle = '#000000';
1219
- this.ctx.fillRect(0, 0, this.width, this.height);
1220
-
1221
- // Grid
1222
- this.ctx.strokeStyle = '#112233';
1223
- this.ctx.lineWidth = 1;
1224
- this.ctx.beginPath();
1225
- const gridSize = 20;
1226
- for(let x=0; x<this.width; x+=gridSize) { this.ctx.moveTo(x,0); this.ctx.lineTo(x,this.height); }
1227
- for(let y=0; y<this.height; y+=gridSize) { this.ctx.moveTo(0,y); this.ctx.lineTo(this.width,y); }
1228
- this.ctx.stroke();
1229
-
1230
- const galaxy = this.app.world.galaxySystem;
1231
-
1232
- galaxy.userNodes.forEach((data, uid) => {
1233
- const mapX = this.cx + data.origin.x * this.scale;
1234
- const mapY = this.cy + data.origin.z * this.scale;
1235
-
1236
- const isMe = uid === this.app.auth.currentUser?.uid;
1237
-
1238
- this.ctx.fillStyle = isMe ? '#00f3ff' : '#0066ff';
1239
- this.ctx.beginPath();
1240
- this.ctx.arc(mapX, mapY, isMe ? 4 : 2, 0, Math.PI * 2);
1241
- this.ctx.fill();
1242
-
1243
- // Nombre
1244
- if (isMe) {
1245
- this.ctx.fillStyle = '#ffffff';
1246
- this.ctx.font = '10px Rajdhani';
1247
- this.ctx.fillText("YO", mapX + 6, mapY + 3);
1248
- }
1249
- });
1250
- }
1251
-
1252
- handleClick(mx, my) {
1253
- const worldX = (mx - this.cx) / this.scale;
1254
- const worldZ = (my - this.cy) / this.scale;
1255
-
1256
- let closest = null;
1257
- let minDist = Infinity;
1258
-
1259
- this.app.world.galaxySystem.userNodes.forEach((data) => {
1260
- const dx = data.origin.x - worldX;
1261
- const dz = data.origin.z - worldZ;
1262
- const dist = Math.sqrt(dx*dx + dz*dz);
1263
- if (dist < minDist) {
1264
- minDist = dist;
1265
- closest = data.origin;
1266
- }
1267
- });
1268
-
1269
- if (closest && minDist < 1000) {
1270
- this.app.world.cameraFlyTo(closest);
1271
- }
1272
- }
1273
- }
1274
-
1275
- class World3D {
1276
- constructor(appInstance) {
1277
- this.app = appInstance;
1278
- this.container = document.getElementById('canvas-container');
1279
- this.font = null;
1280
-
1281
- this.initThree();
1282
- this.initStars();
1283
-
1284
- this.galaxySystem = new GalaxySystem(this.app);
1285
- this.raycaster = new THREE.Raycaster();
1286
- this.mouse = new THREE.Vector2();
1287
-
1288
- window.addEventListener('resize', () => this.onResize());
1289
- document.addEventListener('mousemove', (e) => this.onMouseMove(e));
1290
- }
1291
-
1292
- initThree() {
1293
- this.scene = new THREE.Scene();
1294
- this.scene.fog = new THREE.FogExp2(Config.COLORS.bg, 0.0008);
1295
-
1296
- this.camera = new THREE.PerspectiveCamera(Config.CONSTANTS.CAMERA_FOV, window.innerWidth / window.innerHeight, 0.1, Config.CONSTANTS.CAMERA_FAR);
1297
- this.camera.position.set(0, 100, 200);
1298
-
1299
- this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
1300
- this.renderer.setSize(window.innerWidth, window.innerHeight);
1301
- this.renderer.setPixelRatio(window.devicePixelRatio);
1302
- this.renderer.setClearColor(Config.COLORS.bg);
1303
- this.container.appendChild(this.renderer.domElement);
1304
-
1305
- this.controls = new OrbitControls(this.camera, this.renderer.domElement);
1306
- this.controls.enableDamping = true;
1307
- this.controls.dampingFactor = 0.05;
1308
- this.controls.maxDistance = 2000;
1309
-
1310
- const ambient = new THREE.AmbientLight(0xffffff, 0.5);
1311
- this.scene.add(ambient);
1312
- const dir = new THREE.DirectionalLight(0xffffff, 1);
1313
- dir.position.set(100, 200, 100);
1314
- this.scene.add(dir);
1315
- }
1316
-
1317
- initStars() {
1318
- const count = 8000;
1319
- const geo = new THREE.BufferGeometry();
1320
- const pos = new Float32Array(count * 3);
1321
- const colors = new Float32Array(count * 3);
1322
-
1323
- for(let i=0; i<count; i++) {
1324
- pos[i*3] = (Math.random()-0.5) * 4000;
1325
- pos[i*3+1] = (Math.random()-0.5) * 2000;
1326
- pos[i*3+2] = (Math.random()-0.5) * 4000;
1327
-
1328
- const starType = Math.random();
1329
- if (starType > 0.9) {
1330
- colors[i*3] = 0.5; colors[i*3+1] = 0.8; colors[i*3+2] = 1;
1331
- } else if (starType > 0.7) {
1332
- colors[i*3] = 1; colors[i*3+1] = 0.9; colors[i*3+2] = 0.6;
1333
- } else {
1334
- colors[i*3] = 1; colors[i*3+1] = 1; colors[i*3+2] = 1;
1335
- }
1336
- }
1337
-
1338
- geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
1339
- geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
1340
-
1341
- const mat = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8 });
1342
- this.stars = new THREE.Points(geo, mat);
1343
- this.scene.add(this.stars);
1344
- }
1345
-
1346
- async loadResources() {
1347
- return new Promise((resolve) => {
1348
- const loader = new FontLoader();
1349
- loader.load('https://cdn.jsdelivr.net/npm/three@0.128.0/examples/fonts/helvetiker_bold.typeface.json', (font) => {
1350
- this.font = font;
1351
- this.comet = new Comet(this.scene, font);
1352
- resolve();
1353
- }, undefined, (err) => {
1354
- console.error("Font loading error:", err);
1355
- resolve(); // Continue without font
1356
- });
1357
- });
1358
- }
1359
-
1360
- cameraFlyTo(target) {
1361
- new TWEEN.Tween(this.camera.position)
1362
- .to({ x: target.x, y: target.y + 60, z: target.z + 120 }, 2000)
1363
- .easing(TWEEN.Easing.Cubic.InOut)
1364
- .start();
1365
-
1366
- new TWEEN.Tween(this.controls.target)
1367
- .to({ x: target.x, y: target.y, z: target.z }, 2000)
1368
- .easing(TWEEN.Easing.Cubic.InOut)
1369
- .start();
1370
- }
1371
-
1372
- onResize() {
1373
- this.camera.aspect = window.innerWidth / window.innerHeight;
1374
- this.camera.updateProjectionMatrix();
1375
- this.renderer.setSize(window.innerWidth, window.innerHeight);
1376
- }
1377
-
1378
- onMouseMove(e) {
1379
- this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
1380
- this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
1381
-
1382
- const tooltip = document.getElementById('tooltip');
1383
- tooltip.style.left = (e.clientX + 20) + 'px';
1384
- tooltip.style.top = (e.clientY + 20) + 'px';
1385
- }
1386
-
1387
- animate() {
1388
- requestAnimationFrame(() => this.animate());
1389
- TWEEN.update();
1390
- this.controls.update();
1391
-
1392
- if (this.comet) this.comet.update(this.camera);
1393
- if (this.galaxySystem) this.galaxySystem.update();
1394
- if (this.stars) this.stars.rotation.y += 0.0001;
1395
-
1396
- // Raycasting logic
1397
- this.raycaster.setFromCamera(this.mouse, this.camera);
1398
- const intersects = this.raycaster.intersectObjects(this.galaxySystem.container.children);
1399
-
1400
- const tooltip = document.getElementById('tooltip');
1401
- if (intersects.length > 0) {
1402
- const obj = intersects[0].object;
1403
- if (obj.userData && (obj.userData.text || obj.userData.username)) {
1404
- tooltip.style.display = 'block';
1405
- const title = obj.userData.username || obj.userData.text;
1406
- const type = obj.userData.type || 'NODO DE DATO';
1407
- tooltip.innerHTML = `
1408
- <div class="tooltip-title">${title}</div>
1409
- <div class="tooltip-meta">TIPO: ${type.toUpperCase()}</div>
1410
- `;
1411
- document.body.style.cursor = 'pointer';
1412
- } else {
1413
- tooltip.style.display = 'none';
1414
- document.body.style.cursor = 'default';
1415
- }
1416
- } else {
1417
- tooltip.style.display = 'none';
1418
- document.body.style.cursor = 'default';
1419
- }
1420
-
1421
- this.renderer.render(this.scene, this.camera);
1422
-
1423
- if (this.app.minimap) this.app.minimap.draw();
1424
- }
1425
- }
1426
-
1427
- class UIManager {
1428
- constructor(appInstance) {
1429
- this.app = appInstance;
1430
- this.setupListeners();
1431
- }
1432
-
1433
- setupListeners() {
1434
- const s1 = document.getElementById('range-l1');
1435
- const s2 = document.getElementById('range-l2');
1436
- const s3 = document.getElementById('range-l3');
1437
-
1438
- if(s1) s1.oninput = () => document.getElementById('val-l1').innerText = s1.value;
1439
- if(s2) s2.oninput = () => document.getElementById('val-l2').innerText = s2.value;
1440
- if(s3) s3.oninput = () => document.getElementById('val-l3').innerText = s3.value;
1441
-
1442
- const vizBtn = document.getElementById('btn-visualize');
1443
- if(vizBtn) vizBtn.addEventListener('click', () => this.handleVisualize());
1444
- }
1445
-
1446
- async handleVisualize() {
1447
- const input = document.getElementById('seed-input');
1448
- const topic = input.value.trim();
1449
- if (!topic) return;
1450
-
1451
- const btn = document.getElementById('btn-visualize');
1452
- btn.disabled = true;
1453
- btn.innerHTML = 'PROCESANDO NEURONA...';
1454
-
1455
- const bar = document.getElementById('progress-bar');
1456
- bar.style.width = '20%';
1457
-
1458
- try {
1459
- const levels = {
1460
- l1: document.getElementById('range-l1').value,
1461
- l2: document.getElementById('range-l2').value,
1462
- l3: document.getElementById('range-l3').value
1463
- };
1464
-
1465
- bar.style.width = '60%';
1466
- const data = await this.app.ai.generate(topic, levels);
1467
-
1468
- bar.style.width = '80%';
1469
- await this.app.data.saveNeuron(topic, data);
1470
-
1471
- bar.style.width = '100%';
1472
- input.value = '';
1473
-
1474
- } catch (error) {
1475
- console.error(error);
1476
- alert("ERROR EN LA GENERACIÓN DE LA NEURONA");
1477
- } finally {
1478
- setTimeout(() => {
1479
- btn.disabled = false;
1480
- btn.innerHTML = `
1481
- <span>SEMBRAR EN LA RED</span>
1482
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1483
- <circle cx="12" cy="12" r="10"></circle>
1484
- <line x1="12" y1="8" x2="12" y2="16"></line>
1485
- <line x1="8" y1="12" x2="16" y2="12"></line>
1486
- </svg>
1487
- `;
1488
- bar.style.width = '0%';
1489
- }, 1000);
1490
- }
1491
- }
1492
-
1493
- updateUserList(usersMap) {
1494
- const list = document.getElementById('users-list');
1495
- list.innerHTML = '';
1496
-
1497
- usersMap.forEach((user) => {
1498
- const div = document.createElement('div');
1499
- div.className = 'user-item';
1500
- div.innerHTML = `
1501
- <span>${user.username}</span>
1502
- <span class="user-count-badge">${user.maps.length}</span>
1503
- `;
1504
- div.onclick = () => {
1505
- const node = this.app.world.galaxySystem.userNodes.get(user.id);
1506
- if (node) this.app.world.cameraFlyTo(node.origin);
1507
- };
1508
- list.appendChild(div);
1509
- });
1510
- }
1511
-
1512
- showLogin() {
1513
- const overlay = document.getElementById('login-overlay');
1514
- if(overlay) overlay.classList.remove('fade-out');
1515
- }
1516
-
1517
- hideLogin() {
1518
- const overlay = document.getElementById('login-overlay');
1519
- if(overlay) overlay.classList.add('fade-out');
1520
- }
1521
- }
1522
-
1523
- class App {
1524
- constructor() {
1525
- this.auth = new AuthManager(this);
1526
- this.data = new DataManager(this);
1527
- this.world = new World3D(this);
1528
- this.ui = new UIManager(this);
1529
- this.ai = new AIManager(this);
1530
- this.minimap = new Minimap(this);
1531
- }
1532
-
1533
- async start() {
1534
- try {
1535
- await this.world.loadResources();
1536
- await this.auth.init();
1537
- this.world.animate();
1538
- } catch (err) {
1539
- console.error("FATAL ERROR IN APP START:", err);
1540
- const loader = document.getElementById('loading-text');
1541
- if(loader) loader.innerText = "ERROR FATAL: " + err.message;
1542
- }
1543
- }
1544
- }
1545
-
1546
- const app = new App();
1547
- app.start();
1548
 
1549
- </script>
1550
  </body>
1551
- </html>
 
1
  <!DOCTYPE html>
2
  <html lang="es">
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>ECOTAGS · Galaxias 3D</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="./styles.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </head>
11
  <body>
12
+ <div id="container"></div>
13
+ <div id="tooltip"></div>
14
+
15
+ <div id="ui">
16
+ <div id="hdr">
17
+ <div id="title">
18
+ <svg class="w-6 h-6 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/></svg>
19
+ <span>ECOTAGS</span>
20
+ </div>
21
+ <div class="flex items-center gap-2">
22
+ <span id="statusPill" class="hidden">Conectado</span>
23
+ <button id="logoutBtn" class="btn bg-red-600 text-white px-3 py-1 rounded text-sm hidden">Cerrar sesión</button>
24
+ </div>
 
 
 
 
 
 
25
  </div>
26
+ <div id="panel">
27
+ <div class="mb-3">
28
+ <div class="text-xs text-gray-400 mb-1">Usuario</div>
29
+ <div id="usernameLabel" class="text-green-300 font-semibold">-</div>
30
+ </div>
31
+ <input id="topicInput" type="text" class="w-full p-3 rounded-lg bg-gray-800/70 text-white border border-gray-700 focus:outline-none focus:border-green-500 mb-3" placeholder="Escribe uno o más hashtags">
32
+ <div class="grid grid-cols-3 gap-3 mb-4 text-sm">
33
+ <div>
34
+ <label class="text-gray-300">Nivel 1: <span id="level1Value" class="text-blue-300">10</span></label>
35
+ <input id="level1Slider" type="range" min="1" max="15" value="10">
 
 
 
36
  </div>
37
+ <div>
38
+ <label class="text-gray-300">Nivel 2: <span id="level2Value" class="text-blue-300">5</span></label>
39
+ <input id="level2Slider" type="range" min="3" max="8" value="5">
 
 
40
  </div>
41
+ <div>
42
+ <label class="text-gray-300">Nivel 3: <span id="level3Value" class="text-blue-300">3</span></label>
43
+ <input id="level3Slider" type="range" min="1" max="3" value="3">
 
 
 
 
44
  </div>
45
+ </div>
46
+ <button id="seedBtn" class="btn w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-500 hover:to-blue-500 text-white font-bold py-3 rounded-lg flex items-center justify-center gap-2">
47
+ <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/></svg>
48
+ <span>Sembrar</span>
49
+ </button>
50
+ <div id="progressBarContainer" class="mt-3"><div id="progressBar"></div></div>
51
+ <details class="mt-4 bg-gray-800/60 rounded-md p-3 border border-gray-700">
52
+ <summary class="cursor-pointer text-sm text-blue-300 font-semibold">Configuración</summary>
53
+ <div class="mt-3 space-y-2 text-sm">
54
+ <div>
55
+ <label class="block text-gray-300">Gemini API Key</label>
56
+ <input id="geminiKeyInput" type="password" class="w-full p-2 rounded bg-gray-800 text-white border border-gray-700 focus:outline-none focus:border-blue-500" placeholder="AIza...">
57
+ </div>
58
+ <div class="flex items-center gap-2">
59
+ <button id="saveGeminiKeyBtn" class="btn bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded">Guardar clave</button>
60
+ <span id="geminiKeyStatus" class="text-gray-400"></span>
61
+ </div>
62
  </div>
63
+ </details>
64
+ <div class="mt-4">
65
+ <h2 class="font-bold text-lg text-blue-300 mb-2">Galaxias de Usuarios</h2>
66
+ <div id="userList" class="w-full max-h-[22vh] overflow-y-auto text-gray-300 pr-1 text-sm"></div>
67
+ </div>
68
+ <div id="minimapContainer">
69
+ <div class="flex justify-between items-center mb-2">
70
+ <h2 class="font-bold text-lg text-blue-300">Minimapa</h2>
71
+ <div class="flex gap-1">
72
+ <button id="zoomOutButton" class="btn w-7 h-7 bg-gray-700 hover:bg-gray-600 rounded text-lg font-bold flex items-center justify-center">-</button>
73
+ <button id="zoomInButton" class="btn w-7 h-7 bg-gray-700 hover:bg-gray-600 rounded text-lg font-bold flex items-center justify-center">+</button>
74
+ </div>
75
  </div>
76
+ <canvas id="minimap" width="340" height="200"></canvas>
77
+ </div>
 
 
 
 
 
 
 
78
  </div>
79
+ </div>
80
 
81
+ <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
82
+ <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/controls/OrbitControls.js"></script>
83
+ <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/geometries/TextGeometry.js"></script>
84
+ <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/loaders/FontLoader.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ <script type="module" src="./src/main.js"></script>
87
  </body>
88
+ </html>
src/config.js ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const firebaseConfig={apiKey:"AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",authDomain:"neuronal-1f3b9.firebaseapp.com",projectId:"neuronal-1f3b9",storageBucket:"neuronal-1f3b9.firebasestorage.app",messagingSenderId:"208887839866",appId:"1:208887839866:web:adbb697dd0b63195b10fc3",measurementId:"G-102SEBLQFJ"};
2
+ export const appId="neuronal-1f3b9";
3
+ export const gemKeyGet=()=>localStorage.getItem('GEMINI_API_KEY')||'';
4
+ export const gemKeySet=k=>localStorage.setItem('GEMINI_API_KEY',k||'');
src/db.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import {getFirestore, collection, addDoc, onSnapshot, query, serverTimestamp} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
2
+ import {getDB} from './firebase.js';
3
+
4
+ export async function saveNeuron(n){const db=getDB();const ref=collection(db,'artifacts',n.appId,'public','data','neurons');await addDoc(ref,{userId:n.userId,username:n.username,label:n.label,level:n.level,position:{x:n.position.x,y:n.position.y,z:n.position.z},topic:n.topic||null,createdAt:serverTimestamp()});}
5
+ export function subscribeNeurons(appId,cb){const db=getDB();const ref=query(collection(db,'artifacts',appId,'public','data','neurons'));return onSnapshot(ref,cb);
6
+ }
src/firebase.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {initializeApp} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
2
+ import {getAuth, onAuthStateChanged, signInAnonymously, signOut as fbSignOut} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
3
+ import {getFirestore, doc, setDoc, getDoc, serverTimestamp} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
4
+ import {firebaseConfig, appId} from './config.js';
5
+ import {autoUsername} from './utils.js';
6
+
7
+ let app, db, auth, userId=null, username=null, ready=false;
8
+ let profilesCache={};
9
+
10
+ async function fetchIP(){try{const r=await fetch('https://api.ipify.org?format=json');if(!r.ok)throw 0;const j=await r.json();return j.ip||'';}catch{return''}}
11
+
12
+ export async function initFirebase(){app=initializeApp(firebaseConfig);db=getFirestore(app);auth=getAuth(app);const ip=await fetchIP();const last=localStorage.getItem('last_ip')||'';if(ip&&ip!==last){localStorage.removeItem('local_username');localStorage.setItem('last_ip',ip);}onAuthStateChanged(auth,async u=>{if(u){userId=u.uid;ready=true;const p=await ensureProfile(userId);username=p.username;renderAuthUI(true);}else{try{await signInAnonymously(auth);}catch{userId=localStorage.getItem('local_uid')||crypto.randomUUID();localStorage.setItem('local_uid',userId);let uname=localStorage.getItem('local_username');if(!uname){uname=autoUsername();localStorage.setItem('local_username',uname);}username=uname;ready=true;renderAuthUI(false);}}});return new Promise(res=>{const t=setInterval(()=>{if(ready){clearInterval(t);res();}},50);});}
13
+
14
+ export function getDB(){return db;}
15
+ export function getAuthState(){return{userId,username,ready}};
16
+ export async function ensureProfile(uid){if(profilesCache[uid])return profilesCache[uid];try{const ref=doc(db,'artifacts',appId,'users',uid,'user_data','profile');const s=await getDoc(ref);if(s.exists()){const p=s.data();profilesCache[uid]=p;return p;}const uname=localStorage.getItem('local_username')||autoUsername();const p={username:uname,createdAt:serverTimestamp()};await setDoc(ref,p);profilesCache[uid]=p;return p;}catch{const uname=localStorage.getItem('local_username')||autoUsername();const p={username:uname};profilesCache[uid]=p;return p;}}
17
+ export function getProfile(uid){return profilesCache[uid]||null}
18
+ export async function signOut(){try{await fbSignOut(auth);}catch{} localStorage.removeItem('local_username');}
19
+ function renderAuthUI(logged){const pill=document.getElementById('statusPill');const lo=document.getElementById('logoutBtn');const ul=document.getElementById('usernameLabel');if(logged){pill.classList.remove('hidden');lo.classList.remove('hidden');}else{pill.classList.remove('hidden');lo.classList.remove('hidden');}ul.textContent=username||'-';}
src/gemini.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import {gemKeyGet} from './config.js';
2
+
3
+ export async function callGemini(topic,n1,n2,n3){const modelId='gemini-2.0-flash-exp';const schema={type:'OBJECT',properties:{analisis:{type:'STRING'},lista_palabras:{type:'ARRAY',items:{type:'OBJECT',properties:{palabra_principal:{type:'STRING'},variantes:{type:'ARRAY',items:{type:'OBJECT',properties:{palabra_variante:{type:'STRING'},sub_variantes:{type:'ARRAY',items:{type:'STRING'}}},required:['palabra_variante','sub_variantes']}}},required:['palabra_principal','variantes']}}},required:['lista_palabras']};const sys='Eres un analista. Devuelve JSON sin acentos.';const up=`Tema: "${topic}". 1) ${n1} palabras clave. 2) ${n2} variantes por cada una. 3) ${n3} sub-variantes por variante.`;const payload={contents:[{parts:[{text:up}]}],systemInstruction:{parts:[{text:sys}]},generationConfig:{responseMimeType:'application/json',responseSchema:schema}};const key=gemKeyGet();if(!key)throw new Error('Configura tu Gemini API Key.');const url=`https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${key}`;const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});if(!r.ok){throw new Error(await r.text()||'Error Gemini');}return await r.json();}
src/main.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {initFirebase, getAuthState, ensureProfile, signOut, getProfile} from './firebase.js';
2
+ import {initScene, clearScene, usernameSphere, addNeuronMesh, drawMinimap, focusOnUser, teleportToUser, userCenter} from './scene.js';
3
+ import {initUI, handleSeed} from './ui.js';
4
+ import {subscribeNeurons} from './db.js';
5
+ import {appId} from './config.js';
6
+ import {hslFromString} from './utils.js';
7
+
8
+ const THREE=window.THREE;
9
+
10
+ await initFirebase();
11
+ initScene();
12
+ initUI();
13
+
14
+ document.getElementById('seedBtn').addEventListener('click',handleSeed);
15
+
16
+ document.getElementById('logoutBtn').addEventListener('click',async()=>{await signOut(); location.reload();});
17
+
18
+ let profiles={};
19
+
20
+ subscribeNeurons(appId, async snap=>{
21
+ clearScene();
22
+ const perUser={};
23
+ snap.forEach(d=>{const v=d.data(); if(!v.userId||!v.position||!v.label) return; if(!perUser[v.userId]) perUser[v.userId]=[]; perUser[v.userId].push(v); if(v.username && !profiles[v.userId]) profiles[v.userId]={username:v.username};});
24
+ const uids=Object.keys(perUser);
25
+ const userList=document.getElementById('userList');
26
+ if (userList) userList.innerHTML='';
27
+ for(const uid of uids){
28
+ if(!profiles[uid]){ try{ const p=await ensureProfile(uid); profiles[uid]=p; }catch{} }
29
+ const uname=(profiles[uid]?.username)||`Usuario ${uid.slice(0,4)}`;
30
+ usernameSphere(uid,uname);
31
+ const arr=perUser[uid].sort((a,b)=>(a.createdAt?.seconds||0)-(b.createdAt?.seconds||0));
32
+ for(const n of arr){
33
+ const col=hslFromString(n.label).color;
34
+ const pos=new THREE.Vector3(n.position.x,n.position.y,n.position.z);
35
+ addNeuronMesh(uid,n.label,n.level||1,pos,col);
36
+ }
37
+ if (userList){
38
+ const item=document.createElement('div');
39
+ item.className='p-2 mb-1 rounded-md hover:bg-gray-700 cursor-pointer';
40
+ item.textContent=uname;
41
+ item.addEventListener('click',()=> teleportToUser(uid));
42
+ userList.appendChild(item);
43
+ }
44
+ }
45
+ const me=getAuthState().userId;
46
+ drawMinimap(uids,profiles,me);
47
+ focusOnUser(me);
48
+ });
49
+
50
+ window.addEventListener('teleport',e=>{teleportToUser(e.detail.uid)});
51
+
52
+ document.getElementById('usernameLabel').textContent=getAuthState().username||'-';
src/scene.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {hslFromString, randomWord} from './utils.js';
2
+
3
+ const THREE=window.THREE;const OrbitControls=THREE.OrbitControls;const TextGeometry=THREE.TextGeometry;const FontLoader=THREE.FontLoader;
4
+ let scene,camera,renderer,controls,raycaster,mouse,tooltip,font,rootGroup,starsGroup,comet,cometTrail=[],cometTimer=0,cometWordTimer=0,cometWord='';
5
+ let minimapCtx,minimapScale=0.025,minimapDots=[];const MINIMAP_DOT_SIZE=2;const fontLoader=new FontLoader();
6
+ const usersCenters={};
7
+
8
+ export function getUsersCenters(){return usersCenters;}
9
+ export function userCenter(uid){if(usersCenters[uid])return usersCenters[uid];let hash=0;for(let i=0;i<uid.length;i++)hash=uid.charCodeAt(i)+((hash<<5)-hash);const ang=Math.abs(hash)%360*Math.PI/180;const ring=600+(Math.abs(hash)%7)*180;const x=Math.cos(ang)*ring;const z=Math.sin(ang)*ring;const y=((hash%37)-18)*2;const v=new THREE.Vector3(x,y,z);usersCenters[uid]=v;return v;}
10
+ export function spiralPosition(uid,i){const c=userCenter(uid);const a=0.55;const step=0.9;const ang=i*a;const r=6+step*i*0.45;const y=(i%18-9)*0.22;const x=Math.cos(ang)*r;const z=Math.sin(ang)*r;return new THREE.Vector3(c.x+x,c.y+y,c.z+z);}
11
+
12
+ export function initScene(){scene=new THREE.Scene();camera=new THREE.PerspectiveCamera(70,window.innerWidth/window.innerHeight,0.1,4000);camera.position.set(0,8,28);renderer=new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance'});renderer.setSize(window.innerWidth,window.innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,2));document.getElementById('container').appendChild(renderer.domElement);controls=new OrbitControls(camera,renderer.domElement);controls.enableDamping=true;controls.dampingFactor=.06;controls.target.set(0,0,0);const amb=new THREE.AmbientLight(0xbfd4ff,.75);scene.add(amb);const dir=new THREE.DirectionalLight(0xffffff,.9);dir.position.set(6,10,7);scene.add(dir);const hemi=new THREE.HemisphereLight(0x4fc3f7,0x0b1020,.35);scene.add(hemi);raycaster=new THREE.Raycaster();mouse=new THREE.Vector2();rootGroup=new THREE.Group();scene.add(rootGroup);tooltip=document.getElementById('tooltip');addBackgroundStars();addComet();const minimap=document.getElementById('minimap');minimapCtx=minimap.getContext('2d');minimap.addEventListener('click',onMinimapClick);document.getElementById('zoomInButton').addEventListener('click',()=>{minimapScale*=1.4;drawMinimap([],{})});document.getElementById('zoomOutButton').addEventListener('click',()=>{minimapScale/=1.4;drawMinimap([],{})});window.addEventListener('resize',onResize);window.addEventListener('mousemove',onPointerMove);fontLoader.load('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/fonts/helvetiker_regular.typeface.json',f=>{font=f;});animate();}
13
+
14
+ export function clearScene(){rootGroup.clear();}
15
+ export function focusOnUser(uid){const c=userCenter(uid);controls.target.copy(c);camera.position.copy(c.clone().add(new THREE.Vector3(0,8,26)));controls.update();}
16
+ export function teleportToUser(uid){const c=userCenter(uid);controls.target.copy(c);camera.position.copy(c.clone().add(new THREE.Vector3(0,8,26)));controls.update();}
17
+
18
+ function animate(){requestAnimationFrame(animate);controls.update();updateRaycast();updateComet();renderer.render(scene,camera);}
19
+ function onResize(){camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);}
20
+ function onPointerMove(e){mouse.x=e.clientX/window.innerWidth*2-1;mouse.y=-(e.clientY/window.innerHeight)*2+1;tooltip.style.left=`${e.clientX+16}px`;tooltip.style.top=`${e.clientY}px`}
21
+ function updateRaycast(){raycaster.setFromCamera(mouse,camera);const meshes=[];rootGroup.traverse(o=>{if(o.isMesh&&!o.userData.ignoreHit)meshes.push(o)});const is=raycaster.intersectObjects(meshes,false);if(is.length>0){const o=is[0].object;const d=o.userData;let html=`<strong>${d.label||d.hashtag||'Nodo'}</strong>`;if(d.level!=null)html+=`<br>Nivel: ${d.level}`;tooltip.innerHTML=html;tooltip.style.display='block';}else{tooltip.style.display='none';}}
22
+
23
+ function addBackgroundStars(){starsGroup=new THREE.Group();const geo=new THREE.BufferGeometry();const count=2000;const positions=new Float32Array(count*3);for(let i=0;i<count;i++){const r=1200*Math.pow(Math.random(),.6)+200;const a=Math.random()*Math.PI*2;const e=(Math.random()-0.5)*0.6;const x=Math.cos(a)*r;const z=Math.sin(a)*r;const y=r*e*0.25;positions[i*3]=x;positions[i*3+1]=y;positions[i*3+2]=z;}geo.setAttribute('position',new THREE.BufferAttribute(positions,3));const mat=new THREE.PointsMaterial({color:0x88b4ff,sizeAttenuation:true,size:1.2,transparent:true,opacity:.8});const pts=new THREE.Points(geo,mat);starsGroup.add(pts);scene.add(starsGroup);}
24
+ function addComet(){const cometGeo=new THREE.SphereGeometry(0.35,16,16);const cometMat=new THREE.MeshStandardMaterial({color:0xffe08a,emissive:0xffb703,emissiveIntensity:1.2,metalness:.2,roughness:.4});comet=new THREE.Mesh(cometGeo,cometMat);scene.add(comet);comet.userData.radius=220;comet.userData.speed=0.0022;comet.userData.angle=Math.random()*Math.PI*2;comet.userData.incl=0.25;}
25
+ function updateComet(){if(!comet)return;comet.userData.angle+=comet.userData.speed;const a=comet.userData.angle;const r=comet.userData.radius;const inc=comet.userData.incl;comet.position.set(Math.cos(a)*r,Math.sin(a*inc)*25,Math.sin(a)*r);cometTimer+=1/60;cometWordTimer+=1/60;if(cometTimer>.045){cometTimer=0;leaveTrailWord();}if(cometWordTimer>3){cometWordTimer=0;cometWord=randomWord();}for(let i=cometTrail.length-1;i>=0;i--){const m=cometTrail[i];m.material.opacity-=.01;m.position.y+=.03;if(m.material.opacity<=0){m.geometry.dispose();m.material.dispose();scene.remove(m);cometTrail.splice(i,1);}}}
26
+ function leaveTrailWord(){if(!font)return;const s=cometWord||randomWord();const g=new TextGeometry(s.toUpperCase(),{font,size:.9,height:.02,curveSegments:4,bevelEnabled:false});g.computeBoundingBox();const m=new THREE.MeshBasicMaterial({color:0x4ade80,transparent:true,opacity:.8});const mesh=new THREE.Mesh(g,m);mesh.position.copy(comet.position);mesh.position.y+=.6;mesh.rotation.y=Math.random()*Math.PI;cometTrail.push(mesh);scene.add(mesh);}
27
+
28
+ export function usernameSphere(uid,uname){const center=userCenter(uid);const mat=new THREE.MeshPhysicalMaterial({color:0x60a5fa,emissive:0x1e293b,roughness:.25,metalness:.35,clearcoat:.6,clearcoatRoughness:.2});const geo=new THREE.SphereGeometry(2.2,32,32);const s=new THREE.Mesh(geo,mat);s.position.copy(center);s.userData={ignoreHit:false,label:uname};rootGroup.add(s);if(font){const tg=new TextGeometry(uname.toUpperCase(),{font,size:.9,height:.05,curveSegments:6,bevelEnabled:false});tg.computeBoundingBox();const tm=new THREE.MeshBasicMaterial({color:0x93c5fd,transparent:true,opacity:.9});const text=new THREE.Mesh(tg,tm);text.position.copy(center);text.position.y+=3.1;text.position.x-=(tg.boundingBox.max.x-tg.boundingBox.min.x)/2;text.userData={ignoreHit:false,label:uname};rootGroup.add(text);}}
29
+
30
+ export function addNeuronMesh(uid,label,level,pos,baseColor){const mat=new THREE.MeshStandardMaterial({color:new THREE.Color(baseColor),roughness:.4,metalness:.2,emissive:new THREE.Color(baseColor).multiplyScalar(.15)});const r=level===1?.45:level===2?.28:.18;const geo=new THREE.IcosahedronGeometry(r,1);const m=new THREE.Mesh(geo,mat);m.position.copy(pos);m.userData={label,level};const rimGeo=new THREE.RingGeometry(r*1.2,r*1.35,24);const rimMat=new THREE.MeshBasicMaterial({color:0x94ffa8,transparent:true,opacity:.18,side:THREE.DoubleSide});const rim=new THREE.Mesh(rimGeo,rimMat);rim.position.copy(pos);rim.rotation.x=Math.PI/2;rim.userData={ignoreHit:true};rootGroup.add(rim);rootGroup.add(m);if(font){const tgeo=new TextGeometry(label.toUpperCase(),{font,size:r*.9,height:.02,curveSegments:4});tgeo.computeBoundingBox();const tmat=new THREE.MeshBasicMaterial({color:new THREE.Color(baseColor),transparent:true,opacity:.85});const tm=new THREE.Mesh(tgeo,tmat);tm.position.copy(pos);tm.position.y+=r+.08;tm.position.x-=(tgeo.boundingBox.max.x-tgeo.boundingBox.min.x)/2;tm.userData={label,level};rootGroup.add(tm);}}
31
+
32
+ export function drawMinimap(uids,profiles,me){if(!minimapCtx)return;const c=minimapCtx.canvas;minimapCtx.clearRect(0,0,c.width,c.height);minimapCtx.fillStyle='#0b1321';minimapCtx.fillRect(0,0,c.width,c.height);minimapDots=[];let cx=0,cz=0;if(me){const cc=userCenter(me);cx=cc.x;cz=cc.z;}for(const uid of uids){const center=userCenter(uid);const relX=(center.x-cx)*minimapScale;const relZ=(center.z-cz)*minimapScale;const x=c.width/2+relX;const y=c.height/2+relZ;const self=uid===me;const col=self?'#fde047':'#06b6d4';const size=MINIMAP_DOT_SIZE+(self?1:0);minimapCtx.beginPath();minimapCtx.arc(x,y,size,0,Math.PI*2);minimapCtx.fillStyle=col;minimapCtx.fill();minimapCtx.font='10px Orbitron';minimapCtx.fillStyle='#cbd5e1';const nm=(profiles[uid]?.username)||`Usr ${uid.slice(0,4)}`;minimapCtx.fillText(nm,x+size+3,y+3);minimapDots.push({x,y,uid});}}
33
+ function onMinimapClick(e){const c=minimapCtx.canvas;const r=c.getBoundingClientRect();const x=e.clientX-r.left;const y=e.clientY-r.top;let pick=null,dmin=12;for(let i=minimapDots.length-1;i>=0;i--){const d=Math.hypot(x-minimapDots[i].x,y-minimapDots[i].y);if(d<dmin){dmin=d;pick=minimapDots[i].uid;}}if(pick)window.dispatchEvent(new CustomEvent('teleport',{detail:{uid:pick}}));}
src/ui.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import {gemKeySet, gemKeyGet, appId} from './config.js';
2
+ import {normalizeString, hslFromString} from './utils.js';
3
+ import {callGemini} from './gemini.js';
4
+ import {saveNeuron} from './db.js';
5
+ import {spiralPosition, addNeuronMesh, userCenter} from './scene.js';
6
+ import {getAuthState} from './firebase.js';
7
+
8
+ export function initUI(){document.getElementById('level1Slider').addEventListener('input',e=>document.getElementById('level1Value').innerText=e.target.value);document.getElementById('level2Slider').addEventListener('input',e=>document.getElementById('level2Value').innerText=e.target.value);document.getElementById('level3Slider').addEventListener('input',e=>document.getElementById('level3Value').innerText=e.target.value);document.getElementById('saveGeminiKeyBtn').addEventListener('click',()=>{const k=document.getElementById('geminiKeyInput').value.trim();gemKeySet(k);const s=document.getElementById('geminiKeyStatus');s.textContent='Guardada';setTimeout(()=>s.textContent='',1200)});const egk=gemKeyGet();if(egk)document.getElementById('geminiKeyInput').value=egk;}
9
+
10
+ export async function handleSeed(){const {userId,username}=getAuthState();const topic=normalizeString(document.getElementById('topicInput').value.trim());if(!topic) return;const n1=parseInt(document.getElementById('level1Slider').value,10);const n2=parseInt(document.getElementById('level2Slider').value,10);const n3=parseInt(document.getElementById('level3Slider').value,10);const btn=document.getElementById('seedBtn');const pbc=document.getElementById('progressBarContainer');const pb=document.getElementById('progressBar');btn.disabled=true;const bak=btn.innerHTML;btn.innerHTML='Analizando...';pbc.style.display='block';pb.style.transition='none';pb.style.width='0%';void pb.offsetWidth;pb.style.transition='width 18s ease-out';pb.style.width='92%';try{const data=await callGemini(topic,n1,n2,n3);const cand=data.candidates?.[0];const text=cand?.content?.parts?.[0]?.text||'{}';const parsed=JSON.parse(text);const lista=parsed.lista_palabras||[];const center=userCenter(userId);const rootCol=hslFromString(topic).color;addNeuronMesh(userId,topic,0,center.clone().add(new THREE.Vector3(0,3.1,0)),rootCol);let baseIndex=0;for(const l1 of lista){const tag1=normalizeString(l1.palabra_principal);const c1=hslFromString(tag1).color;const p1=spiralPosition(userId,baseIndex++);await saveNeuron({appId,userId,username,label:tag1,level:1,position:p1,topic});addNeuronMesh(userId,tag1,1,p1,c1);const variantes=l1.variantes||[];for(const v of variantes){const tag2=normalizeString(v.palabra_variante);const p2=spiralPosition(userId,baseIndex++);await saveNeuron({appId,userId,username,label:tag2,level:2,position:p2,topic});addNeuronMesh(userId,tag2,2,p2,c1);const subs=v.sub_variantes||[];for(const s of subs){const tag3=normalizeString(s);const p3=spiralPosition(userId,baseIndex++);await saveNeuron({appId,userId,username,label:tag3,level:3,position:p3,topic});addNeuronMesh(userId,tag3,3,p3,c1);}}}document.getElementById('topicInput').value='';}catch(e){}finally{btn.disabled=false;btn.innerHTML=bak;pb.style.transition='width .25s ease-in';pb.style.width='100%';setTimeout(()=>{pbc.style.display='none';pb.style.width='0%';},400)}}
src/utils.js ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export function normalizeString(s){if(!s)return'';return s.normalize('NFD').replace(/[\u0300-\u036f]/g,'');}
2
+ export function hslFromString(s){let h=0;for(let i=0;i<s.length;i++)h=s.charCodeAt(i)+((h<<5)-h);h=Math.abs(h%360);return{h,color:`hsl(${h},80%,60%)`};}
3
+ export function autoUsername(){const a=["Nova","Orion","Lyra","Vela","Aster","Cosmo","Draco","Vasto","Lumen","Aqua","Gaia","Terra","Nix","Aura","Flux","Vox"];const b=a[Math.floor(Math.random()*a.length)];const k=Math.random().toString(36).slice(2,6).toUpperCase();return`${b}-${k}`;}
4
+ export function randomWord(){const r=["eco","vida","bosque","agua","tierra","aire","luz","verde","azul","flora","fauna","clima","ciclo","sol","lunar","salud","campo","rio","mar","cielo"];return r[Math.floor(Math.random()*r.length)];}
styles.css CHANGED
@@ -1,74 +1 @@
1
- body {
2
- margin: 0;
3
- overflow: hidden;
4
- background: #020617;
5
- font-family: "Orbitron", sans-serif;
6
- }
7
-
8
- #scene {
9
- position: fixed;
10
- inset: 0;
11
- }
12
-
13
- #ui {
14
- position: fixed;
15
- top: 16px;
16
- left: 16px;
17
- z-index: 20;
18
- width: 320px;
19
- background: rgba(15, 23, 42, 0.92);
20
- padding: 16px;
21
- border-radius: 14px;
22
- color: white;
23
- }
24
-
25
- .ui-header {
26
- display: flex;
27
- justify-content: space-between;
28
- align-items: center;
29
- margin-bottom: 8px;
30
- }
31
-
32
- #username {
33
- color: #4ade80;
34
- font-size: 0.85rem;
35
- }
36
-
37
- #logoutBtn {
38
- background: #dc2626;
39
- border: none;
40
- padding: 4px 8px;
41
- font-size: 0.75rem;
42
- border-radius: 4px;
43
- color: white;
44
- cursor: pointer;
45
- }
46
-
47
- #topicInput {
48
- width: 100%;
49
- padding: 8px;
50
- margin-bottom: 8px;
51
- border-radius: 6px;
52
- background: #020617;
53
- border: 1px solid #334155;
54
- color: white;
55
- }
56
-
57
- #seedBtn {
58
- width: 100%;
59
- padding: 10px;
60
- border-radius: 6px;
61
- background: #16a34a;
62
- color: white;
63
- font-weight: bold;
64
- cursor: pointer;
65
- }
66
-
67
- #minimap {
68
- width: 100%;
69
- height: 180px;
70
- margin-top: 12px;
71
- background: #020617;
72
- border-radius: 8px;
73
- border: 1px solid #334155;
74
- }
 
1
+ body{font-family:'Orbitron',sans-serif;margin:0;overflow:hidden;background:radial-gradient(1200px 800px at 70% 10%,#0b1220 0%,#0a0f1a 40%,#070b14 70%,#04080f 100%)}#container{position:fixed;inset:0}canvas{display:block}#ui{position:fixed;top:20px;left:20px;z-index:20;background:rgba(17,24,39,.92);border:1px solid rgba(59,130,246,.18);box-shadow:0 10px 30px rgba(0,0,0,.45);border-radius:16px;width:380px;color:#e5e7eb;display:flex;flex-direction:column;max-height:calc(100vh - 40px)}#hdr{padding:14px 16px;border-bottom:1px solid rgba(59,130,246,.18);display:flex;align-items:center;justify-content:space-between;gap:10px}#title{display:flex;align-items:center;gap:10px;color:#a7f3d0;font-weight:800;letter-spacing:1px}#panel{padding:14px 16px;overflow:auto}#tooltip{position:absolute;display:none;background:rgba(0,0,0,.85);color:#fff;padding:8px 12px;border-radius:8px;z-index:30;pointer-events:none;font-size:13px;border:1px solid rgba(255,255,255,.1)}input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:8px;background:linear-gradient(90deg,#374151,#1f2937);border-radius:6px;outline:none}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;background:#22c55e;border-radius:50%;border:2px solid #052e1a;box-shadow:0 0 0 3px rgba(34,197,94,.25);cursor:pointer}#progressBarContainer{width:100%;background:#1f2937;border-radius:6px;overflow:hidden;display:none;height:8px;margin-top:10px;border:1px solid rgba(59,130,246,.18)}#progressBar{width:0;height:8px;background:linear-gradient(90deg,#22c55e,#3b82f6);box-shadow:0 0 12px #22c55e inset}#minimapContainer{width:100%;height:240px;margin-top:12px}#minimap{width:100%;height:100%;background:linear-gradient(180deg,#0d1726,#0b1321);border-radius:10px;border:1px solid rgba(148,163,184,.25);cursor:pointer}.btn{transition:all .2s ease}.btn:hover{transform:translateY(-1px);filter:brightness(1.05)}#statusPill{font-size:11px;padding:4px 8px;border-radius:999px;background:rgba(20,184,166,.15);color:#5eead4;border:1px solid rgba(45,212,191,.25)}