claude-mpm 4.1.10__py3-none-any.whl → 4.1.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +8 -0
- claude_mpm/cli/__init__.py +11 -0
- claude_mpm/cli/commands/analyze.py +2 -1
- claude_mpm/cli/commands/configure.py +9 -8
- claude_mpm/cli/commands/configure_tui.py +3 -1
- claude_mpm/cli/commands/dashboard.py +288 -0
- claude_mpm/cli/commands/debug.py +0 -1
- claude_mpm/cli/commands/mpm_init.py +442 -0
- claude_mpm/cli/commands/mpm_init_handler.py +84 -0
- claude_mpm/cli/parsers/base_parser.py +15 -0
- claude_mpm/cli/parsers/dashboard_parser.py +113 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +128 -0
- claude_mpm/constants.py +10 -0
- claude_mpm/core/config.py +18 -0
- claude_mpm/core/instruction_reinforcement_hook.py +266 -0
- claude_mpm/core/pm_hook_interceptor.py +105 -8
- claude_mpm/dashboard/analysis_runner.py +52 -25
- claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
- claude_mpm/dashboard/static/built/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/code-tree.css +330 -1
- claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/code-tree.js +2593 -2
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +212 -13
- claude_mpm/dashboard/static/js/components/build-tracker.js +15 -13
- claude_mpm/dashboard/static/js/components/code-tree.js +2503 -917
- claude_mpm/dashboard/static/js/components/event-viewer.js +58 -19
- claude_mpm/dashboard/static/js/dashboard.js +46 -44
- claude_mpm/dashboard/static/js/socket-client.js +74 -32
- claude_mpm/dashboard/templates/index.html +25 -20
- claude_mpm/services/agents/deployment/agent_template_builder.py +11 -7
- claude_mpm/services/agents/memory/memory_format_service.py +3 -1
- claude_mpm/services/cli/agent_cleanup_service.py +1 -4
- claude_mpm/services/cli/socketio_manager.py +39 -8
- claude_mpm/services/cli/startup_checker.py +0 -1
- claude_mpm/services/core/cache_manager.py +0 -1
- claude_mpm/services/infrastructure/monitoring.py +1 -1
- claude_mpm/services/socketio/event_normalizer.py +64 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +449 -0
- claude_mpm/services/socketio/server/connection_manager.py +3 -1
- claude_mpm/tools/code_tree_analyzer.py +930 -24
- claude_mpm/tools/code_tree_builder.py +0 -1
- claude_mpm/tools/code_tree_events.py +113 -15
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/METADATA +2 -1
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/RECORD +56 -48
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/top_level.txt +0 -0
|
@@ -1,2 +1,2593 @@
|
|
|
1
|
-
class t{constructor(){this.container=null,this.svg=null,this.treeData=null,this.root=null,this.treeLayout=null,this.treeGroup=null,this.nodes=new Map,this.stats={files:0,classes:0,functions:0,methods:0,lines:0},this.margin={top:20,right:150,bottom:20,left:150},this.width=960-this.margin.left-this.margin.right,this.height=600-this.margin.top-this.margin.bottom,this.nodeId=0,this.duration=750,this.languageFilter="all",this.searchTerm="",this.tooltip=null,this.initialized=!1,this.analyzing=!1,this.selectedNode=null,this.socket=null}initialize(){if(console.log("CodeTree.initialize() called"),this.initialized)return void console.log("Code tree already initialized");if(this.container=document.getElementById("code-tree-container"),!this.container)return void console.error("Code tree container not found");console.log("Code tree container found:",this.container);const t=document.getElementById("code-tab");t?(console.log("Code tab panel found, active:",t.classList.contains("active")),this.setupControls(),this.initializeTreeData(),this.subscribeToEvents(),t.classList.contains("active")?(console.log("Tab is active, creating visualization"),this.createVisualization(),this.root&&this.svg&&this.update(this.root)):console.log("Tab is not active, deferring visualization"),this.initialized=!0,console.log("Code tree initialization complete")):console.error("Code tab panel not found")}renderWhenVisible(){if(console.log("CodeTree.renderWhenVisible() called"),console.log("Current state - initialized:",this.initialized,"svg:",!!this.svg),!this.initialized)return console.log("Not initialized, calling initialize()"),void this.initialize();this.svg?(console.log("SVG exists, forcing update"),this.root&&this.svg&&this.update(this.root)):(console.log("No SVG found, creating visualization"),this.createVisualization(),this.svg&&this.treeGroup?(console.log("SVG created, updating tree"),this.update(this.root)):console.log("Failed to create SVG or treeGroup"))}setupControls(){const t=document.getElementById("analyze-code");t&&t.addEventListener("click",()=>this.startAnalysis());const e=document.getElementById("cancel-analysis");e&&e.addEventListener("click",()=>this.cancelAnalysis());const s=document.getElementById("code-expand-all");s&&s.addEventListener("click",()=>this.expandAll());const i=document.getElementById("code-collapse-all");i&&i.addEventListener("click",()=>this.collapseAll());const o=document.getElementById("code-reset-zoom");o&&o.addEventListener("click",()=>this.resetZoom());const n=document.getElementById("code-toggle-legend");n&&n.addEventListener("click",()=>this.toggleLegend());const a=document.getElementById("language-filter");a&&a.addEventListener("change",t=>{this.languageFilter=t.target.value,this.filterByLanguage()});const l=document.getElementById("code-search");l&&l.addEventListener("input",t=>{this.searchTerm=t.target.value.toLowerCase(),this.highlightSearchResults()})}createVisualization(){if(console.log("Creating code tree visualization"),"undefined"==typeof d3)return void console.error("D3.js is not loaded! Cannot create code tree visualization.");console.log("D3 is available:",typeof d3);const t=document.getElementById("code-tree");if(!t)return void console.error("Code tree div not found");console.log("Code tree div found:",t),!this.root&&this.treeData&&(console.log("Creating D3 hierarchy from tree data"),this.root=d3.hierarchy(this.treeData),this.root.x0=this.height/2,this.root.y0=0),d3.select(t).selectAll("*").remove();const e=t.getBoundingClientRect();this.width=e.width-this.margin.left-this.margin.right,this.height=Math.max(500,e.height)-this.margin.top-this.margin.bottom,this.svg=d3.select(t).append("svg").attr("width","100%").attr("height","100%").attr("viewBox",`0 0 ${e.width} ${e.height}`).call(d3.zoom().scaleExtent([.1,3]).on("zoom",t=>{this.treeGroup.attr("transform",t.transform)})),this.treeGroup=this.svg.append("g").attr("transform",`translate(${this.margin.left},${this.margin.top})`),this.treeLayout=d3.tree().size([this.height,this.width]),this.tooltip=d3.select("body").append("div").attr("class","code-tooltip").style("opacity",0)}initializeTreeData(){console.log("Initializing tree data..."),this.treeData={name:"Project Root",type:"module",path:"/",complexity:0,children:[]},"undefined"!=typeof d3?(this.root=d3.hierarchy(this.treeData),this.root.x0=this.height/2,this.root.y0=0,console.log("Tree root created:",this.root)):(console.warn("D3 not available yet, deferring hierarchy creation"),this.root=null)}subscribeToEvents(){this.getSocket(),this.socket?(console.log("CodeTree: Socket available, subscribing to events"),this.socket.on("code:analysis:start",t=>this.handleAnalysisStart(t)),this.socket.on("code:analysis:accepted",t=>this.handleAnalysisAccepted(t)),this.socket.on("code:analysis:queued",t=>this.handleAnalysisQueued(t)),this.socket.on("code:analysis:cancelled",t=>this.handleAnalysisCancelled(t)),this.socket.on("code:analysis:progress",t=>this.handleProgress(t)),this.socket.on("code:analysis:complete",t=>this.handleAnalysisComplete(t)),this.socket.on("code:analysis:error",t=>this.handleAnalysisError(t)),this.socket.on("code:file:start",t=>this.handleFileStart(t)),this.socket.on("code:file:complete",t=>this.handleFileComplete(t)),this.socket.on("code:node:found",t=>this.handleNodeFound(t))):console.warn("CodeTree: Socket not available yet, will retry on analysis")}getSocket(){return this.socket||(window.socket?(this.socket=window.socket,console.log("CodeTree: Using window.socket")):window.dashboard?.socketClient?.socket?(this.socket=window.dashboard.socketClient.socket,console.log("CodeTree: Using dashboard.socketClient.socket")):window.socketClient?.socket&&(this.socket=window.socketClient.socket,console.log("CodeTree: Using socketClient.socket"))),this.socket}startAnalysis(){if(this.analyzing)return void console.log("Analysis already in progress");if(console.log("Starting code analysis..."),this.getSocket(),!this.socket)return console.error("Socket not available"),void this.showNotification("Cannot connect to server. Please check connection.","error");this.socket._callbacks&&this.socket._callbacks["code:analysis:start"]||(console.log("Re-subscribing to code analysis events"),this.subscribeToEvents()),this.analyzing=!0;const t=document.getElementById("analyze-code"),e=document.getElementById("cancel-analysis");t&&(t.textContent="Analyzing...",t.classList.add("analyzing")),e&&(e.style.display="inline-block"),this.showFooterAnalysisStatus("Starting analysis..."),this.initializeTreeData(),this.nodes.clear(),this.stats={files:0,classes:0,functions:0,methods:0,lines:0},this.updateStats(),this.svg||this.createVisualization(),this.root&&this.svg&&this.update(this.root);const s=document.getElementById("analysis-path");let i=s?.value?.trim();i&&""!==i||(i=window.workingDirectory||window.dashboard?.workingDirectory||window.socketClient?.sessions?.values()?.next()?.value?.working_directory||".",s&&"."!==i&&(s.value=i));const o=this.getSelectedLanguages(),n=parseInt(document.getElementById("max-depth")?.value)||null,a=this.getIgnorePatterns(),l=this.generateRequestId();this.currentRequestId=l;const r={request_id:l,path:i,languages:o.length>0?o:null,max_depth:n,ignore_patterns:a.length>0?a:null};console.log("Emitting code:analyze:request with payload:",r),this.socket.emit("code:analyze:request",r),this.requestTimeout=setTimeout(()=>{this.analyzing&&this.currentRequestId===l&&(console.warn("Analysis appears stuck after 60 seconds"),this.showNotification("Analysis is taking longer than expected. You can cancel if needed.","warning"))},6e4)}cancelAnalysis(){this.analyzing&&(console.log("Cancelling analysis..."),this.socket&&this.currentRequestId&&this.socket.emit("code:analyze:cancel",{request_id:this.currentRequestId}),this.resetAnalysisState())}resetAnalysisState(){this.analyzing=!1,this.currentRequestId=null,this.requestTimeout&&(clearTimeout(this.requestTimeout),this.requestTimeout=null);const t=document.getElementById("analyze-code"),e=document.getElementById("cancel-analysis");t&&(t.disabled=!1,t.textContent="Analyze",t.classList.remove("analyzing")),e&&(e.style.display="none"),this.hideFooterAnalysisStatus()}generateRequestId(){return`analysis-${Date.now()}-${Math.random().toString(36).substr(2,9)}`}getSelectedLanguages(){const t=[];return document.querySelectorAll(".language-checkbox:checked").forEach(e=>{t.push(e.value)}),t}getIgnorePatterns(){const t=[],e=document.getElementById("ignore-patterns");return e&&e.value&&t.push(...e.value.split(",").map(t=>t.trim()).filter(t=>t)),t}handleAnalysisStart(t){if(console.log("Code analysis started:",t),t.request_id&&t.request_id!==this.currentRequestId)return;this.requestTimeout&&(clearTimeout(this.requestTimeout),this.requestTimeout=null);const e=`Analyzing ${t.total_files||0} files...`;this.updateProgress(0,e),this.updateTicker(e,"progress"),this.showNotification("Analysis started - building tree in real-time...","info")}handleFileStart(t){console.log("Analyzing file:",t.path);const e=`Analyzing: ${t.path}`;this.updateProgress(t.progress||0,e),this.updateTicker(`📄 ${t.path}`,"file");const s={name:t.name||t.path.split("/").pop(),type:"file",path:t.path,language:t.language,children:[]};this.addNodeToTree(s,t.path),this.stats.files++,this.updateStats(),this.svg&&this.root&&(this.updateThrottleTimer||(this.updateThrottleTimer=setTimeout(()=>{this.update(this.root),this.updateThrottleTimer=null},100)))}handleFileComplete(t){if(console.log("File analysis complete:",t.path),t.stats){const e=this.nodes.get(t.path);e&&(e.stats=t.stats,t.stats.lines&&(this.stats.lines+=t.stats.lines,this.updateStats()))}}handleNodeFound(t){console.log("Node found:",t);const e={function:"⚡",class:"🏛️",method:"🔧",module:"📦"}[t.type]||"📌",s=t.name||"unnamed";this.updateTicker(`${e} ${s}`,"node");const i={name:t.name,type:t.type,path:t.path,line:t.line,complexity:t.complexity||0,docstring:t.docstring,params:t.params,returns:t.returns,children:[]};switch(this.addNodeToTree(i,t.parent_path||t.path),t.type){case"class":this.stats.classes++;break;case"function":this.stats.functions++;break;case"method":this.stats.methods++}t.lines&&(this.stats.lines+=t.lines),this.updateStats(),this.svg&&this.root&&(this.updateThrottleTimer&&clearTimeout(this.updateThrottleTimer),this.updateThrottleTimer=setTimeout(()=>{this.update(this.root),this.updateThrottleTimer=null},200))}handleProgress(t){this.updateProgress(t.percentage,t.message)}handleAnalysisComplete(t){if(console.log("Code analysis complete:",t),t.request_id&&t.request_id!==this.currentRequestId)return;this.resetAnalysisState(),this.svg&&this.update(this.root),t.stats&&(this.stats={...this.stats,...t.stats},this.updateStats());const e=`✅ Complete: ${this.stats.files} files, ${this.stats.functions} functions, ${this.stats.classes} classes`;this.updateTicker(e,"progress"),this.showNotification("Analysis complete","success")}handleAnalysisError(t){if(console.error("Code analysis error:",t),t.request_id&&t.request_id!==this.currentRequestId)return;this.resetAnalysisState();const e=t.message||"Unknown error";this.updateTicker(`❌ ${e}`,"error"),this.showNotification(`Analysis failed: ${e}`,"error")}handleAnalysisAccepted(t){console.log("Analysis request accepted:",t),t.request_id===this.currentRequestId&&(this.requestTimeout&&(clearTimeout(this.requestTimeout),this.requestTimeout=null),this.showNotification("Analysis request accepted by server","info"))}handleAnalysisQueued(t){console.log("Analysis queued:",t),t.request_id===this.currentRequestId&&this.showNotification(`Analysis queued (position: ${t.queue_size||1})`,"info")}handleAnalysisCancelled(t){console.log("Analysis cancelled:",t),t.request_id&&t.request_id!==this.currentRequestId||(this.resetAnalysisState(),this.showNotification("Analysis cancelled","warning"))}showNotification(t,e="info"){console.log(`CodeTree notification: ${t} (${e})`);let s=document.querySelector("#code-tab .notification-area");if(!s){const t=document.getElementById("code-tab");if(!t)return void console.error("Code tab not found for notification");s=document.createElement("div"),s.className="notification-area",s.style.cssText="\n position: absolute;\n top: 10px;\n right: 10px;\n max-width: 400px;\n z-index: 1000;\n padding: 12px 16px;\n border-radius: 4px;\n font-size: 14px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n transition: opacity 0.3s ease;\n ",t.insertBefore(s,t.firstChild)}const i={info:{bg:"#e3f2fd",text:"#1976d2",border:"#90caf9"},success:{bg:"#e8f5e9",text:"#388e3c",border:"#81c784"},warning:{bg:"#fff3e0",text:"#f57c00",border:"#ffb74d"},error:{bg:"#ffebee",text:"#d32f2f",border:"#ef5350"}},o=i[e]||i.info;s.style.backgroundColor=o.bg,s.style.color=o.text,s.style.border=`1px solid ${o.border}`,s.textContent=t,s.style.display="block",s.style.opacity="1",this.notificationTimeout&&clearTimeout(this.notificationTimeout),this.notificationTimeout=setTimeout(()=>{s.style.opacity="0",setTimeout(()=>{s.style.display="none"},300)},5e3)}addNodeToTree(t,e){let s=this.findNodeByPath(e);if(s||(s=this.treeData),this.nodes.has(t.path))console.log("Node already exists:",t.path);else if(s.children||(s.children=[]),s.children.push(t),this.nodes.set(t.path,t),"undefined"!=typeof d3){const t=new Set;this.root&&this.root.descendants().forEach(e=>{e.children&&t.add(e.data.path)}),this.root=d3.hierarchy(this.treeData),this.root.x0=this.height/2,this.root.y0=0,this.root.descendants().forEach(e=>{t.has(e.data.path)&&e._children&&(e.children=e._children,e._children=null)})}}findNodeByPath(t){return this.nodes.get(t)}updateProgress(t,e){const s=document.getElementById("footer-analysis-progress");if(s){let i=e||"Analyzing...";t>0&&(i=`[${Math.round(t)}%] ${i}`),s.textContent=i}}showFooterAnalysisStatus(t){const e=document.getElementById("footer-analysis-container"),s=document.getElementById("footer-analysis-progress");e&&(e.style.display="flex"),s&&(s.textContent=t||"Analyzing...",s.style.animation="pulse 1.5s ease-in-out infinite")}hideFooterAnalysisStatus(){const t=document.getElementById("footer-analysis-container"),e=document.getElementById("footer-analysis-progress");t&&setTimeout(()=>{t.style.display="none"},2e3),e&&(e.style.animation="none")}updateStats(){const t=document.getElementById("file-count"),e=document.getElementById("class-count"),s=document.getElementById("function-count"),i=document.getElementById("line-count");t&&(t.textContent=this.stats.files),e&&(e.textContent=this.stats.classes),s&&(s.textContent=this.stats.functions),i&&(i.textContent=this.stats.lines)}update(t){if(!this.svg||!this.treeGroup)return;const e=this.treeLayout(this.root),s=e.descendants(),i=e.links();s.forEach(t=>{t.y=180*t.depth});const o=this.treeGroup.selectAll("g.code-node").data(s,t=>t.id||(t.id=++this.nodeId)),n=o.enter().append("g").attr("class",t=>`code-node ${t.data.type} complexity-${this.getComplexityLevel(t.data.complexity)}`).attr("transform",e=>`translate(${t.y0},${t.x0})`).on("click",(t,e)=>this.toggleNode(t,e)).on("mouseover",(t,e)=>this.showTooltip(t,e)).on("mouseout",()=>this.hideTooltip());n.append("circle").attr("r",1e-6).style("fill",t=>t._children?"#e2e8f0":this.getNodeColor(t.data.type)),n.append("text").attr("dy",".35em").attr("x",t=>t.children||t._children?-13:13).attr("text-anchor",t=>t.children||t._children?"end":"start").text(t=>t.data.name).style("fill-opacity",1e-6),n.append("text").attr("class","node-icon").attr("dy",".35em").attr("x",0).attr("text-anchor","middle").text(t=>this.getNodeIcon(t.data.type)).style("font-size","16px");const a=n.merge(o);a.transition().duration(this.duration).attr("transform",t=>`translate(${t.y},${t.x})`),a.select("circle").attr("r",8).style("fill",t=>t._children?"#e2e8f0":this.getNodeColor(t.data.type)),a.select("text").style("fill-opacity",1);const l=o.exit().transition().duration(this.duration).attr("transform",e=>`translate(${t.y},${t.x})`).remove();l.select("circle").attr("r",1e-6),l.select("text").style("fill-opacity",1e-6);const r=this.treeGroup.selectAll("path.code-link").data(i,t=>t.target.id);r.enter().insert("path","g").attr("class","code-link").attr("d",e=>{const s={x:t.x0,y:t.y0};return this.diagonal(s,s)}).merge(r).transition().duration(this.duration).attr("d",t=>this.diagonal(t.source,t.target)),r.exit().transition().duration(this.duration).attr("d",e=>{const s={x:t.x,y:t.y};return this.diagonal(s,s)}).remove(),s.forEach(t=>{t.x0=t.x,t.y0=t.y})}diagonal(t,e){return`M ${t.y} ${t.x}\n C ${(t.y+e.y)/2} ${t.x},\n ${(t.y+e.y)/2} ${e.x},\n ${e.y} ${e.x}`}toggleNode(t,e){e.children?(e._children=e.children,e.children=null):(e.children=e._children,e._children=null),this.update(e),this.updateBreadcrumb(e),this.selectNode(e),"module"!==e.data.type&&"file"!==e.data.type&&this.showCodeViewer(e.data)}selectNode(t){this.selectedNode&&d3.select(this.selectedNode).classed("selected",!1),this.selectedNode=t,t&&d3.select(t).classed("selected",!0)}updateBreadcrumb(t){const e=[];let s=t;for(;s;)e.unshift(s.data.name),s=s.parent;const i=document.getElementById("breadcrumb-content");i&&(i.textContent=e.join(" > "),i.className="ticker-file")}updateTicker(t,e="info"){const s=document.getElementById("breadcrumb-content");if(s){let i="";switch(e){case"file":i="ticker-file";break;case"node":i="ticker-node";break;case"progress":i="ticker-progress";break;case"error":i="ticker-error";break;default:i=""}s.textContent=t,s.className=i+" ticker-event",s.style.animation="none",setTimeout(()=>{s.style.animation=""},10)}}showCodeViewer(t){window.CodeViewer&&window.CodeViewer.show(t)}showTooltip(t,e){if(!this.tooltip)return;let s=`<strong>${e.data.name}</strong><br/>`;s+=`Type: ${e.data.type}<br/>`,e.data.complexity&&(s+=`Complexity: ${e.data.complexity}<br/>`),e.data.line&&(s+=`Line: ${e.data.line}<br/>`),e.data.docstring&&(s+=`<em>${e.data.docstring.substring(0,100)}...</em>`),this.tooltip.transition().duration(200).style("opacity",.9),this.tooltip.html(s).style("left",t.pageX+10+"px").style("top",t.pageY-28+"px")}hideTooltip(){this.tooltip&&this.tooltip.transition().duration(500).style("opacity",0)}getNodeColor(t){return{module:"#8b5cf6",file:"#6366f1",class:"#3b82f6",function:"#f59e0b",method:"#10b981"}[t]||"#718096"}getNodeIcon(t){return{module:"📦",file:"📄",class:"🏛️",function:"⚡",method:"🔧"}[t]||"📌"}getComplexityLevel(t){return t<=5?"low":t<=10?"medium":"high"}expandAll(){this.expand(this.root),this.update(this.root)}expand(t){t._children&&(t.children=t._children,t._children=null),t.children&&t.children.forEach(t=>this.expand(t))}collapseAll(){this.collapse(this.root),this.update(this.root)}collapse(t){t.children&&(t._children=t.children,t.children.forEach(t=>this.collapse(t)),t.children=null)}resetZoom(){this.svg&&this.svg.transition().duration(750).call(d3.zoom().transform,d3.zoomIdentity)}toggleLegend(){const t=document.getElementById("tree-legend");t&&("none"===t.style.display?t.style.display="block":t.style.display="none")}filterByLanguage(){console.log("Filtering by language:",this.languageFilter),this.update(this.root)}highlightSearchResults(){this.treeGroup&&(this.treeGroup.selectAll(".code-node").classed("highlighted",!1),this.searchTerm&&this.treeGroup.selectAll(".code-node").each((t,e,s)=>{t.data.name.toLowerCase().includes(this.searchTerm)&&d3.select(s[e]).classed("highlighted",!0)}))}}"undefined"!=typeof window&&(window.CodeTree=t,document.addEventListener("DOMContentLoaded",()=>{const e=new t;window.codeTree=e,document.querySelectorAll(".tab-button").forEach(t=>{t.addEventListener("click",()=>{"code"===t.getAttribute("data-tab")&&(console.log("Code tab activated, initializing tree..."),e.renderWhenVisible())})})}));
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Code Tree Component
|
|
3
|
+
*
|
|
4
|
+
* D3.js-based tree visualization for displaying AST-based code structure.
|
|
5
|
+
* Shows modules, classes, functions, and methods with complexity-based coloring.
|
|
6
|
+
* Provides real-time updates during code analysis.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class CodeTree {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.container = null;
|
|
12
|
+
this.svg = null;
|
|
13
|
+
this.treeData = null;
|
|
14
|
+
this.root = null;
|
|
15
|
+
this.treeLayout = null;
|
|
16
|
+
this.treeGroup = null;
|
|
17
|
+
this.nodes = new Map();
|
|
18
|
+
this.stats = {
|
|
19
|
+
files: 0,
|
|
20
|
+
classes: 0,
|
|
21
|
+
functions: 0,
|
|
22
|
+
methods: 0,
|
|
23
|
+
lines: 0
|
|
24
|
+
};
|
|
25
|
+
// Radial layout settings
|
|
26
|
+
this.isRadialLayout = true; // Toggle for radial vs linear layout
|
|
27
|
+
this.margin = {top: 20, right: 20, bottom: 20, left: 20};
|
|
28
|
+
this.width = 960 - this.margin.left - this.margin.right;
|
|
29
|
+
this.height = 600 - this.margin.top - this.margin.bottom;
|
|
30
|
+
this.radius = Math.min(this.width, this.height) / 2;
|
|
31
|
+
this.nodeId = 0;
|
|
32
|
+
this.duration = 750;
|
|
33
|
+
this.languageFilter = 'all';
|
|
34
|
+
this.searchTerm = '';
|
|
35
|
+
this.tooltip = null;
|
|
36
|
+
this.initialized = false;
|
|
37
|
+
this.analyzing = false;
|
|
38
|
+
this.selectedNode = null;
|
|
39
|
+
this.socket = null;
|
|
40
|
+
this.autoDiscovered = false; // Track if auto-discovery has been done
|
|
41
|
+
this.zoom = null; // Store zoom behavior
|
|
42
|
+
this.activeNode = null; // Track currently active node
|
|
43
|
+
this.loadingNodes = new Set(); // Track nodes that are loading
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the code tree visualization
|
|
48
|
+
*/
|
|
49
|
+
initialize() {
|
|
50
|
+
if (this.initialized) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.container = document.getElementById('code-tree-container');
|
|
55
|
+
if (!this.container) {
|
|
56
|
+
console.error('Code tree container not found');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if tab is visible
|
|
61
|
+
const tabPanel = document.getElementById('code-tab');
|
|
62
|
+
if (!tabPanel) {
|
|
63
|
+
console.error('Code tab panel not found');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if working directory is set
|
|
68
|
+
const workingDir = this.getWorkingDirectory();
|
|
69
|
+
if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
|
|
70
|
+
this.showNoWorkingDirectoryMessage();
|
|
71
|
+
this.initialized = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Initialize always
|
|
76
|
+
this.setupControls();
|
|
77
|
+
this.initializeTreeData();
|
|
78
|
+
this.subscribeToEvents();
|
|
79
|
+
|
|
80
|
+
// Set initial status message
|
|
81
|
+
const breadcrumbContent = document.getElementById('breadcrumb-content');
|
|
82
|
+
if (breadcrumbContent && !this.analyzing) {
|
|
83
|
+
this.updateActivityTicker('Loading project structure...', 'info');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Only create visualization if tab is visible
|
|
87
|
+
if (tabPanel.classList.contains('active')) {
|
|
88
|
+
this.createVisualization();
|
|
89
|
+
if (this.root && this.svg) {
|
|
90
|
+
this.update(this.root);
|
|
91
|
+
}
|
|
92
|
+
// Auto-discover root level when tab is active
|
|
93
|
+
this.autoDiscoverRootLevel();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.initialized = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render visualization when tab becomes visible
|
|
101
|
+
*/
|
|
102
|
+
renderWhenVisible() {
|
|
103
|
+
// Check if working directory is set
|
|
104
|
+
const workingDir = this.getWorkingDirectory();
|
|
105
|
+
if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
|
|
106
|
+
this.showNoWorkingDirectoryMessage();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If no directory message is shown, remove it
|
|
111
|
+
this.removeNoWorkingDirectoryMessage();
|
|
112
|
+
|
|
113
|
+
if (!this.initialized) {
|
|
114
|
+
this.initialize();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!this.svg) {
|
|
119
|
+
this.createVisualization();
|
|
120
|
+
if (this.svg && this.treeGroup) {
|
|
121
|
+
this.update(this.root);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Force update with current data
|
|
125
|
+
if (this.root && this.svg) {
|
|
126
|
+
this.update(this.root);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Auto-discover root level if not done yet
|
|
131
|
+
if (!this.autoDiscovered) {
|
|
132
|
+
this.autoDiscoverRootLevel();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Set up control event handlers
|
|
138
|
+
*/
|
|
139
|
+
setupControls() {
|
|
140
|
+
// Remove analyze and cancel button handlers since they're no longer in the UI
|
|
141
|
+
|
|
142
|
+
const languageFilter = document.getElementById('language-filter');
|
|
143
|
+
if (languageFilter) {
|
|
144
|
+
languageFilter.addEventListener('change', (e) => {
|
|
145
|
+
this.languageFilter = e.target.value;
|
|
146
|
+
this.filterTree();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const searchBox = document.getElementById('code-search');
|
|
151
|
+
if (searchBox) {
|
|
152
|
+
searchBox.addEventListener('input', (e) => {
|
|
153
|
+
this.searchTerm = e.target.value.toLowerCase();
|
|
154
|
+
this.filterTree();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const expandBtn = document.getElementById('code-expand-all');
|
|
159
|
+
if (expandBtn) {
|
|
160
|
+
expandBtn.addEventListener('click', () => this.expandAll());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const collapseBtn = document.getElementById('code-collapse-all');
|
|
164
|
+
if (collapseBtn) {
|
|
165
|
+
collapseBtn.addEventListener('click', () => this.collapseAll());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const resetZoomBtn = document.getElementById('code-reset-zoom');
|
|
169
|
+
if (resetZoomBtn) {
|
|
170
|
+
resetZoomBtn.addEventListener('click', () => this.resetZoom());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const toggleLegendBtn = document.getElementById('code-toggle-legend');
|
|
174
|
+
if (toggleLegendBtn) {
|
|
175
|
+
toggleLegendBtn.addEventListener('click', () => this.toggleLegend());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Listen for working directory changes
|
|
179
|
+
document.addEventListener('workingDirectoryChanged', (e) => {
|
|
180
|
+
console.log('Working directory changed to:', e.detail.directory);
|
|
181
|
+
this.onWorkingDirectoryChanged(e.detail.directory);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Handle working directory change
|
|
187
|
+
*/
|
|
188
|
+
onWorkingDirectoryChanged(newDirectory) {
|
|
189
|
+
if (!newDirectory || newDirectory === 'Loading...' || newDirectory === 'Not selected') {
|
|
190
|
+
// Show no directory message
|
|
191
|
+
this.showNoWorkingDirectoryMessage();
|
|
192
|
+
// Reset tree state
|
|
193
|
+
this.autoDiscovered = false;
|
|
194
|
+
this.analyzing = false;
|
|
195
|
+
this.nodes.clear();
|
|
196
|
+
this.stats = {
|
|
197
|
+
files: 0,
|
|
198
|
+
classes: 0,
|
|
199
|
+
functions: 0,
|
|
200
|
+
methods: 0,
|
|
201
|
+
lines: 0
|
|
202
|
+
};
|
|
203
|
+
this.updateStats();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Remove any no directory message
|
|
208
|
+
this.removeNoWorkingDirectoryMessage();
|
|
209
|
+
|
|
210
|
+
// Reset discovery state for new directory
|
|
211
|
+
this.autoDiscovered = false;
|
|
212
|
+
this.analyzing = false;
|
|
213
|
+
|
|
214
|
+
// Clear existing data
|
|
215
|
+
this.nodes.clear();
|
|
216
|
+
this.stats = {
|
|
217
|
+
files: 0,
|
|
218
|
+
classes: 0,
|
|
219
|
+
functions: 0,
|
|
220
|
+
methods: 0,
|
|
221
|
+
lines: 0
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Re-initialize with new directory
|
|
225
|
+
this.initializeTreeData();
|
|
226
|
+
if (this.svg) {
|
|
227
|
+
this.update(this.root);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check if Code tab is currently active
|
|
231
|
+
const tabPanel = document.getElementById('code-tab');
|
|
232
|
+
if (tabPanel && tabPanel.classList.contains('active')) {
|
|
233
|
+
// Auto-discover in the new directory
|
|
234
|
+
this.autoDiscoverRootLevel();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.updateStats();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Show loading spinner
|
|
242
|
+
*/
|
|
243
|
+
showLoading() {
|
|
244
|
+
let loadingDiv = document.getElementById('code-tree-loading');
|
|
245
|
+
if (!loadingDiv) {
|
|
246
|
+
// Create loading element if it doesn't exist
|
|
247
|
+
const container = document.getElementById('code-tree-container');
|
|
248
|
+
if (container) {
|
|
249
|
+
loadingDiv = document.createElement('div');
|
|
250
|
+
loadingDiv.id = 'code-tree-loading';
|
|
251
|
+
loadingDiv.innerHTML = `
|
|
252
|
+
<div class="code-tree-spinner"></div>
|
|
253
|
+
<div class="code-tree-loading-text">Analyzing code structure...</div>
|
|
254
|
+
`;
|
|
255
|
+
container.appendChild(loadingDiv);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (loadingDiv) {
|
|
259
|
+
loadingDiv.classList.remove('hidden');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Hide loading spinner
|
|
265
|
+
*/
|
|
266
|
+
hideLoading() {
|
|
267
|
+
const loadingDiv = document.getElementById('code-tree-loading');
|
|
268
|
+
if (loadingDiv) {
|
|
269
|
+
loadingDiv.classList.add('hidden');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Create the D3.js visualization
|
|
275
|
+
*/
|
|
276
|
+
createVisualization() {
|
|
277
|
+
if (typeof d3 === 'undefined') {
|
|
278
|
+
console.error('D3.js is not loaded');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const container = d3.select('#code-tree-container');
|
|
283
|
+
container.selectAll('*').remove();
|
|
284
|
+
|
|
285
|
+
if (!container || !container.node()) {
|
|
286
|
+
console.error('Code tree container not found');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Calculate dimensions
|
|
291
|
+
const containerNode = container.node();
|
|
292
|
+
const containerWidth = containerNode.clientWidth || 960;
|
|
293
|
+
const containerHeight = containerNode.clientHeight || 600;
|
|
294
|
+
|
|
295
|
+
this.width = containerWidth - this.margin.left - this.margin.right;
|
|
296
|
+
this.height = containerHeight - this.margin.top - this.margin.bottom;
|
|
297
|
+
this.radius = Math.min(this.width, this.height) / 2;
|
|
298
|
+
|
|
299
|
+
// Create SVG
|
|
300
|
+
this.svg = container.append('svg')
|
|
301
|
+
.attr('width', containerWidth)
|
|
302
|
+
.attr('height', containerHeight);
|
|
303
|
+
|
|
304
|
+
// Create tree group with appropriate centering
|
|
305
|
+
const centerX = containerWidth / 2;
|
|
306
|
+
const centerY = containerHeight / 2;
|
|
307
|
+
|
|
308
|
+
// Different initial positioning for different layouts
|
|
309
|
+
if (this.isRadialLayout) {
|
|
310
|
+
// Radial: center in the middle of the canvas
|
|
311
|
+
this.treeGroup = this.svg.append('g')
|
|
312
|
+
.attr('transform', `translate(${centerX},${centerY})`);
|
|
313
|
+
} else {
|
|
314
|
+
// Linear: start from left with some margin
|
|
315
|
+
this.treeGroup = this.svg.append('g')
|
|
316
|
+
.attr('transform', `translate(${this.margin.left + 100},${centerY})`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Create tree layout with improved spacing
|
|
320
|
+
if (this.isRadialLayout) {
|
|
321
|
+
// Use d3.cluster for better radial distribution
|
|
322
|
+
this.treeLayout = d3.cluster()
|
|
323
|
+
.size([2 * Math.PI, this.radius - 100])
|
|
324
|
+
.separation((a, b) => {
|
|
325
|
+
// Enhanced separation for radial layout
|
|
326
|
+
if (a.parent == b.parent) {
|
|
327
|
+
// Base separation on tree depth for better spacing
|
|
328
|
+
const depthFactor = Math.max(1, 4 - a.depth);
|
|
329
|
+
// Increase spacing for nodes with many siblings
|
|
330
|
+
const siblingCount = a.parent ? (a.parent.children?.length || 1) : 1;
|
|
331
|
+
const siblingFactor = siblingCount > 5 ? 2 : (siblingCount > 3 ? 1.5 : 1);
|
|
332
|
+
// More spacing at outer levels where circumference is larger
|
|
333
|
+
const radiusFactor = 1 + (a.depth * 0.2);
|
|
334
|
+
return (depthFactor * siblingFactor) / (a.depth || 1) * radiusFactor;
|
|
335
|
+
} else {
|
|
336
|
+
// Different parents - ensure enough space
|
|
337
|
+
return 4 / (a.depth || 1);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
// Linear layout with dynamic sizing based on node count
|
|
342
|
+
// Use nodeSize for consistent spacing regardless of tree size
|
|
343
|
+
this.treeLayout = d3.tree()
|
|
344
|
+
.nodeSize([30, 200]) // Fixed spacing: 30px vertical, 200px horizontal
|
|
345
|
+
.separation((a, b) => {
|
|
346
|
+
// Consistent separation for linear layout
|
|
347
|
+
if (a.parent == b.parent) {
|
|
348
|
+
// Same parent - standard spacing
|
|
349
|
+
return 1;
|
|
350
|
+
} else {
|
|
351
|
+
// Different parents - slightly more space
|
|
352
|
+
return 1.5;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Add zoom behavior with proper transform handling
|
|
358
|
+
this.zoom = d3.zoom()
|
|
359
|
+
.scaleExtent([0.1, 10])
|
|
360
|
+
.on('zoom', (event) => {
|
|
361
|
+
if (this.isRadialLayout) {
|
|
362
|
+
// Radial: maintain center point
|
|
363
|
+
this.treeGroup.attr('transform',
|
|
364
|
+
`translate(${centerX + event.transform.x},${centerY + event.transform.y}) scale(${event.transform.k})`);
|
|
365
|
+
} else {
|
|
366
|
+
// Linear: maintain left margin
|
|
367
|
+
this.treeGroup.attr('transform',
|
|
368
|
+
`translate(${this.margin.left + 100 + event.transform.x},${centerY + event.transform.y}) scale(${event.transform.k})`);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
this.svg.call(this.zoom);
|
|
373
|
+
|
|
374
|
+
// Add controls overlay
|
|
375
|
+
this.addVisualizationControls();
|
|
376
|
+
|
|
377
|
+
// Create tooltip
|
|
378
|
+
this.tooltip = d3.select('body').append('div')
|
|
379
|
+
.attr('class', 'code-tree-tooltip')
|
|
380
|
+
.style('opacity', 0)
|
|
381
|
+
.style('position', 'absolute')
|
|
382
|
+
.style('background', 'rgba(0, 0, 0, 0.8)')
|
|
383
|
+
.style('color', 'white')
|
|
384
|
+
.style('padding', '8px')
|
|
385
|
+
.style('border-radius', '4px')
|
|
386
|
+
.style('font-size', '12px')
|
|
387
|
+
.style('pointer-events', 'none');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Clear all D3 visualization elements
|
|
392
|
+
*/
|
|
393
|
+
clearD3Visualization() {
|
|
394
|
+
if (this.treeGroup) {
|
|
395
|
+
console.log('[CodeTree] Clearing all D3 visualization elements');
|
|
396
|
+
// Remove all existing nodes and links
|
|
397
|
+
this.treeGroup.selectAll('g.node').remove();
|
|
398
|
+
this.treeGroup.selectAll('path.link').remove();
|
|
399
|
+
}
|
|
400
|
+
// Reset node ID counter for proper tracking
|
|
401
|
+
this.nodeId = 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Initialize tree data structure
|
|
406
|
+
*/
|
|
407
|
+
initializeTreeData() {
|
|
408
|
+
const workingDir = this.getWorkingDirectory();
|
|
409
|
+
const dirName = workingDir ? workingDir.split('/').pop() || 'Project Root' : 'Project Root';
|
|
410
|
+
const path = workingDir || '.';
|
|
411
|
+
|
|
412
|
+
this.treeData = {
|
|
413
|
+
name: dirName,
|
|
414
|
+
path: path,
|
|
415
|
+
type: 'root',
|
|
416
|
+
children: [],
|
|
417
|
+
loaded: false,
|
|
418
|
+
expanded: true // Start expanded
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
if (typeof d3 !== 'undefined') {
|
|
422
|
+
this.root = d3.hierarchy(this.treeData);
|
|
423
|
+
this.root.x0 = this.height / 2;
|
|
424
|
+
this.root.y0 = 0;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Subscribe to code analysis events
|
|
430
|
+
*/
|
|
431
|
+
subscribeToEvents() {
|
|
432
|
+
if (!this.socket) {
|
|
433
|
+
if (window.socket) {
|
|
434
|
+
this.socket = window.socket;
|
|
435
|
+
this.setupEventHandlers();
|
|
436
|
+
} else if (window.dashboard?.socketClient?.socket) {
|
|
437
|
+
this.socket = window.dashboard.socketClient.socket;
|
|
438
|
+
this.setupEventHandlers();
|
|
439
|
+
} else if (window.socketClient?.socket) {
|
|
440
|
+
this.socket = window.socketClient.socket;
|
|
441
|
+
this.setupEventHandlers();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Automatically discover root-level objects when tab opens
|
|
448
|
+
*/
|
|
449
|
+
autoDiscoverRootLevel() {
|
|
450
|
+
if (this.autoDiscovered || this.analyzing) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Update activity ticker
|
|
455
|
+
this.updateActivityTicker('🔍 Discovering project structure...', 'info');
|
|
456
|
+
|
|
457
|
+
// Get working directory
|
|
458
|
+
const workingDir = this.getWorkingDirectory();
|
|
459
|
+
if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
|
|
460
|
+
console.warn('Cannot auto-discover: no working directory set');
|
|
461
|
+
this.showNoWorkingDirectoryMessage();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Ensure we have an absolute path
|
|
466
|
+
if (!workingDir.startsWith('/') && !workingDir.match(/^[A-Z]:\\/)) {
|
|
467
|
+
console.error('Working directory is not absolute:', workingDir);
|
|
468
|
+
this.showNotification('Invalid working directory path', 'error');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
console.log('Auto-discovering root level for:', workingDir);
|
|
473
|
+
|
|
474
|
+
this.autoDiscovered = true;
|
|
475
|
+
this.analyzing = true;
|
|
476
|
+
|
|
477
|
+
// Clear any existing nodes
|
|
478
|
+
this.nodes.clear();
|
|
479
|
+
this.stats = {
|
|
480
|
+
files: 0,
|
|
481
|
+
classes: 0,
|
|
482
|
+
functions: 0,
|
|
483
|
+
methods: 0,
|
|
484
|
+
lines: 0
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Subscribe to events if not already done
|
|
488
|
+
if (this.socket && !this.socket.hasListeners('code:node:found')) {
|
|
489
|
+
this.setupEventHandlers();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Update tree data with working directory as the root
|
|
493
|
+
const dirName = workingDir.split('/').pop() || 'Project Root';
|
|
494
|
+
this.treeData = {
|
|
495
|
+
name: dirName,
|
|
496
|
+
path: workingDir,
|
|
497
|
+
type: 'root',
|
|
498
|
+
children: [],
|
|
499
|
+
loaded: false,
|
|
500
|
+
expanded: true // Start expanded to show discovered items
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
if (typeof d3 !== 'undefined') {
|
|
504
|
+
this.root = d3.hierarchy(this.treeData);
|
|
505
|
+
this.root.x0 = this.height / 2;
|
|
506
|
+
this.root.y0 = 0;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Update UI
|
|
510
|
+
this.showLoading();
|
|
511
|
+
this.updateBreadcrumb(`Discovering structure in ${dirName}...`, 'info');
|
|
512
|
+
|
|
513
|
+
// Get selected languages from checkboxes
|
|
514
|
+
const selectedLanguages = [];
|
|
515
|
+
document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
|
|
516
|
+
selectedLanguages.push(cb.value);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Get ignore patterns
|
|
520
|
+
const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
|
|
521
|
+
|
|
522
|
+
// Enhanced debug logging
|
|
523
|
+
console.log('[DEBUG] Frontend autoDiscoverRootLevel:');
|
|
524
|
+
console.log('[DEBUG] - Working directory:', workingDir);
|
|
525
|
+
console.log('[DEBUG] - Selected languages:', selectedLanguages);
|
|
526
|
+
console.log('[DEBUG] - Ignore patterns:', ignorePatterns);
|
|
527
|
+
|
|
528
|
+
// Request top-level discovery with working directory
|
|
529
|
+
const requestPayload = {
|
|
530
|
+
path: workingDir, // Use working directory instead of '.'
|
|
531
|
+
depth: 'top_level',
|
|
532
|
+
languages: selectedLanguages,
|
|
533
|
+
ignore_patterns: ignorePatterns,
|
|
534
|
+
request_id: `discover_${Date.now()}` // Add request ID for tracking
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
console.log('[DEBUG] Sending discovery request with full payload:', JSON.stringify(requestPayload, null, 2));
|
|
538
|
+
|
|
539
|
+
if (this.socket) {
|
|
540
|
+
this.socket.emit('code:discover:top_level', requestPayload);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Update stats display
|
|
544
|
+
this.updateStats();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Legacy analyzeCode method - redirects to auto-discovery
|
|
549
|
+
*/
|
|
550
|
+
analyzeCode() {
|
|
551
|
+
if (this.analyzing) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Redirect to auto-discovery
|
|
556
|
+
this.autoDiscoverRootLevel();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Cancel ongoing analysis - removed since we no longer have a cancel button
|
|
561
|
+
*/
|
|
562
|
+
cancelAnalysis() {
|
|
563
|
+
this.analyzing = false;
|
|
564
|
+
this.hideLoading();
|
|
565
|
+
|
|
566
|
+
if (this.socket) {
|
|
567
|
+
this.socket.emit('code:analysis:cancel');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.updateBreadcrumb('Analysis cancelled', 'warning');
|
|
571
|
+
this.showNotification('Analysis cancelled', 'warning');
|
|
572
|
+
this.addEventToDisplay('Analysis cancelled', 'warning');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Create the events display area
|
|
577
|
+
*/
|
|
578
|
+
createEventsDisplay() {
|
|
579
|
+
let eventsContainer = document.getElementById('analysis-events');
|
|
580
|
+
if (!eventsContainer) {
|
|
581
|
+
const treeContainer = document.getElementById('code-tree-container');
|
|
582
|
+
if (treeContainer) {
|
|
583
|
+
eventsContainer = document.createElement('div');
|
|
584
|
+
eventsContainer.id = 'analysis-events';
|
|
585
|
+
eventsContainer.className = 'analysis-events';
|
|
586
|
+
eventsContainer.style.display = 'none';
|
|
587
|
+
treeContainer.appendChild(eventsContainer);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Clear the events display
|
|
594
|
+
*/
|
|
595
|
+
clearEventsDisplay() {
|
|
596
|
+
const eventsContainer = document.getElementById('analysis-events');
|
|
597
|
+
if (eventsContainer) {
|
|
598
|
+
eventsContainer.innerHTML = '';
|
|
599
|
+
eventsContainer.style.display = 'block';
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Add an event to the display
|
|
605
|
+
*/
|
|
606
|
+
addEventToDisplay(message, type = 'info') {
|
|
607
|
+
const eventsContainer = document.getElementById('analysis-events');
|
|
608
|
+
if (eventsContainer) {
|
|
609
|
+
const eventEl = document.createElement('div');
|
|
610
|
+
eventEl.className = 'analysis-event';
|
|
611
|
+
eventEl.style.borderLeftColor = type === 'warning' ? '#f59e0b' :
|
|
612
|
+
type === 'error' ? '#ef4444' : '#3b82f6';
|
|
613
|
+
|
|
614
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
615
|
+
eventEl.innerHTML = `<span style="color: #718096;">[${timestamp}]</span> ${message}`;
|
|
616
|
+
|
|
617
|
+
eventsContainer.appendChild(eventEl);
|
|
618
|
+
// Auto-scroll to bottom
|
|
619
|
+
eventsContainer.scrollTop = eventsContainer.scrollHeight;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Setup Socket.IO event handlers
|
|
625
|
+
*/
|
|
626
|
+
setupEventHandlers() {
|
|
627
|
+
if (!this.socket) return;
|
|
628
|
+
|
|
629
|
+
// Analysis lifecycle events
|
|
630
|
+
this.socket.on('code:analysis:accepted', (data) => this.onAnalysisAccepted(data));
|
|
631
|
+
this.socket.on('code:analysis:queued', (data) => this.onAnalysisQueued(data));
|
|
632
|
+
this.socket.on('code:analysis:start', (data) => this.onAnalysisStart(data));
|
|
633
|
+
this.socket.on('code:analysis:complete', (data) => this.onAnalysisComplete(data));
|
|
634
|
+
this.socket.on('code:analysis:cancelled', (data) => this.onAnalysisCancelled(data));
|
|
635
|
+
this.socket.on('code:analysis:error', (data) => this.onAnalysisError(data));
|
|
636
|
+
|
|
637
|
+
// Node discovery events
|
|
638
|
+
this.socket.on('code:directory:discovered', (data) => this.onDirectoryDiscovered(data));
|
|
639
|
+
this.socket.on('code:file:discovered', (data) => this.onFileDiscovered(data));
|
|
640
|
+
this.socket.on('code:file:analyzed', (data) => this.onFileAnalyzed(data));
|
|
641
|
+
this.socket.on('code:node:found', (data) => this.onNodeFound(data));
|
|
642
|
+
|
|
643
|
+
// Progress updates
|
|
644
|
+
this.socket.on('code:analysis:progress', (data) => this.onProgressUpdate(data));
|
|
645
|
+
|
|
646
|
+
// Lazy loading responses
|
|
647
|
+
this.socket.on('code:directory:contents', (data) => {
|
|
648
|
+
// Update the requested directory with its contents
|
|
649
|
+
if (data.path) {
|
|
650
|
+
const node = this.findNodeByPath(data.path);
|
|
651
|
+
if (node && data.children) {
|
|
652
|
+
// Find D3 node and remove loading pulse
|
|
653
|
+
const d3Node = this.findD3NodeByPath(data.path);
|
|
654
|
+
if (d3Node && this.loadingNodes.has(data.path)) {
|
|
655
|
+
this.removeLoadingPulse(d3Node);
|
|
656
|
+
}
|
|
657
|
+
node.children = data.children.map(child => ({
|
|
658
|
+
...child,
|
|
659
|
+
loaded: child.type === 'directory' ? false : undefined,
|
|
660
|
+
analyzed: child.type === 'file' ? false : undefined,
|
|
661
|
+
expanded: false,
|
|
662
|
+
children: []
|
|
663
|
+
}));
|
|
664
|
+
node.loaded = true;
|
|
665
|
+
|
|
666
|
+
// Update D3 hierarchy
|
|
667
|
+
if (this.root && this.svg) {
|
|
668
|
+
this.root = d3.hierarchy(this.treeData);
|
|
669
|
+
this.root.x0 = this.height / 2;
|
|
670
|
+
this.root.y0 = 0;
|
|
671
|
+
this.update(this.root);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Update stats based on discovered contents
|
|
675
|
+
if (data.stats) {
|
|
676
|
+
this.stats.files += data.stats.files || 0;
|
|
677
|
+
this.stats.directories += data.stats.directories || 0;
|
|
678
|
+
this.updateStats();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.updateBreadcrumb(`Loaded ${data.path}`, 'success');
|
|
682
|
+
this.hideLoading();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Top level discovery response
|
|
688
|
+
this.socket.on('code:top_level:discovered', (data) => {
|
|
689
|
+
if (data.items && Array.isArray(data.items)) {
|
|
690
|
+
console.log('[CodeTree] Top-level discovered:', data.items.length, 'items');
|
|
691
|
+
|
|
692
|
+
// Add discovered items to the root node
|
|
693
|
+
this.treeData.children = data.items.map(item => ({
|
|
694
|
+
name: item.name,
|
|
695
|
+
path: item.path,
|
|
696
|
+
type: item.type,
|
|
697
|
+
language: item.type === 'file' ? this.detectLanguage(item.path) : undefined,
|
|
698
|
+
size: item.size,
|
|
699
|
+
lines: item.lines,
|
|
700
|
+
loaded: item.type === 'directory' ? false : undefined,
|
|
701
|
+
analyzed: item.type === 'file' ? false : undefined,
|
|
702
|
+
expanded: false,
|
|
703
|
+
children: []
|
|
704
|
+
}));
|
|
705
|
+
|
|
706
|
+
this.treeData.loaded = true;
|
|
707
|
+
|
|
708
|
+
// Update stats
|
|
709
|
+
if (data.stats) {
|
|
710
|
+
this.stats = { ...this.stats, ...data.stats };
|
|
711
|
+
this.updateStats();
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Update D3 hierarchy
|
|
715
|
+
if (typeof d3 !== 'undefined') {
|
|
716
|
+
// Clear any existing nodes before creating new ones
|
|
717
|
+
this.clearD3Visualization();
|
|
718
|
+
|
|
719
|
+
// Create new hierarchy
|
|
720
|
+
this.root = d3.hierarchy(this.treeData);
|
|
721
|
+
this.root.x0 = this.height / 2;
|
|
722
|
+
this.root.y0 = 0;
|
|
723
|
+
|
|
724
|
+
if (this.svg) {
|
|
725
|
+
this.update(this.root);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this.analyzing = false;
|
|
730
|
+
this.hideLoading();
|
|
731
|
+
this.updateBreadcrumb(`Discovered ${data.items.length} root items`, 'success');
|
|
732
|
+
this.showNotification(`Found ${data.items.length} items in project root`, 'success');
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Handle analysis start event
|
|
739
|
+
*/
|
|
740
|
+
onAnalysisStart(data) {
|
|
741
|
+
this.analyzing = true;
|
|
742
|
+
const message = data.message || 'Starting code analysis...';
|
|
743
|
+
|
|
744
|
+
// Update activity ticker
|
|
745
|
+
this.updateActivityTicker('🚀 Starting analysis...', 'info');
|
|
746
|
+
|
|
747
|
+
this.updateBreadcrumb(message, 'info');
|
|
748
|
+
this.addEventToDisplay(`🚀 ${message}`, 'info');
|
|
749
|
+
|
|
750
|
+
// Initialize or clear the tree
|
|
751
|
+
if (!this.treeData || this.treeData.children.length === 0) {
|
|
752
|
+
this.initializeTreeData();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Reset stats
|
|
756
|
+
this.stats = {
|
|
757
|
+
files: 0,
|
|
758
|
+
classes: 0,
|
|
759
|
+
functions: 0,
|
|
760
|
+
methods: 0,
|
|
761
|
+
lines: 0
|
|
762
|
+
};
|
|
763
|
+
this.updateStats();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Handle directory discovered event
|
|
768
|
+
*/
|
|
769
|
+
onDirectoryDiscovered(data) {
|
|
770
|
+
// Update activity ticker first
|
|
771
|
+
this.updateActivityTicker(`📁 Discovered: ${data.name || 'directory'}`);
|
|
772
|
+
|
|
773
|
+
// Add to events display
|
|
774
|
+
this.addEventToDisplay(`📁 Found ${(data.children || []).length} items in: ${data.name || data.path}`, 'info');
|
|
775
|
+
|
|
776
|
+
// Find the node that was clicked to trigger this discovery
|
|
777
|
+
const node = this.findNodeByPath(data.path);
|
|
778
|
+
if (node && data.children) {
|
|
779
|
+
// Update the node with discovered children
|
|
780
|
+
node.children = data.children.map(child => ({
|
|
781
|
+
name: child.name,
|
|
782
|
+
path: child.path,
|
|
783
|
+
type: child.type,
|
|
784
|
+
loaded: child.type === 'directory' ? false : undefined,
|
|
785
|
+
analyzed: child.type === 'file' ? false : undefined,
|
|
786
|
+
expanded: false,
|
|
787
|
+
children: child.type === 'directory' ? [] : undefined,
|
|
788
|
+
size: child.size,
|
|
789
|
+
has_code: child.has_code
|
|
790
|
+
}));
|
|
791
|
+
node.loaded = true;
|
|
792
|
+
node.expanded = true;
|
|
793
|
+
|
|
794
|
+
// Find D3 node and remove loading pulse
|
|
795
|
+
const d3Node = this.findD3NodeByPath(data.path);
|
|
796
|
+
if (d3Node) {
|
|
797
|
+
// Remove loading animation
|
|
798
|
+
if (this.loadingNodes.has(data.path)) {
|
|
799
|
+
this.removeLoadingPulse(d3Node);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Expand the node in D3
|
|
803
|
+
if (d3Node.data) {
|
|
804
|
+
d3Node.data.children = node.children;
|
|
805
|
+
d3Node._children = null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Update D3 hierarchy and redraw
|
|
810
|
+
if (this.root && this.svg) {
|
|
811
|
+
this.root = d3.hierarchy(this.treeData);
|
|
812
|
+
this.update(this.root);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
this.updateBreadcrumb(`Loaded ${node.children.length} items from ${node.name}`, 'success');
|
|
816
|
+
this.updateStats();
|
|
817
|
+
} else if (!node) {
|
|
818
|
+
// This might be a top-level directory discovery
|
|
819
|
+
const pathParts = data.path ? data.path.split('/').filter(p => p) : [];
|
|
820
|
+
const isTopLevel = pathParts.length === 1;
|
|
821
|
+
|
|
822
|
+
if (isTopLevel || data.forceAdd) {
|
|
823
|
+
const dirNode = {
|
|
824
|
+
name: data.name || pathParts[pathParts.length - 1] || 'Unknown',
|
|
825
|
+
path: data.path,
|
|
826
|
+
type: 'directory',
|
|
827
|
+
children: [],
|
|
828
|
+
loaded: false,
|
|
829
|
+
expanded: false,
|
|
830
|
+
stats: data.stats || {}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
this.addNodeToTree(dirNode, data.parent || '');
|
|
834
|
+
this.updateBreadcrumb(`Discovered: ${data.path}`, 'info');
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Handle file discovered event
|
|
841
|
+
*/
|
|
842
|
+
onFileDiscovered(data) {
|
|
843
|
+
// Update activity ticker
|
|
844
|
+
const fileName = data.name || (data.path ? data.path.split('/').pop() : 'file');
|
|
845
|
+
this.updateActivityTicker(`📄 Found: ${fileName}`);
|
|
846
|
+
|
|
847
|
+
// Add to events display
|
|
848
|
+
this.addEventToDisplay(`📄 Discovered: ${data.path || 'Unknown file'}`, 'info');
|
|
849
|
+
|
|
850
|
+
const pathParts = data.path ? data.path.split('/').filter(p => p) : [];
|
|
851
|
+
const parentPath = pathParts.slice(0, -1).join('/');
|
|
852
|
+
|
|
853
|
+
const fileNode = {
|
|
854
|
+
name: data.name || pathParts[pathParts.length - 1] || 'Unknown',
|
|
855
|
+
path: data.path,
|
|
856
|
+
type: 'file',
|
|
857
|
+
language: data.language || this.detectLanguage(data.path),
|
|
858
|
+
size: data.size || 0,
|
|
859
|
+
lines: data.lines || 0,
|
|
860
|
+
children: [],
|
|
861
|
+
analyzed: false
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
this.addNodeToTree(fileNode, parentPath);
|
|
865
|
+
this.stats.files++;
|
|
866
|
+
this.updateStats();
|
|
867
|
+
this.updateBreadcrumb(`Found: ${data.path}`, 'info');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Handle file analyzed event
|
|
872
|
+
*/
|
|
873
|
+
onFileAnalyzed(data) {
|
|
874
|
+
// Remove loading pulse if this file was being analyzed
|
|
875
|
+
const d3Node = this.findD3NodeByPath(data.path);
|
|
876
|
+
if (d3Node && this.loadingNodes.has(data.path)) {
|
|
877
|
+
this.removeLoadingPulse(d3Node);
|
|
878
|
+
}
|
|
879
|
+
// Update activity ticker
|
|
880
|
+
if (data.path) {
|
|
881
|
+
const fileName = data.path.split('/').pop();
|
|
882
|
+
this.updateActivityTicker(`🔍 Analyzed: ${fileName}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const fileNode = this.findNodeByPath(data.path);
|
|
886
|
+
if (fileNode) {
|
|
887
|
+
fileNode.analyzed = true;
|
|
888
|
+
fileNode.complexity = data.complexity || 0;
|
|
889
|
+
fileNode.lines = data.lines || 0;
|
|
890
|
+
|
|
891
|
+
// Add code elements as children
|
|
892
|
+
if (data.elements && Array.isArray(data.elements)) {
|
|
893
|
+
fileNode.children = data.elements.map(elem => ({
|
|
894
|
+
name: elem.name,
|
|
895
|
+
type: elem.type.toLowerCase(),
|
|
896
|
+
path: `${data.path}#${elem.name}`,
|
|
897
|
+
line: elem.line,
|
|
898
|
+
complexity: elem.complexity || 1,
|
|
899
|
+
docstring: elem.docstring || '',
|
|
900
|
+
children: elem.methods ? elem.methods.map(m => ({
|
|
901
|
+
name: m.name,
|
|
902
|
+
type: 'method',
|
|
903
|
+
path: `${data.path}#${elem.name}.${m.name}`,
|
|
904
|
+
line: m.line,
|
|
905
|
+
complexity: m.complexity || 1,
|
|
906
|
+
docstring: m.docstring || ''
|
|
907
|
+
})) : []
|
|
908
|
+
}));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Update stats
|
|
912
|
+
if (data.stats) {
|
|
913
|
+
this.stats.classes += data.stats.classes || 0;
|
|
914
|
+
this.stats.functions += data.stats.functions || 0;
|
|
915
|
+
this.stats.methods += data.stats.methods || 0;
|
|
916
|
+
this.stats.lines += data.stats.lines || 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
this.updateStats();
|
|
920
|
+
if (this.root) {
|
|
921
|
+
this.update(this.root);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
this.updateBreadcrumb(`Analyzed: ${data.path}`, 'success');
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Handle node found event
|
|
930
|
+
*/
|
|
931
|
+
onNodeFound(data) {
|
|
932
|
+
// Add to events display with appropriate icon
|
|
933
|
+
const typeIcon = data.type === 'class' ? '🏛️' :
|
|
934
|
+
data.type === 'function' ? '⚡' :
|
|
935
|
+
data.type === 'method' ? '🔧' : '📦';
|
|
936
|
+
this.addEventToDisplay(`${typeIcon} Found ${data.type || 'node'}: ${data.name || 'Unknown'}`);
|
|
937
|
+
|
|
938
|
+
// Extract node info
|
|
939
|
+
const nodeInfo = {
|
|
940
|
+
name: data.name || 'Unknown',
|
|
941
|
+
type: (data.type || 'unknown').toLowerCase(),
|
|
942
|
+
path: data.path || '',
|
|
943
|
+
line: data.line || 0,
|
|
944
|
+
complexity: data.complexity || 1,
|
|
945
|
+
docstring: data.docstring || ''
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// Map event types to our internal types
|
|
949
|
+
const typeMapping = {
|
|
950
|
+
'class': 'class',
|
|
951
|
+
'function': 'function',
|
|
952
|
+
'method': 'method',
|
|
953
|
+
'module': 'module',
|
|
954
|
+
'file': 'file',
|
|
955
|
+
'directory': 'directory'
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
nodeInfo.type = typeMapping[nodeInfo.type] || nodeInfo.type;
|
|
959
|
+
|
|
960
|
+
// Determine parent path
|
|
961
|
+
let parentPath = '';
|
|
962
|
+
if (data.parent_path) {
|
|
963
|
+
parentPath = data.parent_path;
|
|
964
|
+
} else if (data.file_path) {
|
|
965
|
+
parentPath = data.file_path;
|
|
966
|
+
} else if (nodeInfo.path.includes('/')) {
|
|
967
|
+
const parts = nodeInfo.path.split('/');
|
|
968
|
+
parts.pop();
|
|
969
|
+
parentPath = parts.join('/');
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Update stats based on node type
|
|
973
|
+
switch(nodeInfo.type) {
|
|
974
|
+
case 'class':
|
|
975
|
+
this.stats.classes++;
|
|
976
|
+
break;
|
|
977
|
+
case 'function':
|
|
978
|
+
this.stats.functions++;
|
|
979
|
+
break;
|
|
980
|
+
case 'method':
|
|
981
|
+
this.stats.methods++;
|
|
982
|
+
break;
|
|
983
|
+
case 'file':
|
|
984
|
+
this.stats.files++;
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Add node to tree
|
|
989
|
+
this.addNodeToTree(nodeInfo, parentPath);
|
|
990
|
+
this.updateStats();
|
|
991
|
+
|
|
992
|
+
// Show progress in breadcrumb
|
|
993
|
+
const elementType = nodeInfo.type.charAt(0).toUpperCase() + nodeInfo.type.slice(1);
|
|
994
|
+
this.updateBreadcrumb(`Found ${elementType}: ${nodeInfo.name}`, 'info');
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Handle progress update
|
|
999
|
+
*/
|
|
1000
|
+
onProgressUpdate(data) {
|
|
1001
|
+
const progress = data.progress || 0;
|
|
1002
|
+
const message = data.message || `Processing... ${progress}%`;
|
|
1003
|
+
|
|
1004
|
+
this.updateBreadcrumb(message, 'info');
|
|
1005
|
+
|
|
1006
|
+
// Update progress bar if it exists
|
|
1007
|
+
const progressBar = document.querySelector('.code-tree-progress');
|
|
1008
|
+
if (progressBar) {
|
|
1009
|
+
progressBar.style.width = `${progress}%`;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Handle analysis complete event
|
|
1015
|
+
*/
|
|
1016
|
+
onAnalysisComplete(data) {
|
|
1017
|
+
this.analyzing = false;
|
|
1018
|
+
this.hideLoading();
|
|
1019
|
+
|
|
1020
|
+
// Update activity ticker
|
|
1021
|
+
this.updateActivityTicker('✅ Ready', 'success');
|
|
1022
|
+
|
|
1023
|
+
// Add completion event
|
|
1024
|
+
this.addEventToDisplay('✅ Analysis complete!', 'success');
|
|
1025
|
+
|
|
1026
|
+
// Update tree visualization
|
|
1027
|
+
if (this.root && this.svg) {
|
|
1028
|
+
this.update(this.root);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Update stats from completion data
|
|
1032
|
+
if (data.stats) {
|
|
1033
|
+
this.stats = { ...this.stats, ...data.stats };
|
|
1034
|
+
this.updateStats();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const message = data.message || `Analysis complete: ${this.stats.files} files, ${this.stats.classes} classes, ${this.stats.functions} functions`;
|
|
1038
|
+
this.updateBreadcrumb(message, 'success');
|
|
1039
|
+
this.showNotification(message, 'success');
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Handle analysis error
|
|
1044
|
+
*/
|
|
1045
|
+
onAnalysisError(data) {
|
|
1046
|
+
this.analyzing = false;
|
|
1047
|
+
this.hideLoading();
|
|
1048
|
+
|
|
1049
|
+
const message = data.message || data.error || 'Analysis failed';
|
|
1050
|
+
this.updateBreadcrumb(message, 'error');
|
|
1051
|
+
this.showNotification(message, 'error');
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Handle analysis accepted
|
|
1056
|
+
*/
|
|
1057
|
+
onAnalysisAccepted(data) {
|
|
1058
|
+
const message = data.message || 'Analysis request accepted';
|
|
1059
|
+
this.updateBreadcrumb(message, 'info');
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Handle analysis queued
|
|
1064
|
+
*/
|
|
1065
|
+
onAnalysisQueued(data) {
|
|
1066
|
+
const position = data.position || 0;
|
|
1067
|
+
const message = `Analysis queued (position ${position})`;
|
|
1068
|
+
this.updateBreadcrumb(message, 'warning');
|
|
1069
|
+
this.showNotification(message, 'info');
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Handle INFO events for granular work tracking
|
|
1074
|
+
*/
|
|
1075
|
+
onInfoEvent(data) {
|
|
1076
|
+
// Log to console for debugging
|
|
1077
|
+
console.log('[INFO]', data.type, data.message);
|
|
1078
|
+
|
|
1079
|
+
// Update breadcrumb for certain events
|
|
1080
|
+
if (data.type && data.type.startsWith('discovery.')) {
|
|
1081
|
+
// Discovery events
|
|
1082
|
+
if (data.type === 'discovery.start') {
|
|
1083
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1084
|
+
} else if (data.type === 'discovery.complete') {
|
|
1085
|
+
this.updateBreadcrumb(data.message, 'success');
|
|
1086
|
+
// Show stats if available
|
|
1087
|
+
if (data.stats) {
|
|
1088
|
+
console.log('[DISCOVERY STATS]', data.stats);
|
|
1089
|
+
}
|
|
1090
|
+
} else if (data.type === 'discovery.directory' || data.type === 'discovery.file') {
|
|
1091
|
+
// Quick flash of discovery events
|
|
1092
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1093
|
+
}
|
|
1094
|
+
} else if (data.type && data.type.startsWith('analysis.')) {
|
|
1095
|
+
// Analysis events
|
|
1096
|
+
if (data.type === 'analysis.start') {
|
|
1097
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1098
|
+
} else if (data.type === 'analysis.complete') {
|
|
1099
|
+
this.updateBreadcrumb(data.message, 'success');
|
|
1100
|
+
// Show stats if available
|
|
1101
|
+
if (data.stats) {
|
|
1102
|
+
const statsMsg = `Found: ${data.stats.classes || 0} classes, ${data.stats.functions || 0} functions, ${data.stats.methods || 0} methods`;
|
|
1103
|
+
console.log('[ANALYSIS STATS]', statsMsg);
|
|
1104
|
+
}
|
|
1105
|
+
} else if (data.type === 'analysis.class' || data.type === 'analysis.function' || data.type === 'analysis.method') {
|
|
1106
|
+
// Show found elements briefly
|
|
1107
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1108
|
+
} else if (data.type === 'analysis.parse') {
|
|
1109
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1110
|
+
}
|
|
1111
|
+
} else if (data.type && data.type.startsWith('filter.')) {
|
|
1112
|
+
// Filter events - optionally show in debug mode
|
|
1113
|
+
if (window.debugMode || this.showFilterEvents) {
|
|
1114
|
+
console.debug('[FILTER]', data.type, data.path, data.reason);
|
|
1115
|
+
if (this.showFilterEvents) {
|
|
1116
|
+
this.updateBreadcrumb(data.message, 'warning');
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
} else if (data.type && data.type.startsWith('cache.')) {
|
|
1120
|
+
// Cache events
|
|
1121
|
+
if (data.type === 'cache.hit') {
|
|
1122
|
+
console.debug('[CACHE HIT]', data.file);
|
|
1123
|
+
if (this.showCacheEvents) {
|
|
1124
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1125
|
+
}
|
|
1126
|
+
} else if (data.type === 'cache.miss') {
|
|
1127
|
+
console.debug('[CACHE MISS]', data.file);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Optionally add to an event log display if enabled
|
|
1132
|
+
if (this.eventLogEnabled && data.message) {
|
|
1133
|
+
this.addEventToDisplay(data);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Add event to display log (if we have one)
|
|
1139
|
+
*/
|
|
1140
|
+
addEventToDisplay(data) {
|
|
1141
|
+
// Could be implemented to show events in a dedicated log area
|
|
1142
|
+
// For now, just maintain a recent events list
|
|
1143
|
+
if (!this.recentEvents) {
|
|
1144
|
+
this.recentEvents = [];
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
this.recentEvents.unshift({
|
|
1148
|
+
timestamp: data.timestamp || new Date().toISOString(),
|
|
1149
|
+
type: data.type,
|
|
1150
|
+
message: data.message,
|
|
1151
|
+
data: data
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// Keep only last 100 events
|
|
1155
|
+
if (this.recentEvents.length > 100) {
|
|
1156
|
+
this.recentEvents.pop();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Could update a UI element here if we had an event log display
|
|
1160
|
+
console.log('[EVENT LOG]', data.type, data.message);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Handle analysis cancelled
|
|
1165
|
+
*/
|
|
1166
|
+
onAnalysisCancelled(data) {
|
|
1167
|
+
this.analyzing = false;
|
|
1168
|
+
this.hideLoading();
|
|
1169
|
+
const message = data.message || 'Analysis cancelled';
|
|
1170
|
+
this.updateBreadcrumb(message, 'warning');
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Show notification toast
|
|
1175
|
+
*/
|
|
1176
|
+
showNotification(message, type = 'info') {
|
|
1177
|
+
const notification = document.createElement('div');
|
|
1178
|
+
notification.className = `code-tree-notification ${type}`;
|
|
1179
|
+
notification.textContent = message;
|
|
1180
|
+
|
|
1181
|
+
// Change from appending to container to positioning absolutely within it
|
|
1182
|
+
const container = document.getElementById('code-tree-container');
|
|
1183
|
+
if (container) {
|
|
1184
|
+
// Position relative to the container
|
|
1185
|
+
notification.style.position = 'absolute';
|
|
1186
|
+
notification.style.top = '10px';
|
|
1187
|
+
notification.style.right = '10px';
|
|
1188
|
+
notification.style.zIndex = '1000';
|
|
1189
|
+
|
|
1190
|
+
// Ensure container is positioned
|
|
1191
|
+
if (!container.style.position || container.style.position === 'static') {
|
|
1192
|
+
container.style.position = 'relative';
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
container.appendChild(notification);
|
|
1196
|
+
|
|
1197
|
+
// Animate out after 3 seconds
|
|
1198
|
+
setTimeout(() => {
|
|
1199
|
+
notification.style.animation = 'slideOutRight 0.3s ease';
|
|
1200
|
+
setTimeout(() => notification.remove(), 300);
|
|
1201
|
+
}, 3000);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Add node to tree structure
|
|
1207
|
+
*/
|
|
1208
|
+
addNodeToTree(nodeInfo, parentPath = '') {
|
|
1209
|
+
// CRITICAL: Validate that nodeInfo.path doesn't contain absolute paths
|
|
1210
|
+
// The backend should only send relative paths now
|
|
1211
|
+
if (nodeInfo.path && nodeInfo.path.startsWith('/')) {
|
|
1212
|
+
console.error('Absolute path detected in node, skipping:', nodeInfo.path);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Also validate parent path
|
|
1217
|
+
if (parentPath && parentPath.startsWith('/')) {
|
|
1218
|
+
console.error('Absolute path detected in parent, skipping:', parentPath);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Find parent node
|
|
1223
|
+
let parentNode = this.treeData;
|
|
1224
|
+
|
|
1225
|
+
if (parentPath) {
|
|
1226
|
+
parentNode = this.findNodeByPath(parentPath);
|
|
1227
|
+
if (!parentNode) {
|
|
1228
|
+
// CRITICAL: Do NOT create parent structure if it doesn't exist
|
|
1229
|
+
// This prevents creating nodes above the working directory
|
|
1230
|
+
console.warn('Parent node not found, skipping node creation:', parentPath);
|
|
1231
|
+
console.warn('Attempted to add node:', nodeInfo);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Check if node already exists
|
|
1237
|
+
const existingNode = parentNode.children?.find(c =>
|
|
1238
|
+
c.path === nodeInfo.path ||
|
|
1239
|
+
(c.name === nodeInfo.name && c.type === nodeInfo.type)
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
if (existingNode) {
|
|
1243
|
+
// Update existing node
|
|
1244
|
+
Object.assign(existingNode, nodeInfo);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Add new node
|
|
1249
|
+
if (!parentNode.children) {
|
|
1250
|
+
parentNode.children = [];
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Ensure the node has a children array
|
|
1254
|
+
if (!nodeInfo.children) {
|
|
1255
|
+
nodeInfo.children = [];
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
parentNode.children.push(nodeInfo);
|
|
1259
|
+
|
|
1260
|
+
// Store node reference for quick access
|
|
1261
|
+
this.nodes.set(nodeInfo.path, nodeInfo);
|
|
1262
|
+
|
|
1263
|
+
// Update tree if initialized
|
|
1264
|
+
if (this.root && this.svg) {
|
|
1265
|
+
// Recreate hierarchy with new data
|
|
1266
|
+
this.root = d3.hierarchy(this.treeData);
|
|
1267
|
+
this.root.x0 = this.height / 2;
|
|
1268
|
+
this.root.y0 = 0;
|
|
1269
|
+
|
|
1270
|
+
// Update only if we have a reasonable number of nodes to avoid performance issues
|
|
1271
|
+
if (this.nodes.size < 1000) {
|
|
1272
|
+
this.update(this.root);
|
|
1273
|
+
} else if (this.nodes.size % 100 === 0) {
|
|
1274
|
+
// Update every 100 nodes for large trees
|
|
1275
|
+
this.update(this.root);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Find node by path in tree
|
|
1282
|
+
*/
|
|
1283
|
+
findNodeByPath(path, node = null) {
|
|
1284
|
+
if (!node) {
|
|
1285
|
+
node = this.treeData;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (node.path === path) {
|
|
1289
|
+
return node;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (node.children) {
|
|
1293
|
+
for (const child of node.children) {
|
|
1294
|
+
const found = this.findNodeByPath(path, child);
|
|
1295
|
+
if (found) {
|
|
1296
|
+
return found;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Find D3 hierarchy node by path
|
|
1306
|
+
*/
|
|
1307
|
+
findD3NodeByPath(path) {
|
|
1308
|
+
if (!this.root) return null;
|
|
1309
|
+
return this.root.descendants().find(d => d.data.path === path);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Update statistics display
|
|
1314
|
+
*/
|
|
1315
|
+
updateStats() {
|
|
1316
|
+
// Update stats display - use correct IDs from HTML
|
|
1317
|
+
const statsElements = {
|
|
1318
|
+
'file-count': this.stats.files,
|
|
1319
|
+
'class-count': this.stats.classes,
|
|
1320
|
+
'function-count': this.stats.functions,
|
|
1321
|
+
'line-count': this.stats.lines
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
for (const [id, value] of Object.entries(statsElements)) {
|
|
1325
|
+
const elem = document.getElementById(id);
|
|
1326
|
+
if (elem) {
|
|
1327
|
+
elem.textContent = value.toLocaleString();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Update progress text
|
|
1332
|
+
const progressText = document.getElementById('code-progress-text');
|
|
1333
|
+
if (progressText) {
|
|
1334
|
+
const statusText = this.analyzing ?
|
|
1335
|
+
`Analyzing... ${this.stats.files} files processed` :
|
|
1336
|
+
`Ready - ${this.stats.files} files in tree`;
|
|
1337
|
+
progressText.textContent = statusText;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Update breadcrumb trail
|
|
1343
|
+
*/
|
|
1344
|
+
updateBreadcrumb(message, type = 'info') {
|
|
1345
|
+
const breadcrumbContent = document.getElementById('breadcrumb-content');
|
|
1346
|
+
if (breadcrumbContent) {
|
|
1347
|
+
breadcrumbContent.textContent = message;
|
|
1348
|
+
breadcrumbContent.className = `breadcrumb-${type}`;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Detect language from file extension
|
|
1354
|
+
*/
|
|
1355
|
+
detectLanguage(filePath) {
|
|
1356
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
1357
|
+
const languageMap = {
|
|
1358
|
+
'py': 'python',
|
|
1359
|
+
'js': 'javascript',
|
|
1360
|
+
'ts': 'typescript',
|
|
1361
|
+
'jsx': 'javascript',
|
|
1362
|
+
'tsx': 'typescript',
|
|
1363
|
+
'java': 'java',
|
|
1364
|
+
'cpp': 'cpp',
|
|
1365
|
+
'c': 'c',
|
|
1366
|
+
'cs': 'csharp',
|
|
1367
|
+
'rb': 'ruby',
|
|
1368
|
+
'go': 'go',
|
|
1369
|
+
'rs': 'rust',
|
|
1370
|
+
'php': 'php',
|
|
1371
|
+
'swift': 'swift',
|
|
1372
|
+
'kt': 'kotlin',
|
|
1373
|
+
'scala': 'scala',
|
|
1374
|
+
'r': 'r',
|
|
1375
|
+
'sh': 'bash',
|
|
1376
|
+
'ps1': 'powershell'
|
|
1377
|
+
};
|
|
1378
|
+
return languageMap[ext] || 'unknown';
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Add visualization controls for layout toggle
|
|
1383
|
+
*/
|
|
1384
|
+
addVisualizationControls() {
|
|
1385
|
+
const controls = this.svg.append('g')
|
|
1386
|
+
.attr('class', 'viz-controls')
|
|
1387
|
+
.attr('transform', 'translate(10, 10)');
|
|
1388
|
+
|
|
1389
|
+
// Add layout toggle button
|
|
1390
|
+
const toggleButton = controls.append('g')
|
|
1391
|
+
.attr('class', 'layout-toggle')
|
|
1392
|
+
.style('cursor', 'pointer')
|
|
1393
|
+
.on('click', () => this.toggleLayout());
|
|
1394
|
+
|
|
1395
|
+
toggleButton.append('rect')
|
|
1396
|
+
.attr('width', 120)
|
|
1397
|
+
.attr('height', 30)
|
|
1398
|
+
.attr('rx', 5)
|
|
1399
|
+
.attr('fill', '#3b82f6')
|
|
1400
|
+
.attr('opacity', 0.8);
|
|
1401
|
+
|
|
1402
|
+
toggleButton.append('text')
|
|
1403
|
+
.attr('x', 60)
|
|
1404
|
+
.attr('y', 20)
|
|
1405
|
+
.attr('text-anchor', 'middle')
|
|
1406
|
+
.attr('fill', 'white')
|
|
1407
|
+
.style('font-size', '12px')
|
|
1408
|
+
.text(this.isRadialLayout ? 'Switch to Linear' : 'Switch to Radial');
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Toggle between radial and linear layouts
|
|
1413
|
+
*/
|
|
1414
|
+
toggleLayout() {
|
|
1415
|
+
this.isRadialLayout = !this.isRadialLayout;
|
|
1416
|
+
this.createVisualization();
|
|
1417
|
+
if (this.root) {
|
|
1418
|
+
this.update(this.root);
|
|
1419
|
+
}
|
|
1420
|
+
this.showNotification(
|
|
1421
|
+
this.isRadialLayout ? 'Switched to radial layout' : 'Switched to linear layout',
|
|
1422
|
+
'info'
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Convert radial coordinates to Cartesian
|
|
1428
|
+
*/
|
|
1429
|
+
radialPoint(x, y) {
|
|
1430
|
+
return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Update D3 tree visualization
|
|
1435
|
+
*/
|
|
1436
|
+
update(source) {
|
|
1437
|
+
if (!this.treeLayout || !this.treeGroup || !source) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Compute the new tree layout
|
|
1442
|
+
const treeData = this.treeLayout(this.root);
|
|
1443
|
+
const nodes = treeData.descendants();
|
|
1444
|
+
const links = treeData.descendants().slice(1);
|
|
1445
|
+
|
|
1446
|
+
if (this.isRadialLayout) {
|
|
1447
|
+
// Radial layout adjustments
|
|
1448
|
+
nodes.forEach(d => {
|
|
1449
|
+
// Store original x,y for transitions
|
|
1450
|
+
if (d.x0 === undefined) {
|
|
1451
|
+
d.x0 = d.x;
|
|
1452
|
+
d.y0 = d.y;
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
} else {
|
|
1456
|
+
// Linear layout with nodeSize doesn't need manual normalization
|
|
1457
|
+
// The tree layout handles spacing automatically
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Update nodes
|
|
1461
|
+
const node = this.treeGroup.selectAll('g.node')
|
|
1462
|
+
.data(nodes, d => d.id || (d.id = ++this.nodeId));
|
|
1463
|
+
|
|
1464
|
+
// Enter new nodes
|
|
1465
|
+
const nodeEnter = node.enter().append('g')
|
|
1466
|
+
.attr('class', 'node')
|
|
1467
|
+
.attr('transform', d => {
|
|
1468
|
+
if (this.isRadialLayout) {
|
|
1469
|
+
const [x, y] = this.radialPoint(source.x0 || 0, source.y0 || 0);
|
|
1470
|
+
return `translate(${x},${y})`;
|
|
1471
|
+
} else {
|
|
1472
|
+
return `translate(${source.y0},${source.x0})`;
|
|
1473
|
+
}
|
|
1474
|
+
})
|
|
1475
|
+
.on('click', (event, d) => this.onNodeClick(event, d));
|
|
1476
|
+
|
|
1477
|
+
// Add circles for nodes
|
|
1478
|
+
nodeEnter.append('circle')
|
|
1479
|
+
.attr('class', 'node-circle')
|
|
1480
|
+
.attr('r', 1e-6)
|
|
1481
|
+
.style('fill', d => this.getNodeColor(d))
|
|
1482
|
+
.style('stroke', d => this.getNodeStrokeColor(d))
|
|
1483
|
+
.style('stroke-width', 2)
|
|
1484
|
+
.on('mouseover', (event, d) => this.showTooltip(event, d))
|
|
1485
|
+
.on('mouseout', () => this.hideTooltip());
|
|
1486
|
+
|
|
1487
|
+
// Add labels for nodes with smart positioning
|
|
1488
|
+
nodeEnter.append('text')
|
|
1489
|
+
.attr('class', 'node-label')
|
|
1490
|
+
.attr('dy', '.35em')
|
|
1491
|
+
.attr('x', d => {
|
|
1492
|
+
if (this.isRadialLayout) {
|
|
1493
|
+
// For radial layout, initial position
|
|
1494
|
+
return 0;
|
|
1495
|
+
} else {
|
|
1496
|
+
// Linear layout: standard positioning
|
|
1497
|
+
return d.children || d._children ? -13 : 13;
|
|
1498
|
+
}
|
|
1499
|
+
})
|
|
1500
|
+
.attr('text-anchor', d => {
|
|
1501
|
+
if (this.isRadialLayout) {
|
|
1502
|
+
return 'start'; // Will be adjusted in update
|
|
1503
|
+
} else {
|
|
1504
|
+
// Linear layout: standard anchoring
|
|
1505
|
+
return d.children || d._children ? 'end' : 'start';
|
|
1506
|
+
}
|
|
1507
|
+
})
|
|
1508
|
+
.text(d => {
|
|
1509
|
+
// Truncate long names
|
|
1510
|
+
const maxLength = 20;
|
|
1511
|
+
const name = d.data.name || '';
|
|
1512
|
+
return name.length > maxLength ?
|
|
1513
|
+
name.substring(0, maxLength - 3) + '...' : name;
|
|
1514
|
+
})
|
|
1515
|
+
.style('fill-opacity', 1e-6)
|
|
1516
|
+
.style('font-size', '12px')
|
|
1517
|
+
.style('font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif')
|
|
1518
|
+
.style('text-shadow', '1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8)');
|
|
1519
|
+
|
|
1520
|
+
// Add icons for node types
|
|
1521
|
+
nodeEnter.append('text')
|
|
1522
|
+
.attr('class', 'node-icon')
|
|
1523
|
+
.attr('dy', '.35em')
|
|
1524
|
+
.attr('x', 0)
|
|
1525
|
+
.attr('text-anchor', 'middle')
|
|
1526
|
+
.text(d => this.getNodeIcon(d))
|
|
1527
|
+
.style('font-size', '10px')
|
|
1528
|
+
.style('fill', 'white');
|
|
1529
|
+
|
|
1530
|
+
// Transition to new positions
|
|
1531
|
+
const nodeUpdate = nodeEnter.merge(node);
|
|
1532
|
+
|
|
1533
|
+
nodeUpdate.transition()
|
|
1534
|
+
.duration(this.duration)
|
|
1535
|
+
.attr('transform', d => {
|
|
1536
|
+
if (this.isRadialLayout) {
|
|
1537
|
+
const [x, y] = this.radialPoint(d.x, d.y);
|
|
1538
|
+
return `translate(${x},${y})`;
|
|
1539
|
+
} else {
|
|
1540
|
+
return `translate(${d.y},${d.x})`;
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
nodeUpdate.select('circle.node-circle')
|
|
1545
|
+
.attr('r', 8)
|
|
1546
|
+
.style('fill', d => this.getNodeColor(d))
|
|
1547
|
+
.style('stroke', d => this.getNodeStrokeColor(d))
|
|
1548
|
+
.attr('cursor', 'pointer');
|
|
1549
|
+
|
|
1550
|
+
// Update text labels with proper rotation for radial layout
|
|
1551
|
+
const isRadial = this.isRadialLayout; // Capture the layout type
|
|
1552
|
+
nodeUpdate.select('text.node-label')
|
|
1553
|
+
.style('fill-opacity', 1)
|
|
1554
|
+
.style('fill', '#333')
|
|
1555
|
+
.each(function(d) {
|
|
1556
|
+
const selection = d3.select(this);
|
|
1557
|
+
|
|
1558
|
+
if (isRadial) {
|
|
1559
|
+
// For radial layout, apply rotation and positioning
|
|
1560
|
+
const angle = (d.x * 180 / Math.PI) - 90; // Convert to degrees
|
|
1561
|
+
|
|
1562
|
+
// Determine if text should be flipped (left side of circle)
|
|
1563
|
+
const shouldFlip = angle > 90 || angle < -90;
|
|
1564
|
+
|
|
1565
|
+
// Calculate text position and rotation
|
|
1566
|
+
if (shouldFlip) {
|
|
1567
|
+
// Text on left side - rotate 180 degrees to read properly
|
|
1568
|
+
selection
|
|
1569
|
+
.attr('transform', `rotate(${angle + 180})`)
|
|
1570
|
+
.attr('x', -15) // Negative offset for flipped text
|
|
1571
|
+
.attr('text-anchor', 'end')
|
|
1572
|
+
.attr('dy', '.35em');
|
|
1573
|
+
} else {
|
|
1574
|
+
// Text on right side - normal orientation
|
|
1575
|
+
selection
|
|
1576
|
+
.attr('transform', `rotate(${angle})`)
|
|
1577
|
+
.attr('x', 15) // Positive offset for normal text
|
|
1578
|
+
.attr('text-anchor', 'start')
|
|
1579
|
+
.attr('dy', '.35em');
|
|
1580
|
+
}
|
|
1581
|
+
} else {
|
|
1582
|
+
// Linear layout - no rotation needed
|
|
1583
|
+
selection
|
|
1584
|
+
.attr('transform', null)
|
|
1585
|
+
.attr('x', d.children || d._children ? -13 : 13)
|
|
1586
|
+
.attr('text-anchor', d.children || d._children ? 'end' : 'start')
|
|
1587
|
+
.attr('dy', '.35em');
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
// Remove exiting nodes
|
|
1592
|
+
const nodeExit = node.exit().transition()
|
|
1593
|
+
.duration(this.duration)
|
|
1594
|
+
.attr('transform', d => {
|
|
1595
|
+
if (this.isRadialLayout) {
|
|
1596
|
+
const [x, y] = this.radialPoint(source.x, source.y);
|
|
1597
|
+
return `translate(${x},${y})`;
|
|
1598
|
+
} else {
|
|
1599
|
+
return `translate(${source.y},${source.x})`;
|
|
1600
|
+
}
|
|
1601
|
+
})
|
|
1602
|
+
.remove();
|
|
1603
|
+
|
|
1604
|
+
nodeExit.select('circle')
|
|
1605
|
+
.attr('r', 1e-6);
|
|
1606
|
+
|
|
1607
|
+
nodeExit.select('text.node-label')
|
|
1608
|
+
.style('fill-opacity', 1e-6);
|
|
1609
|
+
|
|
1610
|
+
nodeExit.select('text.node-icon')
|
|
1611
|
+
.style('fill-opacity', 1e-6);
|
|
1612
|
+
|
|
1613
|
+
// Update links
|
|
1614
|
+
const link = this.treeGroup.selectAll('path.link')
|
|
1615
|
+
.data(links, d => d.id);
|
|
1616
|
+
|
|
1617
|
+
// Enter new links
|
|
1618
|
+
const linkEnter = link.enter().insert('path', 'g')
|
|
1619
|
+
.attr('class', 'link')
|
|
1620
|
+
.attr('d', d => {
|
|
1621
|
+
const o = {x: source.x0, y: source.y0};
|
|
1622
|
+
return this.isRadialLayout ?
|
|
1623
|
+
this.radialDiagonal(o, o) :
|
|
1624
|
+
this.diagonal(o, o);
|
|
1625
|
+
})
|
|
1626
|
+
.style('fill', 'none')
|
|
1627
|
+
.style('stroke', '#ccc')
|
|
1628
|
+
.style('stroke-width', 2);
|
|
1629
|
+
|
|
1630
|
+
// Transition to new positions
|
|
1631
|
+
const linkUpdate = linkEnter.merge(link);
|
|
1632
|
+
|
|
1633
|
+
linkUpdate.transition()
|
|
1634
|
+
.duration(this.duration)
|
|
1635
|
+
.attr('d', d => this.isRadialLayout ?
|
|
1636
|
+
this.radialDiagonal(d, d.parent) :
|
|
1637
|
+
this.diagonal(d, d.parent));
|
|
1638
|
+
|
|
1639
|
+
// Remove exiting links
|
|
1640
|
+
link.exit().transition()
|
|
1641
|
+
.duration(this.duration)
|
|
1642
|
+
.attr('d', d => {
|
|
1643
|
+
const o = {x: source.x, y: source.y};
|
|
1644
|
+
return this.isRadialLayout ?
|
|
1645
|
+
this.radialDiagonal(o, o) :
|
|
1646
|
+
this.diagonal(o, o);
|
|
1647
|
+
})
|
|
1648
|
+
.remove();
|
|
1649
|
+
|
|
1650
|
+
// Store old positions for transition
|
|
1651
|
+
nodes.forEach(d => {
|
|
1652
|
+
d.x0 = d.x;
|
|
1653
|
+
d.y0 = d.y;
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Center the view on a specific node (Linear layout)
|
|
1659
|
+
*/
|
|
1660
|
+
centerOnNode(d) {
|
|
1661
|
+
if (!this.svg || !this.zoom) return;
|
|
1662
|
+
|
|
1663
|
+
// Get current transform or use default zoom level
|
|
1664
|
+
const currentTransform = d3.zoomTransform(this.svg.node());
|
|
1665
|
+
// Zoom in to 2x for better focus on clicked node
|
|
1666
|
+
const targetScale = currentTransform.k < 2 ? 2 : currentTransform.k;
|
|
1667
|
+
|
|
1668
|
+
// Calculate position to center the node
|
|
1669
|
+
const x = -d.y * targetScale + this.width / 2;
|
|
1670
|
+
const y = -d.x * targetScale + this.height / 2;
|
|
1671
|
+
|
|
1672
|
+
this.svg.transition()
|
|
1673
|
+
.duration(750)
|
|
1674
|
+
.call(
|
|
1675
|
+
this.zoom.transform,
|
|
1676
|
+
d3.zoomIdentity
|
|
1677
|
+
.translate(x, y)
|
|
1678
|
+
.scale(targetScale)
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Center the view on a specific node (Radial layout)
|
|
1684
|
+
*/
|
|
1685
|
+
centerOnNodeRadial(d) {
|
|
1686
|
+
if (!this.svg || !this.zoom) return;
|
|
1687
|
+
|
|
1688
|
+
// Use the same radialPoint function for consistency
|
|
1689
|
+
const [x, y] = this.radialPoint(d.x, d.y);
|
|
1690
|
+
|
|
1691
|
+
// Get current transform or use default zoom level
|
|
1692
|
+
const currentTransform = d3.zoomTransform(this.svg.node());
|
|
1693
|
+
// Zoom in to 2x for better focus on clicked node
|
|
1694
|
+
const targetScale = currentTransform.k < 2 ? 2 : currentTransform.k;
|
|
1695
|
+
|
|
1696
|
+
// Calculate translation to center the node
|
|
1697
|
+
// The tree is already centered at width/2, height/2 via transform
|
|
1698
|
+
// So we need to adjust relative to that center
|
|
1699
|
+
const targetX = this.width / 2 - x * targetScale;
|
|
1700
|
+
const targetY = this.height / 2 - y * targetScale;
|
|
1701
|
+
|
|
1702
|
+
// Apply smooth transition to center the node with zoom
|
|
1703
|
+
this.svg.transition()
|
|
1704
|
+
.duration(750)
|
|
1705
|
+
.call(
|
|
1706
|
+
this.zoom.transform,
|
|
1707
|
+
d3.zoomIdentity
|
|
1708
|
+
.translate(targetX, targetY)
|
|
1709
|
+
.scale(targetScale)
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Highlight the active node with larger icon
|
|
1715
|
+
*/
|
|
1716
|
+
highlightActiveNode(d) {
|
|
1717
|
+
// Reset all nodes to normal size and clear parent context
|
|
1718
|
+
// First clear classes on the selection
|
|
1719
|
+
const allCircles = this.treeGroup.selectAll('circle.node-circle');
|
|
1720
|
+
allCircles
|
|
1721
|
+
.classed('active', false)
|
|
1722
|
+
.classed('parent-context', false);
|
|
1723
|
+
|
|
1724
|
+
// Then apply transition separately
|
|
1725
|
+
allCircles
|
|
1726
|
+
.transition()
|
|
1727
|
+
.duration(300)
|
|
1728
|
+
.attr('r', 8)
|
|
1729
|
+
.style('stroke', null)
|
|
1730
|
+
.style('stroke-width', null)
|
|
1731
|
+
.style('opacity', null);
|
|
1732
|
+
|
|
1733
|
+
// Reset all labels to normal
|
|
1734
|
+
this.treeGroup.selectAll('text.node-label')
|
|
1735
|
+
.style('font-weight', 'normal')
|
|
1736
|
+
.style('font-size', '12px');
|
|
1737
|
+
|
|
1738
|
+
// Find and increase size of clicked node - use data matching
|
|
1739
|
+
// Make the size increase MUCH more dramatic: 8 -> 20 (2.5x the size)
|
|
1740
|
+
const activeNodeCircle = this.treeGroup.selectAll('g.node')
|
|
1741
|
+
.filter(node => node === d)
|
|
1742
|
+
.select('circle.node-circle');
|
|
1743
|
+
|
|
1744
|
+
// First set the class (not part of transition)
|
|
1745
|
+
activeNodeCircle.classed('active', true);
|
|
1746
|
+
|
|
1747
|
+
// Then apply the transition with styles - MUCH LARGER
|
|
1748
|
+
activeNodeCircle
|
|
1749
|
+
.transition()
|
|
1750
|
+
.duration(300)
|
|
1751
|
+
.attr('r', 20) // Much larger radius (2.5x)
|
|
1752
|
+
.style('stroke', '#3b82f6')
|
|
1753
|
+
.style('stroke-width', 5) // Thicker border
|
|
1754
|
+
.style('filter', 'drop-shadow(0 0 15px rgba(59, 130, 246, 0.6))'); // Stronger glow effect
|
|
1755
|
+
|
|
1756
|
+
// Also make the label bold
|
|
1757
|
+
this.treeGroup.selectAll('g.node')
|
|
1758
|
+
.filter(node => node === d)
|
|
1759
|
+
.select('text.node-label')
|
|
1760
|
+
.style('font-weight', 'bold')
|
|
1761
|
+
.style('font-size', '14px'); // Slightly larger text
|
|
1762
|
+
|
|
1763
|
+
// Store active node
|
|
1764
|
+
this.activeNode = d;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Add pulsing animation for loading state
|
|
1769
|
+
*/
|
|
1770
|
+
addLoadingPulse(d) {
|
|
1771
|
+
// Use consistent selection pattern
|
|
1772
|
+
const node = this.treeGroup.selectAll('g.node')
|
|
1773
|
+
.filter(node => node === d)
|
|
1774
|
+
.select('circle.node-circle');
|
|
1775
|
+
|
|
1776
|
+
// Add to loading set
|
|
1777
|
+
this.loadingNodes.add(d.data.path);
|
|
1778
|
+
|
|
1779
|
+
// Add pulsing class and orange color - separate operations
|
|
1780
|
+
node.classed('loading-pulse', true);
|
|
1781
|
+
node.style('fill', '#fb923c'); // Orange color for loading
|
|
1782
|
+
|
|
1783
|
+
// Create pulse animation
|
|
1784
|
+
const pulseAnimation = () => {
|
|
1785
|
+
if (!this.loadingNodes.has(d.data.path)) return;
|
|
1786
|
+
|
|
1787
|
+
node.transition()
|
|
1788
|
+
.duration(600)
|
|
1789
|
+
.attr('r', 14)
|
|
1790
|
+
.style('opacity', 0.6)
|
|
1791
|
+
.transition()
|
|
1792
|
+
.duration(600)
|
|
1793
|
+
.attr('r', 10)
|
|
1794
|
+
.style('opacity', 1)
|
|
1795
|
+
.on('end', () => {
|
|
1796
|
+
if (this.loadingNodes.has(d.data.path)) {
|
|
1797
|
+
pulseAnimation(); // Continue pulsing
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
};
|
|
1801
|
+
|
|
1802
|
+
pulseAnimation();
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Remove pulsing animation when loading complete
|
|
1807
|
+
*/
|
|
1808
|
+
removeLoadingPulse(d) {
|
|
1809
|
+
// Remove from loading set
|
|
1810
|
+
this.loadingNodes.delete(d.data.path);
|
|
1811
|
+
|
|
1812
|
+
// Use consistent selection pattern
|
|
1813
|
+
const node = this.treeGroup.selectAll('g.node')
|
|
1814
|
+
.filter(node => node === d)
|
|
1815
|
+
.select('circle.node-circle');
|
|
1816
|
+
|
|
1817
|
+
// Clear class first
|
|
1818
|
+
node.classed('loading-pulse', false);
|
|
1819
|
+
|
|
1820
|
+
// Then interrupt and transition
|
|
1821
|
+
node.interrupt() // Stop animation
|
|
1822
|
+
.transition()
|
|
1823
|
+
.duration(300)
|
|
1824
|
+
.attr('r', this.activeNode === d ? 20 : 8) // Use 20 for active node
|
|
1825
|
+
.style('opacity', 1)
|
|
1826
|
+
.style('fill', d => this.getNodeColor(d)); // Restore original color
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* Show parent node alongside for context
|
|
1831
|
+
*/
|
|
1832
|
+
showWithParent(d) {
|
|
1833
|
+
if (!d.parent) return;
|
|
1834
|
+
|
|
1835
|
+
// Make parent more visible
|
|
1836
|
+
const parentNode = this.treeGroup.selectAll('g.node')
|
|
1837
|
+
.filter(node => node === d.parent);
|
|
1838
|
+
|
|
1839
|
+
// Highlight parent with different style - separate class from styles
|
|
1840
|
+
const parentCircle = parentNode.select('circle.node-circle');
|
|
1841
|
+
parentCircle.classed('parent-context', true);
|
|
1842
|
+
parentCircle
|
|
1843
|
+
.style('stroke', '#10b981')
|
|
1844
|
+
.style('stroke-width', 3)
|
|
1845
|
+
.style('opacity', 0.8);
|
|
1846
|
+
|
|
1847
|
+
// For radial, adjust zoom to show both parent and clicked node
|
|
1848
|
+
if (this.isRadialLayout && d.parent) {
|
|
1849
|
+
// Calculate bounding box including parent and immediate children
|
|
1850
|
+
const nodes = [d, d.parent];
|
|
1851
|
+
if (d.children) nodes.push(...d.children);
|
|
1852
|
+
else if (d._children) nodes.push(...d._children);
|
|
1853
|
+
|
|
1854
|
+
const angles = nodes.map(n => n.x);
|
|
1855
|
+
const radii = nodes.map(n => n.y);
|
|
1856
|
+
|
|
1857
|
+
const minAngle = Math.min(...angles);
|
|
1858
|
+
const maxAngle = Math.max(...angles);
|
|
1859
|
+
const maxRadius = Math.max(...radii);
|
|
1860
|
+
|
|
1861
|
+
// Zoom to fit parent and children
|
|
1862
|
+
const angleSpan = maxAngle - minAngle;
|
|
1863
|
+
const scale = Math.min(
|
|
1864
|
+
angleSpan > 0 ? (Math.PI * 2) / (angleSpan * 2) : 2.5, // Fit angle span
|
|
1865
|
+
this.width / (2 * maxRadius), // Fit radius
|
|
1866
|
+
2.5 // Max zoom
|
|
1867
|
+
);
|
|
1868
|
+
|
|
1869
|
+
// Calculate center angle and radius
|
|
1870
|
+
const centerAngle = (minAngle + maxAngle) / 2;
|
|
1871
|
+
const centerRadius = maxRadius / 2;
|
|
1872
|
+
const centerX = centerRadius * Math.cos(centerAngle - Math.PI / 2);
|
|
1873
|
+
const centerY = centerRadius * Math.sin(centerAngle - Math.PI / 2);
|
|
1874
|
+
|
|
1875
|
+
this.svg.transition()
|
|
1876
|
+
.duration(750)
|
|
1877
|
+
.call(
|
|
1878
|
+
this.zoom.transform,
|
|
1879
|
+
d3.zoomIdentity
|
|
1880
|
+
.translate(this.width / 2 - centerX * scale, this.height / 2 - centerY * scale)
|
|
1881
|
+
.scale(scale)
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* Handle node click - implement lazy loading with enhanced visual feedback
|
|
1888
|
+
*/
|
|
1889
|
+
onNodeClick(event, d) {
|
|
1890
|
+
event.stopPropagation();
|
|
1891
|
+
|
|
1892
|
+
console.log('[CodeTree] Node clicked:', d.data.name, d.data.type);
|
|
1893
|
+
|
|
1894
|
+
// === PHASE 1: Immediate Visual Effects (Synchronous) ===
|
|
1895
|
+
// These execute immediately before any async operations
|
|
1896
|
+
|
|
1897
|
+
// Center on clicked node (immediate visual effect)
|
|
1898
|
+
if (this.isRadialLayout) {
|
|
1899
|
+
this.centerOnNodeRadial(d);
|
|
1900
|
+
} else {
|
|
1901
|
+
this.centerOnNode(d);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// Highlight with larger icon (immediate visual effect)
|
|
1905
|
+
this.highlightActiveNode(d);
|
|
1906
|
+
|
|
1907
|
+
// Show parent context (immediate visual effect)
|
|
1908
|
+
this.showWithParent(d);
|
|
1909
|
+
|
|
1910
|
+
// Add pulsing animation immediately for directories
|
|
1911
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
1912
|
+
console.log('[CodeTree] Adding pulsing animation for:', d.data.name);
|
|
1913
|
+
this.addLoadingPulse(d);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// === PHASE 2: Prepare Data (Synchronous) ===
|
|
1917
|
+
|
|
1918
|
+
// Get selected languages from checkboxes
|
|
1919
|
+
const selectedLanguages = [];
|
|
1920
|
+
document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
|
|
1921
|
+
selectedLanguages.push(cb.value);
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
// Get ignore patterns
|
|
1925
|
+
const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
|
|
1926
|
+
|
|
1927
|
+
// === PHASE 3: Async Operations (Delayed) ===
|
|
1928
|
+
// Add a small delay to ensure visual effects are rendered first
|
|
1929
|
+
|
|
1930
|
+
// For directories that haven't been loaded yet, request discovery
|
|
1931
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
1932
|
+
// Mark as loading immediately to prevent duplicate requests
|
|
1933
|
+
d.data.loaded = 'loading';
|
|
1934
|
+
|
|
1935
|
+
// Ensure path is absolute or relative to working directory
|
|
1936
|
+
const fullPath = this.ensureFullPath(d.data.path);
|
|
1937
|
+
|
|
1938
|
+
// Delay the socket request to ensure visual effects are rendered
|
|
1939
|
+
setTimeout(() => {
|
|
1940
|
+
console.log('[CodeTree] Sending discovery request for:', fullPath);
|
|
1941
|
+
|
|
1942
|
+
// Request directory contents via Socket.IO
|
|
1943
|
+
if (this.socket) {
|
|
1944
|
+
this.socket.emit('code:discover:directory', {
|
|
1945
|
+
path: fullPath,
|
|
1946
|
+
depth: 1, // Only get immediate children
|
|
1947
|
+
languages: selectedLanguages,
|
|
1948
|
+
ignore_patterns: ignorePatterns
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
this.updateBreadcrumb(`Loading ${d.data.name}...`, 'info');
|
|
1952
|
+
this.showNotification(`Loading directory: ${d.data.name}`, 'info');
|
|
1953
|
+
}
|
|
1954
|
+
}, 100); // 100ms delay to ensure visual effects render first
|
|
1955
|
+
}
|
|
1956
|
+
// For files that haven't been analyzed, request analysis
|
|
1957
|
+
else if (d.data.type === 'file' && !d.data.analyzed) {
|
|
1958
|
+
// Only analyze files of selected languages
|
|
1959
|
+
const fileLanguage = this.detectLanguage(d.data.path);
|
|
1960
|
+
if (!selectedLanguages.includes(fileLanguage) && fileLanguage !== 'unknown') {
|
|
1961
|
+
this.showNotification(`Skipping ${d.data.name} - ${fileLanguage} not selected`, 'warning');
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Add pulsing animation immediately
|
|
1966
|
+
console.log('[CodeTree] Adding pulsing animation for file:', d.data.name);
|
|
1967
|
+
this.addLoadingPulse(d);
|
|
1968
|
+
|
|
1969
|
+
// Mark as loading immediately
|
|
1970
|
+
d.data.analyzed = 'loading';
|
|
1971
|
+
|
|
1972
|
+
// Ensure path is absolute or relative to working directory
|
|
1973
|
+
const fullPath = this.ensureFullPath(d.data.path);
|
|
1974
|
+
|
|
1975
|
+
// Delay the socket request to ensure visual effects are rendered
|
|
1976
|
+
setTimeout(() => {
|
|
1977
|
+
console.log('[CodeTree] Sending analysis request for:', fullPath);
|
|
1978
|
+
|
|
1979
|
+
if (this.socket) {
|
|
1980
|
+
this.socket.emit('code:analyze:file', {
|
|
1981
|
+
path: fullPath
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
|
|
1985
|
+
this.showNotification(`Analyzing: ${d.data.name}`, 'info');
|
|
1986
|
+
}
|
|
1987
|
+
}, 100); // 100ms delay to ensure visual effects render first
|
|
1988
|
+
}
|
|
1989
|
+
// Toggle children visibility for already loaded nodes
|
|
1990
|
+
else if (d.children || d._children) {
|
|
1991
|
+
if (d.children) {
|
|
1992
|
+
d._children = d.children;
|
|
1993
|
+
d.children = null;
|
|
1994
|
+
d.data.expanded = false;
|
|
1995
|
+
} else {
|
|
1996
|
+
d.children = d._children;
|
|
1997
|
+
d._children = null;
|
|
1998
|
+
d.data.expanded = true;
|
|
1999
|
+
}
|
|
2000
|
+
this.update(d);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Update selection
|
|
2004
|
+
this.selectedNode = d;
|
|
2005
|
+
this.highlightNode(d);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Ensure path is absolute or relative to working directory
|
|
2010
|
+
*/
|
|
2011
|
+
ensureFullPath(path) {
|
|
2012
|
+
if (!path) return path;
|
|
2013
|
+
|
|
2014
|
+
// If already absolute, return as is
|
|
2015
|
+
if (path.startsWith('/')) {
|
|
2016
|
+
return path;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Get working directory
|
|
2020
|
+
const workingDir = this.getWorkingDirectory();
|
|
2021
|
+
if (!workingDir) {
|
|
2022
|
+
return path;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// If path is relative, make it relative to working directory
|
|
2026
|
+
if (path === '.' || path === workingDir) {
|
|
2027
|
+
return workingDir;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Combine working directory with relative path
|
|
2031
|
+
return `${workingDir}/${path}`.replace(/\/+/g, '/');
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Highlight selected node
|
|
2036
|
+
*/
|
|
2037
|
+
highlightNode(node) {
|
|
2038
|
+
// Remove previous highlights
|
|
2039
|
+
this.treeGroup.selectAll('circle.node-circle')
|
|
2040
|
+
.style('stroke-width', 2)
|
|
2041
|
+
.classed('selected', false);
|
|
2042
|
+
|
|
2043
|
+
// Highlight selected node
|
|
2044
|
+
this.treeGroup.selectAll('circle.node-circle')
|
|
2045
|
+
.filter(d => d === node)
|
|
2046
|
+
.style('stroke-width', 4)
|
|
2047
|
+
.classed('selected', true);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/**
|
|
2051
|
+
* Create diagonal path for links
|
|
2052
|
+
*/
|
|
2053
|
+
diagonal(s, d) {
|
|
2054
|
+
return `M ${s.y} ${s.x}
|
|
2055
|
+
C ${(s.y + d.y) / 2} ${s.x},
|
|
2056
|
+
${(s.y + d.y) / 2} ${d.x},
|
|
2057
|
+
${d.y} ${d.x}`;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* Create radial diagonal path for links
|
|
2062
|
+
*/
|
|
2063
|
+
radialDiagonal(s, d) {
|
|
2064
|
+
const path = d3.linkRadial()
|
|
2065
|
+
.angle(d => d.x)
|
|
2066
|
+
.radius(d => d.y);
|
|
2067
|
+
return path({source: s, target: d});
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/**
|
|
2071
|
+
* Get node color based on type and complexity
|
|
2072
|
+
*/
|
|
2073
|
+
getNodeColor(d) {
|
|
2074
|
+
const type = d.data.type;
|
|
2075
|
+
const complexity = d.data.complexity || 1;
|
|
2076
|
+
|
|
2077
|
+
// Base colors by type
|
|
2078
|
+
const baseColors = {
|
|
2079
|
+
'root': '#6B7280',
|
|
2080
|
+
'directory': '#3B82F6',
|
|
2081
|
+
'file': '#10B981',
|
|
2082
|
+
'module': '#8B5CF6',
|
|
2083
|
+
'class': '#F59E0B',
|
|
2084
|
+
'function': '#EF4444',
|
|
2085
|
+
'method': '#EC4899'
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
const baseColor = baseColors[type] || '#6B7280';
|
|
2089
|
+
|
|
2090
|
+
// Adjust brightness based on complexity (higher complexity = darker)
|
|
2091
|
+
if (complexity > 10) {
|
|
2092
|
+
return d3.color(baseColor).darker(0.5);
|
|
2093
|
+
} else if (complexity > 5) {
|
|
2094
|
+
return d3.color(baseColor).darker(0.25);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
return baseColor;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* Get node stroke color
|
|
2102
|
+
*/
|
|
2103
|
+
getNodeStrokeColor(d) {
|
|
2104
|
+
if (d.data.loaded === 'loading' || d.data.analyzed === 'loading') {
|
|
2105
|
+
return '#FCD34D'; // Yellow for loading
|
|
2106
|
+
}
|
|
2107
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2108
|
+
return '#94A3B8'; // Gray for unloaded
|
|
2109
|
+
}
|
|
2110
|
+
if (d.data.type === 'file' && !d.data.analyzed) {
|
|
2111
|
+
return '#CBD5E1'; // Light gray for unanalyzed
|
|
2112
|
+
}
|
|
2113
|
+
return this.getNodeColor(d);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* Get icon for node type
|
|
2118
|
+
*/
|
|
2119
|
+
getNodeIcon(d) {
|
|
2120
|
+
const icons = {
|
|
2121
|
+
'root': '📦',
|
|
2122
|
+
'directory': '📁',
|
|
2123
|
+
'file': '📄',
|
|
2124
|
+
'module': '📦',
|
|
2125
|
+
'class': 'C',
|
|
2126
|
+
'function': 'ƒ',
|
|
2127
|
+
'method': 'm'
|
|
2128
|
+
};
|
|
2129
|
+
return icons[d.data.type] || '•';
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/**
|
|
2133
|
+
* Show tooltip on hover
|
|
2134
|
+
*/
|
|
2135
|
+
showTooltip(event, d) {
|
|
2136
|
+
if (!this.tooltip) return;
|
|
2137
|
+
|
|
2138
|
+
const info = [];
|
|
2139
|
+
info.push(`<strong>${d.data.name}</strong>`);
|
|
2140
|
+
info.push(`Type: ${d.data.type}`);
|
|
2141
|
+
|
|
2142
|
+
if (d.data.language) {
|
|
2143
|
+
info.push(`Language: ${d.data.language}`);
|
|
2144
|
+
}
|
|
2145
|
+
if (d.data.complexity) {
|
|
2146
|
+
info.push(`Complexity: ${d.data.complexity}`);
|
|
2147
|
+
}
|
|
2148
|
+
if (d.data.lines) {
|
|
2149
|
+
info.push(`Lines: ${d.data.lines}`);
|
|
2150
|
+
}
|
|
2151
|
+
if (d.data.path) {
|
|
2152
|
+
info.push(`Path: ${d.data.path}`);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// Special messages for lazy-loaded nodes
|
|
2156
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2157
|
+
info.push('<em>Click to explore contents</em>');
|
|
2158
|
+
} else if (d.data.type === 'file' && !d.data.analyzed) {
|
|
2159
|
+
info.push('<em>Click to analyze file</em>');
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
this.tooltip.transition()
|
|
2163
|
+
.duration(200)
|
|
2164
|
+
.style('opacity', .9);
|
|
2165
|
+
|
|
2166
|
+
this.tooltip.html(info.join('<br>'))
|
|
2167
|
+
.style('left', (event.pageX + 10) + 'px')
|
|
2168
|
+
.style('top', (event.pageY - 28) + 'px');
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Hide tooltip
|
|
2173
|
+
*/
|
|
2174
|
+
hideTooltip() {
|
|
2175
|
+
if (!this.tooltip) return;
|
|
2176
|
+
|
|
2177
|
+
this.tooltip.transition()
|
|
2178
|
+
.duration(500)
|
|
2179
|
+
.style('opacity', 0);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Filter tree based on language and search
|
|
2184
|
+
*/
|
|
2185
|
+
filterTree() {
|
|
2186
|
+
if (!this.root) return;
|
|
2187
|
+
|
|
2188
|
+
// Apply filters
|
|
2189
|
+
this.root.descendants().forEach(d => {
|
|
2190
|
+
d.data._hidden = false;
|
|
2191
|
+
|
|
2192
|
+
// Language filter
|
|
2193
|
+
if (this.languageFilter !== 'all') {
|
|
2194
|
+
if (d.data.type === 'file' && d.data.language !== this.languageFilter) {
|
|
2195
|
+
d.data._hidden = true;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// Search filter
|
|
2200
|
+
if (this.searchTerm) {
|
|
2201
|
+
if (!d.data.name.toLowerCase().includes(this.searchTerm)) {
|
|
2202
|
+
d.data._hidden = true;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
// Update display
|
|
2208
|
+
this.update(this.root);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* Expand all nodes in the tree
|
|
2213
|
+
*/
|
|
2214
|
+
expandAll() {
|
|
2215
|
+
if (!this.root) return;
|
|
2216
|
+
|
|
2217
|
+
// Recursively expand all nodes
|
|
2218
|
+
const expandRecursive = (node) => {
|
|
2219
|
+
if (node._children) {
|
|
2220
|
+
node.children = node._children;
|
|
2221
|
+
node._children = null;
|
|
2222
|
+
}
|
|
2223
|
+
if (node.children) {
|
|
2224
|
+
node.children.forEach(expandRecursive);
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
expandRecursive(this.root);
|
|
2229
|
+
this.update(this.root);
|
|
2230
|
+
this.showNotification('All nodes expanded', 'info');
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Collapse all nodes in the tree
|
|
2235
|
+
*/
|
|
2236
|
+
collapseAll() {
|
|
2237
|
+
if (!this.root) return;
|
|
2238
|
+
|
|
2239
|
+
// Recursively collapse all nodes except root
|
|
2240
|
+
const collapseRecursive = (node) => {
|
|
2241
|
+
if (node.children) {
|
|
2242
|
+
node._children = node.children;
|
|
2243
|
+
node.children = null;
|
|
2244
|
+
}
|
|
2245
|
+
if (node._children) {
|
|
2246
|
+
node._children.forEach(collapseRecursive);
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
this.root.children?.forEach(collapseRecursive);
|
|
2251
|
+
this.update(this.root);
|
|
2252
|
+
this.showNotification('All nodes collapsed', 'info');
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Reset zoom to fit the tree
|
|
2257
|
+
*/
|
|
2258
|
+
resetZoom() {
|
|
2259
|
+
if (!this.svg || !this.zoom) return;
|
|
2260
|
+
|
|
2261
|
+
// Reset to identity transform for radial layout (centered)
|
|
2262
|
+
this.svg.transition()
|
|
2263
|
+
.duration(750)
|
|
2264
|
+
.call(
|
|
2265
|
+
this.zoom.transform,
|
|
2266
|
+
d3.zoomIdentity
|
|
2267
|
+
);
|
|
2268
|
+
|
|
2269
|
+
this.showNotification('Zoom reset', 'info');
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/**
|
|
2273
|
+
* Focus on a specific node and its subtree
|
|
2274
|
+
*/
|
|
2275
|
+
focusOnNode(node) {
|
|
2276
|
+
if (!this.svg || !this.zoom || !node) return;
|
|
2277
|
+
|
|
2278
|
+
// Get all descendants of this node
|
|
2279
|
+
const descendants = node.descendants ? node.descendants() : [node];
|
|
2280
|
+
|
|
2281
|
+
if (this.isRadialLayout) {
|
|
2282
|
+
// For radial layout, calculate the bounding box in polar coordinates
|
|
2283
|
+
const angles = descendants.map(d => d.x);
|
|
2284
|
+
const radii = descendants.map(d => d.y);
|
|
2285
|
+
|
|
2286
|
+
const minAngle = Math.min(...angles);
|
|
2287
|
+
const maxAngle = Math.max(...angles);
|
|
2288
|
+
const minRadius = Math.min(...radii);
|
|
2289
|
+
const maxRadius = Math.max(...radii);
|
|
2290
|
+
|
|
2291
|
+
// Convert polar bounds to Cartesian for centering
|
|
2292
|
+
const centerAngle = (minAngle + maxAngle) / 2;
|
|
2293
|
+
const centerRadius = (minRadius + maxRadius) / 2;
|
|
2294
|
+
|
|
2295
|
+
// Convert to Cartesian coordinates
|
|
2296
|
+
const centerX = centerRadius * Math.cos(centerAngle - Math.PI / 2);
|
|
2297
|
+
const centerY = centerRadius * Math.sin(centerAngle - Math.PI / 2);
|
|
2298
|
+
|
|
2299
|
+
// Calculate the span for zoom scale
|
|
2300
|
+
const angleSpan = maxAngle - minAngle;
|
|
2301
|
+
const radiusSpan = maxRadius - minRadius;
|
|
2302
|
+
|
|
2303
|
+
// Calculate scale to fit the subtree
|
|
2304
|
+
// Use angle span to determine scale (radial layout specific)
|
|
2305
|
+
let scale = 1;
|
|
2306
|
+
if (angleSpan > 0 && radiusSpan > 0) {
|
|
2307
|
+
// Scale based on the larger dimension
|
|
2308
|
+
const angleFactor = Math.PI * 2 / angleSpan; // Full circle / angle span
|
|
2309
|
+
const radiusFactor = this.radius / radiusSpan;
|
|
2310
|
+
scale = Math.min(angleFactor, radiusFactor, 3); // Max zoom of 3x
|
|
2311
|
+
scale = Math.max(scale, 1); // Min zoom of 1x
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// Animate the zoom and center
|
|
2315
|
+
this.svg.transition()
|
|
2316
|
+
.duration(750)
|
|
2317
|
+
.call(
|
|
2318
|
+
this.zoom.transform,
|
|
2319
|
+
d3.zoomIdentity
|
|
2320
|
+
.translate(this.width/2 - centerX * scale, this.height/2 - centerY * scale)
|
|
2321
|
+
.scale(scale)
|
|
2322
|
+
);
|
|
2323
|
+
|
|
2324
|
+
} else {
|
|
2325
|
+
// For linear/tree layout
|
|
2326
|
+
const xValues = descendants.map(d => d.x);
|
|
2327
|
+
const yValues = descendants.map(d => d.y);
|
|
2328
|
+
|
|
2329
|
+
const minX = Math.min(...xValues);
|
|
2330
|
+
const maxX = Math.max(...xValues);
|
|
2331
|
+
const minY = Math.min(...yValues);
|
|
2332
|
+
const maxY = Math.max(...yValues);
|
|
2333
|
+
|
|
2334
|
+
// Calculate center
|
|
2335
|
+
const centerX = (minX + maxX) / 2;
|
|
2336
|
+
const centerY = (minY + maxY) / 2;
|
|
2337
|
+
|
|
2338
|
+
// Calculate bounds
|
|
2339
|
+
const width = maxX - minX;
|
|
2340
|
+
const height = maxY - minY;
|
|
2341
|
+
|
|
2342
|
+
// Calculate scale to fit
|
|
2343
|
+
const padding = 100;
|
|
2344
|
+
let scale = 1;
|
|
2345
|
+
if (width > 0 && height > 0) {
|
|
2346
|
+
const scaleX = (this.width - padding) / width;
|
|
2347
|
+
const scaleY = (this.height - padding) / height;
|
|
2348
|
+
scale = Math.min(scaleX, scaleY, 2.5); // Max zoom of 2.5x
|
|
2349
|
+
scale = Math.max(scale, 0.5); // Min zoom of 0.5x
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// Animate zoom to focus
|
|
2353
|
+
this.svg.transition()
|
|
2354
|
+
.duration(750)
|
|
2355
|
+
.call(
|
|
2356
|
+
this.zoom.transform,
|
|
2357
|
+
d3.zoomIdentity
|
|
2358
|
+
.translate(this.width/2 - centerX * scale, this.height/2 - centerY * scale)
|
|
2359
|
+
.scale(scale)
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// Update breadcrumb with focused path
|
|
2364
|
+
const path = this.getNodePath(node);
|
|
2365
|
+
this.updateBreadcrumb(`Focused: ${path}`, 'info');
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
/**
|
|
2369
|
+
* Get the full path of a node
|
|
2370
|
+
*/
|
|
2371
|
+
getNodePath(node) {
|
|
2372
|
+
const path = [];
|
|
2373
|
+
let current = node;
|
|
2374
|
+
while (current) {
|
|
2375
|
+
if (current.data && current.data.name) {
|
|
2376
|
+
path.unshift(current.data.name);
|
|
2377
|
+
}
|
|
2378
|
+
current = current.parent;
|
|
2379
|
+
}
|
|
2380
|
+
return path.join(' / ');
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Toggle legend visibility
|
|
2385
|
+
*/
|
|
2386
|
+
toggleLegend() {
|
|
2387
|
+
const legend = document.getElementById('tree-legend');
|
|
2388
|
+
if (legend) {
|
|
2389
|
+
if (legend.style.display === 'none') {
|
|
2390
|
+
legend.style.display = 'block';
|
|
2391
|
+
} else {
|
|
2392
|
+
legend.style.display = 'none';
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* Get the current working directory
|
|
2399
|
+
*/
|
|
2400
|
+
getWorkingDirectory() {
|
|
2401
|
+
// Try to get from dashboard's working directory manager
|
|
2402
|
+
if (window.dashboard && window.dashboard.workingDirectoryManager) {
|
|
2403
|
+
return window.dashboard.workingDirectoryManager.getCurrentWorkingDir();
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// Fallback to checking the DOM element
|
|
2407
|
+
const workingDirPath = document.getElementById('working-dir-path');
|
|
2408
|
+
if (workingDirPath) {
|
|
2409
|
+
const pathText = workingDirPath.textContent.trim();
|
|
2410
|
+
if (pathText && pathText !== 'Loading...' && pathText !== 'Not selected') {
|
|
2411
|
+
return pathText;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* Show a message when no working directory is selected
|
|
2420
|
+
*/
|
|
2421
|
+
showNoWorkingDirectoryMessage() {
|
|
2422
|
+
const container = document.getElementById('code-tree-container');
|
|
2423
|
+
if (!container) return;
|
|
2424
|
+
|
|
2425
|
+
// Remove any existing message
|
|
2426
|
+
this.removeNoWorkingDirectoryMessage();
|
|
2427
|
+
|
|
2428
|
+
// Hide loading if shown
|
|
2429
|
+
this.hideLoading();
|
|
2430
|
+
|
|
2431
|
+
// Create message element
|
|
2432
|
+
const messageDiv = document.createElement('div');
|
|
2433
|
+
messageDiv.id = 'no-working-dir-message';
|
|
2434
|
+
messageDiv.className = 'no-working-dir-message';
|
|
2435
|
+
messageDiv.innerHTML = `
|
|
2436
|
+
<div class="message-icon">📁</div>
|
|
2437
|
+
<h3>No Working Directory Selected</h3>
|
|
2438
|
+
<p>Please select a working directory from the top menu to analyze code.</p>
|
|
2439
|
+
<button id="select-working-dir-btn" class="btn btn-primary">
|
|
2440
|
+
Select Working Directory
|
|
2441
|
+
</button>
|
|
2442
|
+
`;
|
|
2443
|
+
messageDiv.style.cssText = `
|
|
2444
|
+
text-align: center;
|
|
2445
|
+
padding: 40px;
|
|
2446
|
+
color: #666;
|
|
2447
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2448
|
+
`;
|
|
2449
|
+
|
|
2450
|
+
// Style the message elements
|
|
2451
|
+
const messageIcon = messageDiv.querySelector('.message-icon');
|
|
2452
|
+
if (messageIcon) {
|
|
2453
|
+
messageIcon.style.cssText = 'font-size: 48px; margin-bottom: 16px; opacity: 0.5;';
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
const h3 = messageDiv.querySelector('h3');
|
|
2457
|
+
if (h3) {
|
|
2458
|
+
h3.style.cssText = 'margin: 16px 0; color: #333; font-size: 20px;';
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
const p = messageDiv.querySelector('p');
|
|
2462
|
+
if (p) {
|
|
2463
|
+
p.style.cssText = 'margin: 16px 0; color: #666; font-size: 14px;';
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
const button = messageDiv.querySelector('button');
|
|
2467
|
+
if (button) {
|
|
2468
|
+
button.style.cssText = `
|
|
2469
|
+
margin-top: 20px;
|
|
2470
|
+
padding: 10px 20px;
|
|
2471
|
+
background: #3b82f6;
|
|
2472
|
+
color: white;
|
|
2473
|
+
border: none;
|
|
2474
|
+
border-radius: 6px;
|
|
2475
|
+
cursor: pointer;
|
|
2476
|
+
font-size: 14px;
|
|
2477
|
+
transition: background 0.2s;
|
|
2478
|
+
`;
|
|
2479
|
+
button.addEventListener('mouseenter', () => {
|
|
2480
|
+
button.style.background = '#2563eb';
|
|
2481
|
+
});
|
|
2482
|
+
button.addEventListener('mouseleave', () => {
|
|
2483
|
+
button.style.background = '#3b82f6';
|
|
2484
|
+
});
|
|
2485
|
+
button.addEventListener('click', () => {
|
|
2486
|
+
// Trigger working directory selection
|
|
2487
|
+
const changeDirBtn = document.getElementById('change-dir-btn');
|
|
2488
|
+
if (changeDirBtn) {
|
|
2489
|
+
changeDirBtn.click();
|
|
2490
|
+
} else if (window.dashboard && window.dashboard.workingDirectoryManager) {
|
|
2491
|
+
window.dashboard.workingDirectoryManager.showChangeDirDialog();
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
container.appendChild(messageDiv);
|
|
2497
|
+
|
|
2498
|
+
// Update breadcrumb
|
|
2499
|
+
this.updateBreadcrumb('Please select a working directory', 'warning');
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* Remove the no working directory message
|
|
2504
|
+
*/
|
|
2505
|
+
removeNoWorkingDirectoryMessage() {
|
|
2506
|
+
const message = document.getElementById('no-working-dir-message');
|
|
2507
|
+
if (message) {
|
|
2508
|
+
message.remove();
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
/**
|
|
2513
|
+
* Export tree data
|
|
2514
|
+
*/
|
|
2515
|
+
exportTree() {
|
|
2516
|
+
const exportData = {
|
|
2517
|
+
timestamp: new Date().toISOString(),
|
|
2518
|
+
workingDirectory: this.getWorkingDirectory(),
|
|
2519
|
+
stats: this.stats,
|
|
2520
|
+
tree: this.treeData
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
const blob = new Blob([JSON.stringify(exportData, null, 2)],
|
|
2524
|
+
{type: 'application/json'});
|
|
2525
|
+
const url = URL.createObjectURL(blob);
|
|
2526
|
+
const link = document.createElement('a');
|
|
2527
|
+
link.href = url;
|
|
2528
|
+
link.download = `code-tree-${Date.now()}.json`;
|
|
2529
|
+
link.click();
|
|
2530
|
+
URL.revokeObjectURL(url);
|
|
2531
|
+
|
|
2532
|
+
this.showNotification('Tree exported successfully', 'success');
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* Update activity ticker with real-time messages
|
|
2537
|
+
*/
|
|
2538
|
+
updateActivityTicker(message, type = 'info') {
|
|
2539
|
+
const breadcrumb = document.getElementById('breadcrumb-content');
|
|
2540
|
+
if (breadcrumb) {
|
|
2541
|
+
// Add spinning icon for loading states
|
|
2542
|
+
const icon = type === 'info' && message.includes('...') ? '⟳ ' : '';
|
|
2543
|
+
breadcrumb.innerHTML = `${icon}${message}`;
|
|
2544
|
+
breadcrumb.className = `breadcrumb-${type}`;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/**
|
|
2549
|
+
* Update ticker message
|
|
2550
|
+
*/
|
|
2551
|
+
updateTicker(message, type = 'info') {
|
|
2552
|
+
const ticker = document.getElementById('code-tree-ticker');
|
|
2553
|
+
if (ticker) {
|
|
2554
|
+
ticker.textContent = message;
|
|
2555
|
+
ticker.className = `ticker ticker-${type}`;
|
|
2556
|
+
|
|
2557
|
+
// Auto-hide after 5 seconds for non-error messages
|
|
2558
|
+
if (type !== 'error') {
|
|
2559
|
+
setTimeout(() => {
|
|
2560
|
+
ticker.style.opacity = '0';
|
|
2561
|
+
setTimeout(() => {
|
|
2562
|
+
ticker.style.opacity = '1';
|
|
2563
|
+
ticker.textContent = '';
|
|
2564
|
+
}, 300);
|
|
2565
|
+
}, 5000);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// Export for use in other modules
|
|
2572
|
+
window.CodeTree = CodeTree;
|
|
2573
|
+
|
|
2574
|
+
// Auto-initialize when DOM is ready
|
|
2575
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2576
|
+
// Check if we're on a page with code tree container
|
|
2577
|
+
if (document.getElementById('code-tree-container')) {
|
|
2578
|
+
window.codeTree = new CodeTree();
|
|
2579
|
+
|
|
2580
|
+
// Listen for tab changes to initialize when code tab is selected
|
|
2581
|
+
document.addEventListener('click', (e) => {
|
|
2582
|
+
if (e.target.matches('[data-tab="code"]')) {
|
|
2583
|
+
setTimeout(() => {
|
|
2584
|
+
if (window.codeTree && !window.codeTree.initialized) {
|
|
2585
|
+
window.codeTree.initialize();
|
|
2586
|
+
} else if (window.codeTree) {
|
|
2587
|
+
window.codeTree.renderWhenVisible();
|
|
2588
|
+
}
|
|
2589
|
+
}, 100);
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
});
|