salomonsky commited on
Commit
45e26f6
·
verified ·
1 Parent(s): c6cfc3a

Delete main.js

Browse files
Files changed (1) hide show
  1. main.js +0 -928
main.js DELETED
@@ -1,928 +0,0 @@
1
- import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
2
- import { getAnalytics } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-analytics.js";
3
- import { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, signInAnonymously, signOut, setPersistence, browserLocalPersistence } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
4
- import { getFirestore, doc, addDoc, onSnapshot, collection, setDoc, getDoc, query, setLogLevel } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
5
-
6
- const firebaseConfig = {
7
- apiKey: "AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",
8
- authDomain: "neuronal-1f3b9.firebaseapp.com",
9
- projectId: "neuronal-1f3b9",
10
- storageBucket: "neuronal-1f3b9.firebasestorage.app",
11
- messagingSenderId: "208887839866",
12
- appId: "1:208887839866:web:adbb697dd0b63195b10fc3",
13
- measurementId: "G-102SEBLQFJ"
14
- };
15
-
16
- const DEFAULT_GEMINI_KEY = 'AIzaSyDmoQNpzgzW21f_WFCU9YbaAeI1fdOJMlo';
17
- function getLocalGeminiKey() {
18
- try { return localStorage.getItem('GEMINI_API_KEY') || DEFAULT_GEMINI_KEY; } catch { return DEFAULT_GEMINI_KEY; }
19
- }
20
- function setLocalGeminiKey(k) {
21
- try { localStorage.setItem('GEMINI_API_KEY', k || ''); } catch {}
22
- }
23
-
24
- const THREE = window.THREE;
25
- const OrbitControls = THREE.OrbitControls;
26
- const TextGeometry = THREE.TextGeometry;
27
- const FontLoader = THREE.FontLoader;
28
-
29
- let scene, camera, renderer, controls, raycaster, mouse;
30
- let composer;
31
- let hashtagGroup, tooltip;
32
- let font;
33
- let clock = new THREE.Clock();
34
-
35
- let cometGroup, cometHead, cometLight, cometText;
36
- let cometParticlesGeometry, cometParticlesMaterial, cometParticlesMesh;
37
- let cometParticlesData = [];
38
- const COMET_PARTICLE_COUNT = 400;
39
- const COMET_WORDS = ["NEXUS", "DATA", "VOID", "SIGNAL", "CYBER", "PULSE", "NODE", "FLUX", "SYNTH", "CORE", "ORBIT", "LINK"];
40
- let cometAngle = 0;
41
- let userCentroidForComet = new THREE.Vector3(0,0,0);
42
-
43
- let bgParticles;
44
-
45
- let db, auth, analytics, userId = null, appId = "neuronal-1f3b9", userProfile = null;
46
- let userMaps = {}, userProfileCache = {}, allMapsDataCache = {};
47
- let isAuthReady = false, isFontReady = false;
48
- let minimapCtx, minimapDotCoords = [], minimapScale = 0.025;
49
- const MINIMAP_DOT_SIZE = 2;
50
- let intersected = {};
51
-
52
- const isHFStatic = /\.hf\.space$/.test(location.hostname);
53
- let authMode = null;
54
-
55
- const normalizeString = (str) => {
56
- if (!str) return "";
57
- return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
58
- };
59
-
60
- const loader = new FontLoader();
61
- loader.load('https://cdn.jsdelivr.net/npm/three@0.128.0/examples/fonts/helvetiker_regular.typeface.json', (loadedFont) => {
62
- font = loadedFont;
63
- isFontReady = true;
64
- checkAppReady();
65
- });
66
-
67
- initFirebase();
68
-
69
- const gkInput = document.getElementById('geminiKeyInput');
70
- const gkBtn = document.getElementById('saveGeminiKeyBtn');
71
- const gkStatus = document.getElementById('geminiKeyStatus');
72
- if (gkInput && gkBtn) {
73
- const existing = getLocalGeminiKey();
74
- if(existing && existing !== DEFAULT_GEMINI_KEY) gkInput.value = existing;
75
- gkBtn.addEventListener('click', (e) => {
76
- e.preventDefault();
77
- setLocalGeminiKey(gkInput.value.trim());
78
- gkStatus.textContent = 'GUARDADO';
79
- setTimeout(() => gkStatus.textContent = '', 2000);
80
- });
81
- }
82
-
83
- async function initFirebase() {
84
- try {
85
- const app = initializeApp(firebaseConfig);
86
- db = getFirestore(app);
87
- auth = getAuth(app);
88
- analytics = getAnalytics(app);
89
- setLogLevel('Silent');
90
-
91
- const els = {
92
- msg: document.getElementById('loginMessage'),
93
- overlay: document.getElementById('loginOverlay'),
94
- step1: document.getElementById('authStep1'),
95
- step2: document.getElementById('authStep2'),
96
- email: document.getElementById('loginEmail'),
97
- pass: document.getElementById('loginPassword'),
98
- loginBtn: document.getElementById('loginButton'),
99
- regBtn: document.getElementById('registerButton'),
100
- anonBtn: document.getElementById('btnGoToAnon'),
101
- saveUserBtn: document.getElementById('saveUsernameButton'),
102
- userInput: document.getElementById('usernameInput'),
103
- backBtn: document.getElementById('backToStep1'),
104
- logoutBtn: document.getElementById('mainLogoutButton'),
105
- ui: document.getElementById('ui')
106
- };
107
-
108
- els.regBtn.addEventListener('click', async () => {
109
- if(els.email.value.length < 6) {
110
- els.msg.innerText = "Email/Pass muy corto"; return;
111
- }
112
- els.msg.innerText = "Procesando registro...";
113
- authMode = 'email';
114
- try {
115
- await setPersistence(auth, browserLocalPersistence);
116
- await createUserWithEmailAndPassword(auth, els.email.value, els.pass.value);
117
- } catch(e) { els.msg.innerText = "Error: " + e.message; }
118
- });
119
-
120
- els.loginBtn.addEventListener('click', async () => {
121
- els.msg.innerText = "Autenticando...";
122
- authMode = 'email';
123
- try {
124
- await setPersistence(auth, browserLocalPersistence);
125
- await signInWithEmailAndPassword(auth, els.email.value, els.pass.value);
126
- } catch(e) { els.msg.innerText = "Error: " + e.message; }
127
- });
128
-
129
- els.anonBtn.addEventListener('click', () => {
130
- authMode = 'anonymous';
131
- els.step1.classList.add('hidden');
132
- els.step2.classList.remove('hidden');
133
- });
134
-
135
- els.backBtn.addEventListener('click', () => {
136
- els.step2.classList.add('hidden');
137
- els.step1.classList.remove('hidden');
138
- els.msg.innerText = "";
139
- });
140
-
141
- els.saveUserBtn.addEventListener('click', async () => {
142
- const name = normalizeString(els.userInput.value.trim());
143
- if(name.length < 3) {
144
- els.userInput.classList.add('border-red-500'); return;
145
- }
146
- els.userInput.classList.remove('border-red-500');
147
- els.saveUserBtn.innerText = "ESTABLECIENDO ENLACE...";
148
- els.saveUserBtn.disabled = true;
149
- try {
150
- if(authMode === 'anonymous') await signInAnonymously(auth);
151
- if(auth.currentUser) {
152
- await saveUserProfile(auth.currentUser.uid, name);
153
- els.overlay.style.opacity = '0';
154
- setTimeout(() => els.overlay.style.display = 'none', 700);
155
- initScene();
156
- loadAllMaps();
157
- els.ui.classList.remove('hidden');
158
- setTimeout(() => {
159
- els.ui.style.transform = 'translateX(0)';
160
- els.ui.style.opacity = '1';
161
- }, 100);
162
- }
163
- } catch(e) {
164
- els.saveUserBtn.innerText = "ERROR DE CONEXIÓN";
165
- els.saveUserBtn.disabled = false;
166
- console.error(e);
167
- }
168
- });
169
-
170
- els.logoutBtn.addEventListener('click', async () => { await signOut(auth); location.reload(); });
171
-
172
- onAuthStateChanged(auth, async (user) => {
173
- if(user) {
174
- userId = user.uid;
175
- isAuthReady = true;
176
- await fetchUserProfile(userId);
177
- if(userProfile) {
178
- els.overlay.style.opacity = '0';
179
- setTimeout(() => els.overlay.style.display = 'none', 700);
180
- els.ui.classList.remove('hidden');
181
- setTimeout(() => {
182
- els.ui.style.transform = 'translateX(0)';
183
- els.ui.style.opacity = '1';
184
- }, 100);
185
- document.getElementById('authStatus').textContent = userProfile.username;
186
- if(!scene) { initScene(); loadAllMaps(); }
187
- } else {
188
- els.step1.classList.add('hidden');
189
- els.step2.classList.remove('hidden');
190
- if(authMode === 'email') els.userInput.focus();
191
- }
192
- } else {
193
- els.overlay.style.display = 'flex';
194
- els.overlay.style.opacity = '1';
195
- els.step1.classList.remove('hidden');
196
- els.step2.classList.add('hidden');
197
- els.ui.classList.add('hidden');
198
- }
199
- });
200
-
201
- } catch (e) { console.error("Firebase Error", e); }
202
- }
203
-
204
- async function fetchUserProfile(uid) {
205
- if(!db) return;
206
- try {
207
- const snap = await getDoc(doc(db, 'artifacts', appId, 'users', uid, 'user_data', 'profile'));
208
- if(snap.exists()) { userProfile = snap.data(); userProfileCache[uid] = userProfile; }
209
- } catch {}
210
- }
211
- async function saveUserProfile(uid, name) {
212
- if(!db) return;
213
- await setDoc(doc(db, 'artifacts', appId, 'users', uid, 'user_data', 'profile'), { username: name });
214
- userProfile = { username: name };
215
- userProfileCache[uid] = userProfile;
216
- document.getElementById('authStatus').textContent = name;
217
- }
218
- async function getProfile(uid) {
219
- if(userProfileCache[uid]) return userProfileCache[uid];
220
- try {
221
- const snap = await getDoc(doc(db, 'artifacts', appId, 'users', uid, 'user_data', 'profile'));
222
- if(snap.exists()) { userProfileCache[uid] = snap.data(); return snap.data(); }
223
- } catch {}
224
- return null;
225
- }
226
-
227
- function checkAppReady() { if(isAuthReady && isFontReady && userProfile) { initScene(); loadAllMaps(); } }
228
-
229
- function initScene() {
230
- if(scene) return;
231
- scene = new THREE.Scene();
232
- scene.fog = new THREE.FogExp2(0x020205, 0.005);
233
-
234
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 2500);
235
- camera.position.set(0, 5, 30);
236
-
237
- renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
238
- renderer.setSize(window.innerWidth, window.innerHeight);
239
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
240
- renderer.toneMapping = THREE.ReinhardToneMapping;
241
- renderer.toneMappingExposure = 1.2;
242
- document.getElementById('container').appendChild(renderer.domElement);
243
-
244
- tooltip = document.getElementById('tooltip');
245
-
246
- controls = new OrbitControls(camera, renderer.domElement);
247
- controls.enableDamping = true;
248
- controls.dampingFactor = 0.04;
249
- controls.rotateSpeed = 0.5;
250
- controls.zoomSpeed = 0.7;
251
- controls.maxDistance = 600;
252
- controls.target.set(0,0,0);
253
-
254
- scene.add(new THREE.AmbientLight(0x404040, 1.0));
255
- const dirLight = new THREE.DirectionalLight(0xaaccff, 1.2);
256
- dirLight.position.set(50, 80, 50);
257
- scene.add(dirLight);
258
-
259
- const renderScene = new THREE.RenderPass(scene, camera);
260
-
261
- const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
262
- bloomPass.threshold = 0.15;
263
- bloomPass.strength = 1.4;
264
- bloomPass.radius = 0.6;
265
-
266
- composer = new THREE.EffectComposer(renderer);
267
- composer.addPass(renderScene);
268
- composer.addPass(bloomPass);
269
-
270
- raycaster = new THREE.Raycaster();
271
- mouse = new THREE.Vector2();
272
-
273
- hashtagGroup = new THREE.Group();
274
- scene.add(hashtagGroup);
275
-
276
- createAdvancedBackground();
277
- initAdvancedComet();
278
-
279
- const visBtn = document.getElementById('visualizeButton');
280
- if(!visBtn.dataset.bound) {
281
- visBtn.addEventListener('click', handleAnalysisAndVisualization);
282
- visBtn.dataset.bound = '1';
283
- }
284
-
285
- ['level1', 'level2', 'level3'].forEach(l => {
286
- document.getElementById(`${l}Slider`).addEventListener('input', e => document.getElementById(`${l}Value`).innerText = e.target.value);
287
- });
288
-
289
- window.addEventListener('resize', onWindowResize);
290
- window.addEventListener('mousemove', onPointerMove);
291
-
292
- const mmCanvas = document.getElementById('minimap');
293
- if(mmCanvas) {
294
- minimapCtx = mmCanvas.getContext('2d');
295
- mmCanvas.addEventListener('click', onMinimapClick);
296
- }
297
- document.getElementById('zoomInButton').addEventListener('click', () => { minimapScale *= 1.5; drawMinimap(); });
298
- document.getElementById('zoomOutButton').addEventListener('click', () => { minimapScale /= 1.5; drawMinimap(); });
299
-
300
- animate();
301
- }
302
-
303
- function createAdvancedBackground() {
304
- const pGeo = new THREE.BufferGeometry();
305
- const count = 5000;
306
- const pos = new Float32Array(count * 3);
307
- const sizes = new Float32Array(count);
308
-
309
- for(let i=0; i<count; i++) {
310
- pos[i*3] = (Math.random()-0.5) * 1500;
311
- pos[i*3+1] = (Math.random()-0.5) * 1500;
312
- pos[i*3+2] = (Math.random()-0.5) * 1500;
313
- sizes[i] = Math.random();
314
- }
315
- pGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
316
- pGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
317
-
318
- const pMat = new THREE.PointsMaterial({
319
- color: 0x88ccff,
320
- size: 1.0,
321
- transparent: true,
322
- opacity: 0.6,
323
- sizeAttenuation: true
324
- });
325
-
326
- bgParticles = new THREE.Points(pGeo, pMat);
327
- scene.add(bgParticles);
328
- }
329
-
330
- function initAdvancedComet() {
331
- if(cometGroup) { scene.remove(cometGroup); }
332
- if(cometParticlesMesh) { scene.remove(cometParticlesMesh); }
333
-
334
- if(!font || !userId) return;
335
-
336
- userCentroidForComet.copy(getCurrentUserCentroid());
337
-
338
- cometGroup = new THREE.Group();
339
-
340
- const coreGeo = new THREE.SphereGeometry(0.5, 32, 32);
341
- const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
342
- cometHead = new THREE.Mesh(coreGeo, coreMat);
343
-
344
- const haloGeo = new THREE.SphereGeometry(0.9, 32, 32);
345
- const haloMat = new THREE.MeshBasicMaterial({
346
- color: 0x00ffff,
347
- transparent: true,
348
- opacity: 0.25,
349
- blending: THREE.AdditiveBlending
350
- });
351
- const halo = new THREE.Mesh(haloGeo, haloMat);
352
- cometHead.add(halo);
353
- cometGroup.add(cometHead);
354
-
355
- cometLight = new THREE.PointLight(0x00ffff, 2.5, 60);
356
- cometGroup.add(cometLight);
357
-
358
- const word = COMET_WORDS[Math.floor(Math.random() * COMET_WORDS.length)];
359
- const tGeo = new TextGeometry(word, { font: font, size: 0.4, height: 0.02, bevelEnabled: false });
360
- tGeo.center();
361
- const tMat = new THREE.MeshBasicMaterial({ color: 0xccffff });
362
- cometText = new THREE.Mesh(tGeo, tMat);
363
- cometText.position.y = 1.3;
364
- cometGroup.add(cometText);
365
-
366
- scene.add(cometGroup);
367
-
368
- const pGeo = new THREE.BufferGeometry();
369
- const positions = new Float32Array(COMET_PARTICLE_COUNT * 3);
370
- const colors = new Float32Array(COMET_PARTICLE_COUNT * 3);
371
- const sizes = new Float32Array(COMET_PARTICLE_COUNT);
372
-
373
- pGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
374
- pGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
375
- pGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
376
-
377
- const pMat = new THREE.PointsMaterial({
378
- vertexColors: true,
379
- size: 1.0,
380
- transparent: true,
381
- opacity: 0.9,
382
- blending: THREE.AdditiveBlending,
383
- depthWrite: false,
384
- sizeAttenuation: true
385
- });
386
-
387
- cometParticlesMesh = new THREE.Points(pGeo, pMat);
388
- scene.add(cometParticlesMesh);
389
-
390
- cometParticlesData = [];
391
- for(let i=0; i<COMET_PARTICLE_COUNT; i++) {
392
- cometParticlesData.push({
393
- life: -1,
394
- velocity: new THREE.Vector3()
395
- });
396
- positions[i*3] = 99999;
397
- }
398
- }
399
-
400
- function updateAdvancedComet(delta, time) {
401
- if(!cometGroup || !cometParticlesMesh) return;
402
-
403
- cometAngle += delta * 0.35;
404
- const rX = 80; const rZ = 60;
405
-
406
- const x = userCentroidForComet.x + Math.cos(cometAngle) * rX;
407
- const z = userCentroidForComet.z + Math.sin(cometAngle) * rZ;
408
- const y = userCentroidForComet.y + Math.sin(cometAngle * 2.0) * 20;
409
-
410
- const targetPos = new THREE.Vector3(x, y, z);
411
-
412
- const nextPos = new THREE.Vector3(
413
- userCentroidForComet.x + Math.cos(cometAngle + 0.1) * rX,
414
- userCentroidForComet.y + Math.sin((cometAngle + 0.1) * 2.0) * 20,
415
- userCentroidForComet.z + Math.sin(cometAngle + 0.1) * rZ
416
- );
417
- cometGroup.position.copy(targetPos);
418
- cometGroup.lookAt(nextPos);
419
-
420
- if(cometText) cometText.quaternion.copy(camera.quaternion);
421
-
422
- const positions = cometParticlesMesh.geometry.attributes.position.array;
423
- const colors = cometParticlesMesh.geometry.attributes.color.array;
424
- const sizes = cometParticlesMesh.geometry.attributes.size.array;
425
-
426
- let spawnCount = 5;
427
- for(let i=0; i<COMET_PARTICLE_COUNT; i++) {
428
- if(spawnCount > 0 && cometParticlesData[i].life < 0) {
429
- cometParticlesData[i].life = 1.0;
430
-
431
- positions[i*3] = cometGroup.position.x + (Math.random()-0.5);
432
- positions[i*3+1] = cometGroup.position.y + (Math.random()-0.5);
433
- positions[i*3+2] = cometGroup.position.z + (Math.random()-0.5);
434
-
435
- colors[i*3] = 0.2; colors[i*3+1] = 1.0; colors[i*3+2] = 1.0;
436
- sizes[i] = 1.2;
437
- spawnCount--;
438
- }
439
- }
440
-
441
- for(let i=0; i<COMET_PARTICLE_COUNT; i++) {
442
- if(cometParticlesData[i].life > 0) {
443
- const d = cometParticlesData[i];
444
- d.life -= delta * 0.7;
445
-
446
- if(d.life > 0.6) {
447
- colors[i*3] = 0.2; colors[i*3+1] = 1.0; colors[i*3+2] = 1.0;
448
- } else if (d.life > 0.3) {
449
- colors[i*3] = 0.6; colors[i*3+1] = 0.2; colors[i*3+2] = 1.0;
450
- } else {
451
- colors[i*3] = 0.1; colors[i*3+1] = 0.1; colors[i*3+2] = 0.5;
452
- }
453
-
454
- sizes[i] = d.life * 1.8;
455
-
456
- } else {
457
- positions[i*3] = 99999;
458
- }
459
- }
460
-
461
- cometParticlesMesh.geometry.attributes.position.needsUpdate = true;
462
- cometParticlesMesh.geometry.attributes.color.needsUpdate = true;
463
- cometParticlesMesh.geometry.attributes.size.needsUpdate = true;
464
- }
465
-
466
- function animate() {
467
- requestAnimationFrame(animate);
468
-
469
- const delta = clock.getDelta();
470
- const time = clock.getElapsedTime();
471
-
472
- controls.update();
473
-
474
- if(bgParticles) {
475
- bgParticles.rotation.y = time * 0.015;
476
- bgParticles.rotation.z = time * 0.005;
477
- }
478
-
479
- updateAdvancedComet(delta, time);
480
- updateRaycaster();
481
- drawMinimap();
482
-
483
- hashtagGroup.children.forEach(obj => {
484
- if (obj.userData.isText) {
485
- obj.lookAt(camera.position);
486
- const dist = obj.position.distanceTo(camera.position);
487
- let scale = (1/dist) * 12;
488
- scale = Math.max(0.6, Math.min(5.0, scale));
489
- obj.scale.set(scale, scale, scale);
490
- }
491
- });
492
-
493
- composer.render();
494
- }
495
-
496
- function getCurrentUserCentroid() {
497
- if (!userId || !userMaps[userId] || userMaps[userId].length === 0) return new THREE.Vector3(0,0,0);
498
- const centroid = new THREE.Vector3(0,0,0);
499
- userMaps[userId].forEach(o => centroid.add(o));
500
- centroid.divideScalar(userMaps[userId].length);
501
- return centroid;
502
- }
503
-
504
- function visualizeHashtags(dataList, origin, level, parentColor = null) {
505
- if (!dataList || dataList.length === 0) return;
506
- for (const item of dataList) {
507
- let currentTag, variantsList;
508
- if (level === 1) {
509
- currentTag = normalizeString(item.palabra_principal);
510
- variantsList = item.variantes || [];
511
- } else if (level === 2) {
512
- currentTag = normalizeString(item.palabra_variante);
513
- variantsList = item.sub_variantes || [];
514
- } else {
515
- currentTag = normalizeString(item);
516
- variantsList = [];
517
- }
518
- if (!currentTag) continue;
519
-
520
- const { color, h } = stringToHslColor(currentTag);
521
- const nodeColor = (level === 1) ? color : parentColor;
522
-
523
- const nodeMaterial = new THREE.MeshPhysicalMaterial({
524
- color: new THREE.Color(nodeColor),
525
- emissive: new THREE.Color(nodeColor),
526
- emissiveIntensity: level === 1 ? 0.8 : 0.4,
527
- roughness: 0.2,
528
- metalness: 0.1,
529
- transmission: 0.1,
530
- transparent: true,
531
- opacity: 0.95
532
- });
533
-
534
- const theta = (h / 360) * Math.PI * 2;
535
- let phiHash = 0;
536
- for (let i = 0; i < currentTag.length; i++) phiHash = (phiHash + currentTag.charCodeAt(i) * 13) % 180;
537
- const phi = ((phiHash / 180) * 90 + 45) * (Math.PI / 180);
538
-
539
- const baseRadius = 12 / (level * level);
540
- const cx = baseRadius * Math.sin(phi) * Math.cos(theta);
541
- const cy = baseRadius * Math.cos(phi);
542
- const cz = baseRadius * Math.sin(phi) * Math.sin(theta);
543
- const clusterCenter = new THREE.Vector3(cx, cy, cz).add(origin);
544
-
545
- const lineMat = new THREE.LineBasicMaterial({
546
- color: new THREE.Color(nodeColor),
547
- transparent: true,
548
- opacity: 0.25
549
- });
550
- const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints([origin, clusterCenter]), lineMat);
551
- hashtagGroup.add(line);
552
-
553
- let sRad = (level === 1) ? 0.35 : (level === 2 ? 0.18 : 0.1);
554
- const sphere = new THREE.Mesh(new THREE.SphereGeometry(sRad, 16, 16), nodeMaterial);
555
- sphere.position.copy(clusterCenter);
556
- sphere.userData.hashtag = currentTag;
557
- sphere.userData.level = level;
558
- hashtagGroup.add(sphere);
559
-
560
- let tSize = (level === 1) ? 0.35 : (level === 2 ? 0.18 : 0.12);
561
- const tGeo = new THREE.TextGeometry(currentTag.toUpperCase(), { font: font, size: tSize, height: 0.01, bevelEnabled: false });
562
- tGeo.computeBoundingBox();
563
- const tMesh = new THREE.Mesh(tGeo, new THREE.MeshBasicMaterial({ color: nodeColor }));
564
- tMesh.position.copy(clusterCenter);
565
- tMesh.position.y += sRad + 0.15;
566
- tMesh.position.x -= (tGeo.boundingBox.max.x - tGeo.boundingBox.min.x)/2;
567
- tMesh.userData.isText = true;
568
- hashtagGroup.add(tMesh);
569
-
570
- visualizeHashtags(variantsList, clusterCenter, level + 1, nodeColor);
571
- }
572
- }
573
-
574
- function visualizeRoot(topic, origin) {
575
- const { color } = stringToHslColor(topic);
576
- const mat = new THREE.MeshStandardMaterial({
577
- color: new THREE.Color(color),
578
- emissive: new THREE.Color(color),
579
- emissiveIntensity: 2.2,
580
- roughness: 0.4
581
- });
582
- const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.7, 32, 32), mat);
583
- sphere.position.copy(origin);
584
- sphere.userData.hashtag = topic;
585
- sphere.userData.level = 0;
586
- hashtagGroup.add(sphere);
587
-
588
- const tGeo = new THREE.TextGeometry(topic.toUpperCase(), { font: font, size: 0.6, height: 0.05, bevelEnabled: false });
589
- tGeo.computeBoundingBox();
590
- const tMesh = new THREE.Mesh(tGeo, new THREE.MeshBasicMaterial({ color: 0xffffff }));
591
- tMesh.position.copy(origin);
592
- tMesh.position.y += 1.0;
593
- tMesh.position.x -= (tGeo.boundingBox.max.x - tGeo.boundingBox.min.x) / 2;
594
- tMesh.userData.isText = true;
595
- hashtagGroup.add(tMesh);
596
- }
597
-
598
- function createUserSun(pos, name, isMe) {
599
- const col = isMe ? 0xffaa00 : 0x00ff88;
600
- const mat = new THREE.MeshStandardMaterial({
601
- color: col, emissive: col, emissiveIntensity: 1.8, roughness: 0.2
602
- });
603
- const mesh = new THREE.Mesh(new THREE.SphereGeometry(3, 32, 32), mat);
604
- mesh.position.copy(pos);
605
- mesh.userData.hashtag = `Usuario: ${name}`;
606
- mesh.userData.isPlaceholder = true;
607
- hashtagGroup.add(mesh);
608
-
609
- const tGeo = new THREE.TextGeometry(name.toUpperCase(), { font: font, size: 1.2, height: 0.1 });
610
- tGeo.computeBoundingBox();
611
- const tMesh = new THREE.Mesh(tGeo, new THREE.MeshBasicMaterial({ color: 0xffffff }));
612
- tMesh.position.copy(pos);
613
- tMesh.position.y += 4;
614
- tMesh.position.x -= (tGeo.boundingBox.max.x - tGeo.boundingBox.min.x)/2;
615
- tMesh.userData.isText = true;
616
- hashtagGroup.add(tMesh);
617
- }
618
-
619
- async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
620
- const controller = new AbortController();
621
- const id = setTimeout(() => controller.abort(), timeoutMs);
622
- try {
623
- return await fetch(url, { ...options, signal: controller.signal });
624
- } finally { clearTimeout(id); }
625
- }
626
-
627
- async function fetchWithBackoff(url, options, retries = 2, delay = 1000) {
628
- try {
629
- return await fetchWithTimeout(url, options);
630
- } catch (err) {
631
- if (retries > 0) {
632
- await new Promise(r => setTimeout(r, delay));
633
- return fetchWithBackoff(url, options, retries - 1, delay * 2);
634
- } else { throw err; }
635
- }
636
- }
637
-
638
- async function callGemini(topic, mc, vc, svc) {
639
- const k = getLocalGeminiKey();
640
- if(!k || k.length<10) throw new Error("Falta API Key");
641
-
642
- const schema = {
643
- type: "OBJECT",
644
- properties: {
645
- analisis: { type: "STRING" },
646
- 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"}}}} } } }
647
- }
648
- };
649
- const prompt = `Tema: ${topic}. 1. Genera ${mc} palabras clave (Nivel 1). 2. Para cada una, ${vc} variantes (Nivel 2). 3. Para cada variante, ${svc} sub-variantes (Nivel 3). JSON Puro sin markdown.`;
650
-
651
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${k}`;
652
-
653
- if (!isHFStatic) {
654
- try {
655
- const proxyResp = await fetchWithBackoff('/.netlify/functions/gemini-proxy', {
656
- method: 'POST', headers: { 'Content-Type': 'application/json' },
657
- body: JSON.stringify({ model: 'gemini-2.5-flash-preview-09-2025', payload: { contents: [{parts:[{text: prompt}]}] } })
658
- }, 1, 1000);
659
- if (proxyResp.ok) return await proxyResp.json();
660
- } catch (e) {}
661
- }
662
-
663
- const resp = await fetchWithBackoff(url, {
664
- method: 'POST', headers: {'Content-Type': 'application/json'},
665
- body: JSON.stringify({ contents: [{parts:[{text: prompt}]}], generationConfig: { responseMimeType: "application/json", responseSchema: schema } })
666
- });
667
- if(!resp.ok) throw new Error(await resp.text());
668
- return await resp.json();
669
- }
670
-
671
- async function handleAnalysisAndVisualization() {
672
- if(!font) return;
673
- const topic = normalizeString(document.getElementById('topicInput').value);
674
- if(!topic) return;
675
-
676
- const btn = document.getElementById('visualizeButton');
677
- const pb = document.getElementById('progressBar');
678
- const pbc = document.getElementById('progressBarContainer');
679
- const originalBtnHtml = btn.innerHTML;
680
-
681
- btn.disabled = true; btn.innerHTML = '<span class="animate-pulse">PROCESANDO RED...</span>';
682
- pbc.classList.remove('hidden'); pb.style.width = "90%"; pb.style.transition = "width 15s ease-out";
683
-
684
- try {
685
- const mc = document.getElementById('level1Slider').value;
686
- const vc = document.getElementById('level2Slider').value;
687
- const svc = document.getElementById('level3Slider').value;
688
-
689
- let origin = new THREE.Vector3(0,0,0);
690
- const myMaps = userMaps[userId] || [];
691
- if(myMaps.length === 0) {
692
- const globalIdx = Object.keys(userMaps).length;
693
- origin.set(globalIdx*800*Math.cos(globalIdx), 0, globalIdx*800*Math.sin(globalIdx));
694
- } else {
695
- let center = new THREE.Vector3(); myMaps.forEach(m=>center.add(m)); center.divideScalar(myMaps.length);
696
- const r = Math.sqrt(myMaps.length)*40; const a = myMaps.length;
697
- origin.set(center.x + r*Math.cos(a), 0, center.z + r*Math.sin(a));
698
- }
699
-
700
- const res = await callGemini(topic, mc, vc, svc);
701
- const txt = res.candidates?.[0]?.content?.parts?.[0]?.text;
702
- if(!txt) throw new Error("La IA no generó datos válidos.");
703
- const json = JSON.parse(txt);
704
-
705
- visualizeRoot(topic, origin);
706
- visualizeHashtags(json.lista_palabras, origin, 1);
707
- if(db && userId) await addDoc(collection(db,'artifacts',appId,'public','data','maps'), {
708
- topic, depth: "3", origin: {x:origin.x, y:origin.y, z:origin.z}, data: JSON.stringify(json), createdAt: new Date(), userId
709
- });
710
-
711
- } catch(e) {
712
- console.error(e);
713
- alert("Error en análisis: " + e.message);
714
- } finally {
715
- btn.disabled = false; btn.innerHTML = originalBtnHtml;
716
- pb.style.width = "100%"; setTimeout(()=>{pbc.classList.add('hidden'); pb.style.width="0%";}, 500);
717
- }
718
- }
719
-
720
- function loadAllMaps() {
721
- if(!db || !font) { setTimeout(loadAllMaps,500); return; }
722
- const q = query(collection(db,'artifacts',appId,'public','data','maps'));
723
-
724
- onSnapshot(q, async (snap) => {
725
- while(hashtagGroup.children.length > 0){
726
- let obj = hashtagGroup.children[0];
727
- hashtagGroup.remove(obj);
728
- if(obj.geometry) obj.geometry.dispose();
729
- if(obj.material) {
730
- if(Array.isArray(obj.material)) obj.material.forEach(m=>m.dispose());
731
- else obj.material.dispose();
732
- }
733
- }
734
-
735
- userMaps = {}; allMapsDataCache = {};
736
-
737
- snap.docs.forEach(d => {
738
- const m = d.data();
739
- if(!m.origin) return;
740
- const o = new THREE.Vector3(m.origin.x, m.origin.y, m.origin.z);
741
- if(!userMaps[m.userId]) userMaps[m.userId] = [];
742
- userMaps[m.userId].push(o);
743
-
744
- try {
745
- const data = JSON.parse(m.data);
746
- visualizeRoot(m.topic, o);
747
- visualizeHashtags(data.lista_palabras, o, 1);
748
- } catch {}
749
- });
750
-
751
- const uids = Object.keys(userMaps);
752
- const list = document.getElementById('userList');
753
- if(list) list.innerHTML = '';
754
-
755
- for(const uid of uids) {
756
- const prof = await getProfile(uid);
757
- const name = prof ? prof.username : "ANON " + uid.substring(0,4);
758
-
759
- if(list) {
760
- const item = document.createElement('div');
761
- item.className = 'text-cyan-400 hover:text-white cursor-pointer hover:bg-white/5 p-1 rounded transition-colors text-[10px] tracking-wide';
762
- item.innerHTML = `> <span class="font-bold">${name}</span>`;
763
- item.onclick = () => teleportToUser(uid);
764
- list.appendChild(item);
765
- }
766
-
767
- const origins = userMaps[uid];
768
- if(origins.length > 0) {
769
- const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
770
- createUserSun(c, name, uid === userId);
771
- }
772
- }
773
-
774
- initAdvancedComet();
775
- drawMinimap();
776
- focusOnUserMaps();
777
- });
778
- }
779
-
780
- function focusOnUserMaps() {
781
- if(!controls || !userId || !userMaps[userId]) return;
782
- const c = getCurrentUserCentroid();
783
- controls.target.copy(c);
784
- camera.position.copy(c).add(new THREE.Vector3(0,10,40));
785
- }
786
-
787
- function teleportToUser(uid) {
788
- if(!userMaps[uid]) return;
789
- const origins = userMaps[uid];
790
- const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
791
- controls.target.copy(c);
792
- camera.position.copy(c).add(new THREE.Vector3(0,10,40));
793
- }
794
-
795
- function drawMinimap() {
796
- if(!minimapCtx) return;
797
- const w = minimapCtx.canvas.width; const h = minimapCtx.canvas.height;
798
- minimapCtx.clearRect(0,0,w,h);
799
- minimapDotCoords = [];
800
-
801
- const myC = getCurrentUserCentroid();
802
-
803
- Object.keys(userMaps).forEach(uid => {
804
- const origins = userMaps[uid];
805
- const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
806
-
807
- const x = w/2 + (c.x - myC.x)*minimapScale;
808
- const y = h/2 + (c.z - myC.z)*minimapScale;
809
-
810
- const isMe = (uid === userId);
811
- const mainColor = isMe ? '#22d3ee' : '#64748b';
812
-
813
- minimapDotCoords.push({x, y, uid});
814
-
815
- if(origins.length > 0) {
816
- const satColor = isMe ? 'rgba(34, 211, 238, 0.4)' : 'rgba(100, 116, 139, 0.4)';
817
- origins.forEach(o => {
818
- const sx = w/2 + (o.x - myC.x)*minimapScale;
819
- const sy = h/2 + (o.z - myC.z)*minimapScale;
820
- minimapCtx.beginPath();
821
- minimapCtx.arc(sx, sy, 1, 0, Math.PI*2);
822
- minimapCtx.fillStyle = satColor;
823
- minimapCtx.fill();
824
- });
825
- }
826
-
827
- minimapCtx.fillStyle = mainColor;
828
- minimapCtx.beginPath();
829
- minimapCtx.arc(x,y, isMe?4:2.5, 0, Math.PI*2);
830
- minimapCtx.fill();
831
-
832
- if(isMe) {
833
- minimapCtx.strokeStyle = 'rgba(34, 211, 238, 0.3)';
834
- minimapCtx.beginPath(); minimapCtx.arc(x,y, 8, 0, Math.PI*2); minimapCtx.stroke();
835
- }
836
- });
837
-
838
- if (cometGroup) {
839
- const cometX = w/2 + (cometGroup.position.x - myC.x) * minimapScale;
840
- const cometY = h/2 + (cometGroup.position.z - myC.z) * minimapScale;
841
-
842
- minimapCtx.beginPath();
843
- minimapCtx.arc(cometX, cometY, 3, 0, Math.PI * 2);
844
- minimapCtx.fillStyle = 'rgba(0, 255, 255, 0.4)';
845
- minimapCtx.fill();
846
-
847
- minimapCtx.beginPath();
848
- minimapCtx.arc(cometX, cometY, 1.5, 0, Math.PI * 2);
849
- minimapCtx.fillStyle = '#ffffff';
850
- minimapCtx.fill();
851
- }
852
- }
853
-
854
- function onMinimapClick(e) {
855
- if(!minimapCtx) return;
856
- const rect = minimapCtx.canvas.getBoundingClientRect();
857
- const x = e.clientX - rect.left;
858
- const y = e.clientY - rect.top;
859
-
860
- let closest = null;
861
- let minD = 20;
862
-
863
- minimapDotCoords.forEach(dot => {
864
- const d = Math.sqrt((x-dot.x)**2 + (y-dot.y)**2);
865
- if(d < minD) { minD = d; closest = dot.uid; }
866
- });
867
-
868
- if(closest) teleportToUser(closest);
869
- }
870
-
871
- function onWindowResize() {
872
- camera.aspect = window.innerWidth/window.innerHeight;
873
- camera.updateProjectionMatrix();
874
- renderer.setSize(window.innerWidth, window.innerHeight);
875
- composer.setSize(window.innerWidth, window.innerHeight);
876
- }
877
-
878
- function onPointerMove(e) {
879
- mouse.x = (e.clientX/window.innerWidth)*2-1;
880
- mouse.y = -(e.clientY/window.innerHeight)*2+1;
881
- tooltip.style.left = e.clientX+20+'px';
882
- tooltip.style.top = e.clientY+'px';
883
- }
884
-
885
- function updateRaycaster() {
886
- raycaster.setFromCamera(mouse, camera);
887
- let targets = [...hashtagGroup.children];
888
- if(cometHead) targets.push(cometHead);
889
-
890
- const intersects = raycaster.intersectObjects(targets, false);
891
-
892
- if(intersects.length > 0) {
893
- let o = intersects[0].object;
894
- if(o.parent === cometHead || o.parent === cometGroup) o = cometHead;
895
-
896
- if(o === cometHead) {
897
- tooltip.classList.remove('hidden');
898
- const word = cometText ? cometText.geometry.parameters.text : "ENLACE";
899
- tooltip.innerHTML = `
900
- <div class="border-b border-cyan-500/50 pb-1 mb-1 text-cyan-300 font-bold tracking-widest text-xs">COMETA NEURAL</div>
901
- <div class="text-white text-[10px]">SEÑAL: ${word}</div>
902
- `;
903
- document.body.style.cursor = 'pointer';
904
- return;
905
- }
906
-
907
- const d = o.userData;
908
- if(d.hashtag) {
909
- tooltip.classList.remove('hidden');
910
- let typeColor = d.isPlaceholder ? "text-yellow-400" : "text-cyan-300";
911
- let typeText = d.isPlaceholder ? "NODO USUARIO" : `NIVEL ${d.level}`;
912
-
913
- tooltip.innerHTML = `
914
- <div class="${typeColor} font-bold tracking-widest text-sm mb-1">${d.hashtag}</div>
915
- <div class="text-gray-400 text-[10px] uppercase">${typeText}</div>
916
- `;
917
- document.body.style.cursor = 'pointer';
918
- }
919
- } else {
920
- tooltip.classList.add('hidden');
921
- document.body.style.cursor = 'default';
922
- }
923
- }
924
-
925
- function stringToHslColor(str) {
926
- let hash = 0; for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash);
927
- return { color: `hsl(${Math.abs(hash % 360)}, 75%, 60%)`, h: Math.abs(hash % 360) };
928
- }