salomonsky commited on
Commit
fd2bdff
·
verified ·
1 Parent(s): 37ab4cc

Upload 10 files

Browse files
Files changed (3) hide show
  1. src/db.js +1 -1
  2. src/scene.js +53 -1
  3. src/ui.js +23 -2
src/db.js CHANGED
@@ -10,7 +10,7 @@ export async function saveNeuron(n){
10
  export function subscribeNeurons(appId,cb){
11
  const db=getDB();
12
  if(!db){
13
- setTimeout(()=>{ try{ cb({ forEach:()=>{} }); }catch{} },0);
14
  return ()=>{};
15
  }
16
  const ref=query(collection(db,'artifacts',appId,'public','data','neurons'));
 
10
  export function subscribeNeurons(appId,cb){
11
  const db=getDB();
12
  if(!db){
13
+ setTimeout(()=>{ try{ cb({ empty:true, forEach:()=>{} }); }catch{} },0);
14
  return ()=>{};
15
  }
16
  const ref=query(collection(db,'artifacts',appId,'public','data','neurons'));
src/scene.js CHANGED
@@ -1,4 +1,4 @@
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='';
@@ -29,5 +29,57 @@ export function usernameSphere(uid,uname){const center=userCenter(uid);const mat
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}}));}
 
1
+ import {hslFromString, randomWord, normalizeString} 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='';
 
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
+ // Option B: hierarchical tree around a topic root like the original implementation
33
+ export function visualizeTree(topic, lista_palabras, origin){
34
+ const acc=[];
35
+ const rootCol=hslFromString(topic).color;
36
+ // Root sphere and label
37
+ const rootMat=new THREE.MeshStandardMaterial({color:new THREE.Color(rootCol),roughness:.5,metalness:.1});
38
+ const rootGeo=new THREE.SphereGeometry(0.4,16,16);
39
+ const root=new THREE.Mesh(rootGeo,rootMat); root.position.copy(origin); root.userData={label:topic,level:0}; rootGroup.add(root);
40
+ if(font){ const tg=new TextGeometry(topic.toUpperCase(),{font,size:.3,height:.02,curveSegments:4}); tg.computeBoundingBox(); const tm=new THREE.MeshBasicMaterial({color:new THREE.Color(rootCol),transparent:true,opacity:.8}); const text=new THREE.Mesh(tg,tm); text.position.copy(origin); text.position.y+=0.5; text.position.x-=(tg.boundingBox.max.x-tg.boundingBox.min.x)/2; text.userData={label:topic,level:0,isText:true}; rootGroup.add(text);}
41
+ acc.push({label:topic,level:0,position:origin.clone()});
42
+
43
+ function addBranchNode(currentTag, level, parentPos, parentColor){
44
+ const {color,h}=hslFromString(currentTag);
45
+ const nodeColor=(level===1)?color:parentColor;
46
+ const theta=(h/360)*Math.PI*2;
47
+ let phiHash=0; for(let i=0;i<currentTag.length;i++){ phiHash=(phiHash+currentTag.charCodeAt(i)*13)%180; }
48
+ const phi=((phiHash/180)*90+45)*(Math.PI/180);
49
+ const baseRadius=10/(level*level);
50
+ const cx=baseRadius*Math.sin(phi)*Math.cos(theta);
51
+ const cy=baseRadius*Math.cos(phi);
52
+ const cz=baseRadius*Math.sin(phi)*Math.sin(theta);
53
+ const pos=new THREE.Vector3(cx,cy,cz).add(parentPos);
54
+ const branchColor=new THREE.Color(nodeColor).multiplyScalar(0.4);
55
+ const lineMat=new THREE.LineBasicMaterial({color:branchColor});
56
+ const lineGeom=new THREE.BufferGeometry().setFromPoints([parentPos,pos]);
57
+ const line=new THREE.Line(lineGeom,lineMat); line.userData={ignoreHit:true}; rootGroup.add(line);
58
+ const r= level===1?0.2: level===2?0.1: 0.05;
59
+ const sphGeo=new THREE.SphereGeometry(r,12,12);
60
+ const sphMat=new THREE.MeshStandardMaterial({color:new THREE.Color(nodeColor),roughness:.5,metalness:.1});
61
+ const sph=new THREE.Mesh(sphGeo,sphMat); sph.position.copy(pos); sph.userData={label:currentTag,level}; rootGroup.add(sph);
62
+ if(font){ const tGeo=new TextGeometry(currentTag.toUpperCase(),{font,size: level===1?0.24: level===2?0.12: 0.08,height:0.02/level,curveSegments:4}); tGeo.computeBoundingBox(); const tMat=new THREE.MeshBasicMaterial({color:new THREE.Color(nodeColor),transparent:true,opacity:.8}); const tMesh=new THREE.Mesh(tGeo,tMat); tMesh.position.copy(pos); tMesh.position.y+=r+ (0.05/level); tMesh.position.x-=(tGeo.boundingBox.max.x-tGeo.boundingBox.min.x)/2; tMesh.userData={label:currentTag,level,isText:true}; rootGroup.add(tMesh);}
63
+ return pos;
64
+ }
65
+
66
+ function walk(list, parentPos, level, parentColor){
67
+ if(!list||!list.length) return;
68
+ for(const item of list){
69
+ let tag, subs;
70
+ if(level===1){ tag=normalizeString(item.palabra_principal); subs=item.variantes||[]; }
71
+ else if(level===2){ tag=normalizeString(item.palabra_variante); subs=item.sub_variantes||[]; }
72
+ else { tag=normalizeString(item); subs=[]; }
73
+ if(!tag) continue;
74
+ const pos=addBranchNode(tag, level, parentPos, parentColor||hslFromString(tag).color);
75
+ acc.push({label:tag,level,position:pos.clone()});
76
+ walk(subs, pos, level+1, hslFromString(tag).color);
77
+ }
78
+ }
79
+
80
+ walk(lista_palabras, origin, 1, null);
81
+ return acc; // list of nodes to be saved
82
+ }
83
+
84
  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});}}
85
  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 CHANGED
@@ -2,9 +2,30 @@ 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)}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import {normalizeString, hslFromString} from './utils.js';
3
  import {callGemini} from './gemini.js';
4
  import {saveNeuron} from './db.js';
5
+ import {visualizeTree, 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(){
11
+ const {userId,username}=getAuthState();
12
+ const topic=normalizeString(document.getElementById('topicInput').value.trim()); if(!topic) return;
13
+ const n1=parseInt(document.getElementById('level1Slider').value,10);
14
+ const n2=parseInt(document.getElementById('level2Slider').value,10);
15
+ const n3=parseInt(document.getElementById('level3Slider').value,10);
16
+ const btn=document.getElementById('seedBtn');
17
+ const pbc=document.getElementById('progressBarContainer');
18
+ const pb=document.getElementById('progressBar');
19
+ btn.disabled=true; const bak=btn.innerHTML; btn.innerHTML='Analizando...';
20
+ 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%';
21
+ try{
22
+ const data=await callGemini(topic,n1,n2,n3);
23
+ const cand=data.candidates?.[0]; const text=cand?.content?.parts?.[0]?.text||'{}';
24
+ const parsed=JSON.parse(text); const lista=parsed.lista_palabras||[];
25
+ const origin=userCenter(userId);
26
+ const nodes=visualizeTree(topic, lista, origin);
27
+ for(const node of nodes){ await saveNeuron({appId,userId,username,label:node.label,level:node.level,position:node.position,topic}); }
28
+ document.getElementById('topicInput').value='';
29
+ }catch(e){}
30
+ 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); }
31
+ }