Spaces:
Running
Running
Upload 10 files
Browse files- src/db.js +1 -1
- src/scene.js +53 -1
- 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 {
|
| 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(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|