|
1338 | 1338 | async function syncNow(){const b=document.getElementById('sync-btn');const s=document.getElementById('sync-status');if(b){b.disabled=true;b.textContent='Syncing…'}if(s)s.textContent='Contacting the cloud…';try{const d=await api('/sync/run',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});const su=d.summary||{};toast('Synced — pushed '+(su.exported||0)+', '+(su.added||0)+' new from other devices','ok');await loadSyncStatus()}catch(e){toast('Sync failed: '+e.message,'err');if(b){b.disabled=false;b.textContent='Sync now'}if(s)s.textContent='Sync failed — try again.'}} |
1339 | 1339 |
|
1340 | 1340 | /* ─── knowledge graph (force-graph + d3-force: compact defaults and selectable layouts) ─── */ |
1341 | | -let GRAPH=null, FG=null, GRESIZE=false, GRESIZEFRAME=0, GADJ={}, GCOMPONENTS={}, GHILITE=null, GHOVERSET=null, GLABELRANK={}, GLABELBOXES=[], GDATA_CACHE=null, GACTIVE_DATA=null, GREDRAWFRAME=0, GPERF={large:false,dense:false}; |
| 1341 | +let GRAPH=null, FG=null, GRESIZE=false, GRESIZEFRAME=0, GADJ={}, GCOMPONENTS={}, GCOMPONENT_LAYOUT=null, GHILITE=null, GHOVERSET=null, GLABELRANK={}, GLABELBOXES=[], GDATA_CACHE=null, GACTIVE_DATA=null, GREDRAWFRAME=0, GPERF={large:false,dense:false}; |
1342 | 1342 | const GRAPH_PRESETS={ |
1343 | 1343 | original:{label:'Original force',repel:120,link:30,gravity:14,font:13,size:5,linkw:1,labelDensity:40,curve:0,particles:0}, |
1344 | 1344 | compact:{label:'Compact clusters',repel:42,link:20,gravity:26,font:12,size:5,linkw:.7,labelDensity:30,curve:.08,particles:0}, |
|
1414 | 1414 | if(count&&data)count.textContent=data.nodes.length.toLocaleString()+' entities · '+data.links.length.toLocaleString()+' relations'; |
1415 | 1415 | if(badge)badge.textContent=GPERF.large?'Large graph mode':'Adaptive rendering'; |
1416 | 1416 | } |
1417 | | -function graphInvalidateData(){GDATA_CACHE=null;GACTIVE_DATA=null;GHILITE=null;GHOVERSET=null} |
| 1417 | +function graphInvalidateData(){GDATA_CACHE=null;GACTIVE_DATA=null;GCOMPONENT_LAYOUT=null;GHILITE=null;GHOVERSET=null} |
1418 | 1418 | async function loadGraph(){ |
1419 | 1419 | graphInjectCss();graphInvalidateData();GRAPH=null; |
1420 | 1420 | const empty=document.getElementById('graph-empty'),net=document.getElementById('graph-net'),nodesBox=document.getElementById('graph-entity-list'),edgesBox=document.getElementById('graph-relation-list'); |
|
1428 | 1428 | GRESIZEFRAME=requestAnimationFrame(()=>{GRESIZEFRAME=0;const element=document.getElementById('graph-net');if(FG&&element)FG.width(element.clientWidth).height(element.clientHeight)}); |
1429 | 1429 | }); |
1430 | 1430 | } |
1431 | | - const layers=Array.from(document.querySelectorAll('#graph-layer-filters input:checked')).map(input=>input.value).join(','),includeCode=document.getElementById('graph-include-code').checked,repo=(document.getElementById('graph-repo-filter').value||'').trim(); |
| 1431 | + const layerInputs=Array.from(document.querySelectorAll('#graph-layer-filters input')),selectedLayers=layerInputs.filter(input=>input.checked).map(input=>input.value),layerFilter=selectedLayers.length===layerInputs.length?'':'&layers='+encodeURIComponent(selectedLayers.join(',')),includeCode=document.getElementById('graph-include-code').checked,repo=(document.getElementById('graph-repo-filter').value||'').trim(); |
1432 | 1432 | try{ |
1433 | | - GRAPH=await api('/graph?workspace='+encodeURIComponent(WS||'')+'&layers='+encodeURIComponent(layers)+'&include_code='+(includeCode?'true':'false')+(repo?'&repo='+encodeURIComponent(repo):'')); |
| 1433 | + GRAPH=await api('/graph?workspace='+encodeURIComponent(WS||'')+layerFilter+'&include_code='+(includeCode?'true':'false')+(repo?'&repo='+encodeURIComponent(repo):'')); |
1434 | 1434 | renderGraphSide();graphRender(); |
1435 | 1435 | }catch(error){ |
1436 | 1436 | empty.style.display='flex';empty.textContent='Graph failed: '+error.message;graphSetLayoutStatus('Load failed',false); |
|
1472 | 1472 | ids.forEach(id=>{GCOMPONENTS[id]={index,size:ids.length,x,y}}); |
1473 | 1473 | }); |
1474 | 1474 | } |
| 1475 | +function graphRefreshComponentCenters(nodes,force=false){ |
| 1476 | + const layout=window.GSET.mode+'|'+window.GSET.link; |
| 1477 | + if(!force&&GCOMPONENT_LAYOUT===layout)return; |
| 1478 | + graphIndexComponents(nodes);GCOMPONENT_LAYOUT=layout; |
| 1479 | +} |
1475 | 1480 | function graphAlpha(color,alpha){ |
1476 | 1481 | const hex=/^#([0-9a-f]{6})$/i.exec(color||''); |
1477 | 1482 | if(hex){const value=parseInt(hex[1],16);return `rgba(${value>>16},${(value>>8)&255},${value&255},${alpha})`} |
|
1523 | 1528 | GPERF={large:data.nodes.length>600||data.links.length>2400,dense:data.links.length>1500}; |
1524 | 1529 | if(dataChanged){ |
1525 | 1530 | buildAdj(data.links);GLABELRANK={};data.nodes.forEach(node=>{GLABELRANK[node.id]=node.rank});GACTIVE_DATA=data; |
1526 | | - graphIndexComponents(data.nodes); |
1527 | 1531 | } |
1528 | | - graphSetHighlight(null); |
| 1532 | + graphRefreshComponentCenters(data.nodes,dataChanged); |
| 1533 | + if(dataChanged)graphSetHighlight(null); |
1529 | 1534 | if(!data.nodes.length){ |
1530 | 1535 | empty.style.display='flex'; |
1531 | 1536 | empty.textContent=GRAPH.nodes.length?('No connected entities — untick "Hide unconnected" to see all '+GRAPH.nodes.length+'.'):'No entities in this workspace yet.'; |
|
1579 | 1584 | }else{FG.linkCanvasObjectMode(()=>undefined)} |
1580 | 1585 | FG.onRenderFramePost((ctx,scale)=>{ |
1581 | 1586 | if(!(settings.font>0))return; |
1582 | | - const nodes=GACTIVE_DATA.nodes,focus=GHOVERSET&&GHOVERSET.size>1,cap=Math.max(1,Math.round((settings.labelDensity||40)*Math.max(.32,scale-.85))),fontSize=settings.font/scale,boxes=GLABELBOXES,candidateLimit=Math.min(nodes.length,Math.max(72,cap*10)); |
| 1587 | + const nodes=(GACTIVE_DATA&&GACTIVE_DATA.nodes)||[]; |
| 1588 | + if(!nodes.length)return; |
| 1589 | + const focus=GHOVERSET&&GHOVERSET.size>1,cap=Math.max(1,Math.round((settings.labelDensity||40)*Math.max(.32,scale-.85))),fontSize=settings.font/scale,boxes=GLABELBOXES,candidateLimit=Math.min(nodes.length,Math.max(72,cap*10)); |
1583 | 1590 | boxes.length=0; |
1584 | 1591 | ctx.textAlign='center';ctx.textBaseline='top';ctx.lineJoin='round';ctx.font=fontSize+'px -apple-system,Segoe UI,sans-serif';ctx.lineWidth=3/scale;ctx.strokeStyle=window.GCOL.panel;ctx.fillStyle=window.GCOL.label; |
1585 | 1592 | let drawn=0; |
|
1615 | 1622 | if(!FG)return; |
1616 | 1623 | const layout=key==='repel'||key==='link'||key==='gravity'||key==='size'; |
1617 | 1624 | if(key==='size')graphRefreshNodeMetrics(); |
| 1625 | + if(key==='link'&&GACTIVE_DATA)graphRefreshComponentCenters(GACTIVE_DATA.nodes); |
1618 | 1626 | if(layout)graphApplyForces(); |
1619 | 1627 | if(key==='linkw'){FG.linkWidth(FG.linkWidth());FG.linkColor(FG.linkColor())}else graphRedraw(); |
1620 | 1628 | if(layout&&!prefersReducedMotion()){graphSetLayoutStatus('Updating layout',true);FG.d3ReheatSimulation()} |
|
0 commit comments