mcp-vector-search 0.9.3__py3-none-any.whl → 0.12.1__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.
Potentially problematic release.
This version of mcp-vector-search might be problematic. Click here for more details.
- mcp_vector_search/__init__.py +2 -2
- mcp_vector_search/cli/commands/index.py +44 -22
- mcp_vector_search/cli/commands/install.py +502 -523
- mcp_vector_search/cli/commands/install_old.py +696 -0
- mcp_vector_search/cli/commands/status.py +7 -5
- mcp_vector_search/cli/commands/uninstall.py +485 -0
- mcp_vector_search/cli/commands/visualize.py +677 -53
- mcp_vector_search/cli/didyoumean.py +10 -0
- mcp_vector_search/cli/main.py +39 -21
- mcp_vector_search/core/connection_pool.py +49 -11
- mcp_vector_search/core/database.py +61 -28
- mcp_vector_search/core/directory_index.py +318 -0
- mcp_vector_search/core/indexer.py +146 -19
- mcp_vector_search/core/models.py +61 -0
- mcp_vector_search/core/project.py +16 -5
- mcp_vector_search/parsers/base.py +54 -18
- mcp_vector_search/parsers/javascript.py +41 -20
- mcp_vector_search/parsers/python.py +19 -11
- mcp_vector_search/parsers/registry.py +3 -2
- mcp_vector_search/utils/gitignore.py +17 -5
- mcp_vector_search/visualization/index.html +658 -0
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/METADATA +87 -24
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/RECORD +26 -22
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/WHEEL +0 -0
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-0.9.3.dist-info → mcp_vector_search-0.12.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
import shutil
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
import typer
|
|
@@ -57,7 +58,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
|
57
58
|
project_manager = ProjectManager(Path.cwd())
|
|
58
59
|
|
|
59
60
|
if not project_manager.is_initialized():
|
|
60
|
-
console.print(
|
|
61
|
+
console.print(
|
|
62
|
+
"[red]Project not initialized. Run 'mcp-vector-search init' first.[/red]"
|
|
63
|
+
)
|
|
61
64
|
raise typer.Exit(1)
|
|
62
65
|
|
|
63
66
|
config = project_manager.load_config()
|
|
@@ -75,7 +78,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
|
75
78
|
chunks = await database.get_all_chunks()
|
|
76
79
|
|
|
77
80
|
if len(chunks) == 0:
|
|
78
|
-
console.print(
|
|
81
|
+
console.print(
|
|
82
|
+
"[yellow]No chunks found in index. Run 'mcp-vector-search index' first.[/yellow]"
|
|
83
|
+
)
|
|
79
84
|
raise typer.Exit(1)
|
|
80
85
|
|
|
81
86
|
console.print(f"[green]✓[/green] Retrieved {len(chunks)} chunks")
|
|
@@ -83,8 +88,11 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
|
83
88
|
# Apply file filter if specified
|
|
84
89
|
if file_filter:
|
|
85
90
|
from fnmatch import fnmatch
|
|
91
|
+
|
|
86
92
|
chunks = [c for c in chunks if fnmatch(str(c.file_path), file_filter)]
|
|
87
|
-
console.print(
|
|
93
|
+
console.print(
|
|
94
|
+
f"[cyan]Filtered to {len(chunks)} chunks matching '{file_filter}'[/cyan]"
|
|
95
|
+
)
|
|
88
96
|
|
|
89
97
|
# Collect subprojects for monorepo support
|
|
90
98
|
subprojects = {}
|
|
@@ -93,17 +101,23 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
|
93
101
|
subprojects[chunk.subproject_name] = {
|
|
94
102
|
"name": chunk.subproject_name,
|
|
95
103
|
"path": chunk.subproject_path,
|
|
96
|
-
"color": _get_subproject_color(
|
|
104
|
+
"color": _get_subproject_color(
|
|
105
|
+
chunk.subproject_name, len(subprojects)
|
|
106
|
+
),
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
# Build graph data structure
|
|
100
110
|
nodes = []
|
|
101
111
|
links = []
|
|
102
112
|
chunk_id_map = {} # Map chunk IDs to array indices
|
|
113
|
+
file_nodes = {} # Track file nodes by path
|
|
114
|
+
dir_nodes = {} # Track directory nodes by path
|
|
103
115
|
|
|
104
116
|
# Add subproject root nodes for monorepos
|
|
105
117
|
if subprojects:
|
|
106
|
-
console.print(
|
|
118
|
+
console.print(
|
|
119
|
+
f"[cyan]Detected monorepo with {len(subprojects)} subprojects[/cyan]"
|
|
120
|
+
)
|
|
107
121
|
for sp_name, sp_data in subprojects.items():
|
|
108
122
|
node = {
|
|
109
123
|
"id": f"subproject_{sp_name}",
|
|
@@ -118,11 +132,96 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
|
118
132
|
}
|
|
119
133
|
nodes.append(node)
|
|
120
134
|
|
|
135
|
+
# Load directory index for enhanced directory metadata
|
|
136
|
+
console.print("[cyan]Loading directory index...[/cyan]")
|
|
137
|
+
from ...core.directory_index import DirectoryIndex
|
|
138
|
+
|
|
139
|
+
dir_index_path = (
|
|
140
|
+
project_manager.project_root / ".mcp-vector-search" / "directory_index.json"
|
|
141
|
+
)
|
|
142
|
+
dir_index = DirectoryIndex(dir_index_path)
|
|
143
|
+
dir_index.load()
|
|
144
|
+
|
|
145
|
+
# Create directory nodes from directory index
|
|
146
|
+
console.print(
|
|
147
|
+
f"[green]✓[/green] Loaded {len(dir_index.directories)} directories"
|
|
148
|
+
)
|
|
149
|
+
for dir_path_str, directory in dir_index.directories.items():
|
|
150
|
+
dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
|
|
151
|
+
dir_nodes[dir_path_str] = {
|
|
152
|
+
"id": dir_id,
|
|
153
|
+
"name": directory.name,
|
|
154
|
+
"type": "directory",
|
|
155
|
+
"file_path": dir_path_str,
|
|
156
|
+
"start_line": 0,
|
|
157
|
+
"end_line": 0,
|
|
158
|
+
"complexity": 0,
|
|
159
|
+
"depth": directory.depth,
|
|
160
|
+
"dir_path": dir_path_str,
|
|
161
|
+
"file_count": directory.file_count,
|
|
162
|
+
"subdirectory_count": directory.subdirectory_count,
|
|
163
|
+
"total_chunks": directory.total_chunks,
|
|
164
|
+
"languages": directory.languages or {},
|
|
165
|
+
"is_package": directory.is_package,
|
|
166
|
+
"last_modified": directory.last_modified,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Create file nodes from chunks
|
|
170
|
+
for chunk in chunks:
|
|
171
|
+
file_path_str = str(chunk.file_path)
|
|
172
|
+
file_path = Path(file_path_str)
|
|
173
|
+
|
|
174
|
+
# Create file node with parent directory reference
|
|
175
|
+
if file_path_str not in file_nodes:
|
|
176
|
+
file_id = f"file_{hash(file_path_str) & 0xFFFFFFFF:08x}"
|
|
177
|
+
|
|
178
|
+
# Convert absolute path to relative path for parent directory lookup
|
|
179
|
+
try:
|
|
180
|
+
relative_file_path = file_path.relative_to(
|
|
181
|
+
project_manager.project_root
|
|
182
|
+
)
|
|
183
|
+
parent_dir = relative_file_path.parent
|
|
184
|
+
# Use relative path for parent directory (matches directory_index)
|
|
185
|
+
parent_dir_str = (
|
|
186
|
+
str(parent_dir) if parent_dir != Path(".") else None
|
|
187
|
+
)
|
|
188
|
+
except ValueError:
|
|
189
|
+
# File is outside project root
|
|
190
|
+
parent_dir_str = None
|
|
191
|
+
|
|
192
|
+
# Look up parent directory ID from dir_nodes (must match exactly)
|
|
193
|
+
parent_dir_id = None
|
|
194
|
+
if parent_dir_str and parent_dir_str in dir_nodes:
|
|
195
|
+
parent_dir_id = dir_nodes[parent_dir_str]["id"]
|
|
196
|
+
|
|
197
|
+
file_nodes[file_path_str] = {
|
|
198
|
+
"id": file_id,
|
|
199
|
+
"name": file_path.name,
|
|
200
|
+
"type": "file",
|
|
201
|
+
"file_path": file_path_str,
|
|
202
|
+
"start_line": 0,
|
|
203
|
+
"end_line": 0,
|
|
204
|
+
"complexity": 0,
|
|
205
|
+
"depth": len(file_path.parts) - 1,
|
|
206
|
+
"parent_dir_id": parent_dir_id,
|
|
207
|
+
"parent_dir_path": parent_dir_str,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Add directory nodes to graph
|
|
211
|
+
for dir_node in dir_nodes.values():
|
|
212
|
+
nodes.append(dir_node)
|
|
213
|
+
|
|
214
|
+
# Add file nodes to graph
|
|
215
|
+
for file_node in file_nodes.values():
|
|
216
|
+
nodes.append(file_node)
|
|
217
|
+
|
|
121
218
|
# Add chunk nodes
|
|
122
219
|
for chunk in chunks:
|
|
123
220
|
node = {
|
|
124
221
|
"id": chunk.chunk_id or chunk.id,
|
|
125
|
-
"name": chunk.function_name
|
|
222
|
+
"name": chunk.function_name
|
|
223
|
+
or chunk.class_name
|
|
224
|
+
or f"L{chunk.start_line}",
|
|
126
225
|
"type": chunk.chunk_type,
|
|
127
226
|
"file_path": str(chunk.file_path),
|
|
128
227
|
"start_line": chunk.start_line,
|
|
@@ -130,6 +229,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
|
130
229
|
"complexity": chunk.complexity_score,
|
|
131
230
|
"parent_id": chunk.parent_chunk_id,
|
|
132
231
|
"depth": chunk.chunk_depth,
|
|
232
|
+
"content": chunk.content, # Add content for code viewer
|
|
233
|
+
"docstring": chunk.docstring,
|
|
234
|
+
"language": chunk.language,
|
|
133
235
|
}
|
|
134
236
|
|
|
135
237
|
# Add subproject info for monorepos
|
|
@@ -140,34 +242,90 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
|
140
242
|
nodes.append(node)
|
|
141
243
|
chunk_id_map[node["id"]] = len(nodes) - 1
|
|
142
244
|
|
|
245
|
+
# Link directories to their parent directories (hierarchical structure)
|
|
246
|
+
for dir_path_str, dir_info in dir_index.directories.items():
|
|
247
|
+
if dir_info.parent_path:
|
|
248
|
+
parent_path_str = str(dir_info.parent_path)
|
|
249
|
+
if parent_path_str in dir_nodes:
|
|
250
|
+
parent_dir_id = f"dir_{hash(parent_path_str) & 0xFFFFFFFF:08x}"
|
|
251
|
+
child_dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
|
|
252
|
+
links.append(
|
|
253
|
+
{
|
|
254
|
+
"source": parent_dir_id,
|
|
255
|
+
"target": child_dir_id,
|
|
256
|
+
"type": "dir_hierarchy",
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Link directories to subprojects in monorepos (simple flat structure)
|
|
261
|
+
if subprojects:
|
|
262
|
+
for dir_path_str, dir_node in dir_nodes.items():
|
|
263
|
+
for sp_name, sp_data in subprojects.items():
|
|
264
|
+
if dir_path_str.startswith(sp_data.get("path", "")):
|
|
265
|
+
links.append(
|
|
266
|
+
{
|
|
267
|
+
"source": f"subproject_{sp_name}",
|
|
268
|
+
"target": dir_node["id"],
|
|
269
|
+
"type": "dir_containment",
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
# Link files to their parent directories
|
|
275
|
+
for _file_path_str, file_node in file_nodes.items():
|
|
276
|
+
if file_node.get("parent_dir_id"):
|
|
277
|
+
links.append(
|
|
278
|
+
{
|
|
279
|
+
"source": file_node["parent_dir_id"],
|
|
280
|
+
"target": file_node["id"],
|
|
281
|
+
"type": "dir_containment",
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
143
285
|
# Build hierarchical links from parent-child relationships
|
|
144
286
|
for chunk in chunks:
|
|
145
287
|
chunk_id = chunk.chunk_id or chunk.id
|
|
288
|
+
file_path = str(chunk.file_path)
|
|
289
|
+
|
|
290
|
+
# Link chunk to its file node if it has no parent (top-level chunks)
|
|
291
|
+
if not chunk.parent_chunk_id and file_path in file_nodes:
|
|
292
|
+
links.append(
|
|
293
|
+
{
|
|
294
|
+
"source": file_nodes[file_path]["id"],
|
|
295
|
+
"target": chunk_id,
|
|
296
|
+
"type": "file_containment",
|
|
297
|
+
}
|
|
298
|
+
)
|
|
146
299
|
|
|
147
300
|
# Link to subproject root if in monorepo
|
|
148
301
|
if chunk.subproject_name and not chunk.parent_chunk_id:
|
|
149
|
-
links.append(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
302
|
+
links.append(
|
|
303
|
+
{
|
|
304
|
+
"source": f"subproject_{chunk.subproject_name}",
|
|
305
|
+
"target": chunk_id,
|
|
306
|
+
}
|
|
307
|
+
)
|
|
153
308
|
|
|
154
309
|
# Link to parent chunk
|
|
155
310
|
if chunk.parent_chunk_id and chunk.parent_chunk_id in chunk_id_map:
|
|
156
|
-
links.append(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
311
|
+
links.append(
|
|
312
|
+
{
|
|
313
|
+
"source": chunk.parent_chunk_id,
|
|
314
|
+
"target": chunk_id,
|
|
315
|
+
}
|
|
316
|
+
)
|
|
160
317
|
|
|
161
318
|
# Parse inter-project dependencies for monorepos
|
|
162
319
|
if subprojects:
|
|
163
320
|
console.print("[cyan]Parsing inter-project dependencies...[/cyan]")
|
|
164
321
|
dep_links = _parse_project_dependencies(
|
|
165
|
-
project_manager.project_root,
|
|
166
|
-
subprojects
|
|
322
|
+
project_manager.project_root, subprojects
|
|
167
323
|
)
|
|
168
324
|
links.extend(dep_links)
|
|
169
325
|
if dep_links:
|
|
170
|
-
console.print(
|
|
326
|
+
console.print(
|
|
327
|
+
f"[green]✓[/green] Found {len(dep_links)} inter-project dependencies"
|
|
328
|
+
)
|
|
171
329
|
|
|
172
330
|
# Get stats
|
|
173
331
|
stats = await database.get_stats()
|
|
@@ -261,11 +419,13 @@ def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[d
|
|
|
261
419
|
for other_sp_name in subprojects.keys():
|
|
262
420
|
if other_sp_name != sp_name and dep_name == other_sp_name:
|
|
263
421
|
# Found inter-project dependency
|
|
264
|
-
dependency_links.append(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
422
|
+
dependency_links.append(
|
|
423
|
+
{
|
|
424
|
+
"source": f"subproject_{sp_name}",
|
|
425
|
+
"target": f"subproject_{other_sp_name}",
|
|
426
|
+
"type": "dependency",
|
|
427
|
+
}
|
|
428
|
+
)
|
|
269
429
|
|
|
270
430
|
except Exception as e:
|
|
271
431
|
logger.debug(f"Failed to parse {package_json}: {e}")
|
|
@@ -276,7 +436,9 @@ def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[d
|
|
|
276
436
|
|
|
277
437
|
@app.command()
|
|
278
438
|
def serve(
|
|
279
|
-
port: int = typer.Option(
|
|
439
|
+
port: int = typer.Option(
|
|
440
|
+
8080, "--port", "-p", help="Port for visualization server"
|
|
441
|
+
),
|
|
280
442
|
graph_file: Path = typer.Option(
|
|
281
443
|
Path("chunk-graph.json"),
|
|
282
444
|
"--graph",
|
|
@@ -339,24 +501,30 @@ def serve(
|
|
|
339
501
|
|
|
340
502
|
# Copy graph file to visualization directory if it exists
|
|
341
503
|
if graph_file.exists():
|
|
342
|
-
import shutil
|
|
343
|
-
|
|
344
504
|
dest = viz_dir / "chunk-graph.json"
|
|
345
505
|
shutil.copy(graph_file, dest)
|
|
346
506
|
console.print(f"[green]✓[/green] Copied graph data to {dest}")
|
|
347
507
|
else:
|
|
508
|
+
# Auto-generate graph file if it doesn't exist
|
|
348
509
|
console.print(
|
|
349
|
-
f"[yellow]
|
|
350
|
-
"Run 'mcp-vector-search visualize export' first.[/yellow]"
|
|
510
|
+
f"[yellow]Graph file {graph_file} not found. Generating it now...[/yellow]"
|
|
351
511
|
)
|
|
512
|
+
asyncio.run(_export_chunks(graph_file, None))
|
|
513
|
+
console.print()
|
|
514
|
+
|
|
515
|
+
# Copy the newly generated graph to visualization directory
|
|
516
|
+
if graph_file.exists():
|
|
517
|
+
dest = viz_dir / "chunk-graph.json"
|
|
518
|
+
shutil.copy(graph_file, dest)
|
|
519
|
+
console.print(f"[green]✓[/green] Copied graph data to {dest}")
|
|
352
520
|
|
|
353
521
|
# Change to visualization directory
|
|
354
522
|
os.chdir(viz_dir)
|
|
355
523
|
|
|
356
524
|
# Start server
|
|
357
|
-
|
|
525
|
+
handler = http.server.SimpleHTTPRequestHandler
|
|
358
526
|
try:
|
|
359
|
-
with socketserver.TCPServer(("", port),
|
|
527
|
+
with socketserver.TCPServer(("", port), handler) as httpd:
|
|
360
528
|
url = f"http://localhost:{port}"
|
|
361
529
|
console.print()
|
|
362
530
|
console.print(
|
|
@@ -390,7 +558,7 @@ def serve(
|
|
|
390
558
|
|
|
391
559
|
def _create_visualization_html(html_file: Path) -> None:
|
|
392
560
|
"""Create the D3.js visualization HTML file."""
|
|
393
|
-
html_content =
|
|
561
|
+
html_content = """<!DOCTYPE html>
|
|
394
562
|
<html>
|
|
395
563
|
<head>
|
|
396
564
|
<meta charset="utf-8">
|
|
@@ -476,8 +644,30 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
476
644
|
.node.function circle { fill: #d29922; }
|
|
477
645
|
.node.method circle { fill: #8957e5; }
|
|
478
646
|
.node.code circle { fill: #6e7681; }
|
|
647
|
+
.node.file circle {
|
|
648
|
+
fill: none;
|
|
649
|
+
stroke: #58a6ff;
|
|
650
|
+
stroke-width: 2px;
|
|
651
|
+
stroke-dasharray: 5,3;
|
|
652
|
+
opacity: 0.6;
|
|
653
|
+
}
|
|
654
|
+
.node.directory circle {
|
|
655
|
+
fill: none;
|
|
656
|
+
stroke: #79c0ff;
|
|
657
|
+
stroke-width: 2px;
|
|
658
|
+
stroke-dasharray: 3,3;
|
|
659
|
+
opacity: 0.5;
|
|
660
|
+
}
|
|
479
661
|
.node.subproject circle { fill: #da3633; stroke-width: 3px; }
|
|
480
662
|
|
|
663
|
+
/* Non-code document nodes - squares */
|
|
664
|
+
.node.docstring rect { fill: #8b949e; }
|
|
665
|
+
.node.comment rect { fill: #6e7681; }
|
|
666
|
+
.node rect {
|
|
667
|
+
stroke: #c9d1d9;
|
|
668
|
+
stroke-width: 1.5px;
|
|
669
|
+
}
|
|
670
|
+
|
|
481
671
|
.node text {
|
|
482
672
|
font-size: 11px;
|
|
483
673
|
fill: #c9d1d9;
|
|
@@ -519,6 +709,146 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
519
709
|
font-size: 12px;
|
|
520
710
|
color: #8b949e;
|
|
521
711
|
}
|
|
712
|
+
|
|
713
|
+
#content-pane {
|
|
714
|
+
position: fixed;
|
|
715
|
+
top: 0;
|
|
716
|
+
right: 0;
|
|
717
|
+
width: 600px;
|
|
718
|
+
height: 100vh;
|
|
719
|
+
background: rgba(13, 17, 23, 0.98);
|
|
720
|
+
border-left: 1px solid #30363d;
|
|
721
|
+
overflow-y: auto;
|
|
722
|
+
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
|
|
723
|
+
transform: translateX(100%);
|
|
724
|
+
transition: transform 0.3s ease-in-out;
|
|
725
|
+
z-index: 1000;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#content-pane.visible {
|
|
729
|
+
transform: translateX(0);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#content-pane .pane-header {
|
|
733
|
+
position: sticky;
|
|
734
|
+
top: 0;
|
|
735
|
+
background: rgba(13, 17, 23, 0.98);
|
|
736
|
+
padding: 20px;
|
|
737
|
+
border-bottom: 1px solid #30363d;
|
|
738
|
+
z-index: 1;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
#content-pane .pane-title {
|
|
742
|
+
font-size: 16px;
|
|
743
|
+
font-weight: bold;
|
|
744
|
+
color: #58a6ff;
|
|
745
|
+
margin-bottom: 8px;
|
|
746
|
+
padding-right: 30px;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
#content-pane .pane-meta {
|
|
750
|
+
font-size: 12px;
|
|
751
|
+
color: #8b949e;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
#content-pane .collapse-btn {
|
|
755
|
+
position: absolute;
|
|
756
|
+
top: 20px;
|
|
757
|
+
right: 20px;
|
|
758
|
+
cursor: pointer;
|
|
759
|
+
color: #8b949e;
|
|
760
|
+
font-size: 24px;
|
|
761
|
+
line-height: 1;
|
|
762
|
+
background: none;
|
|
763
|
+
border: none;
|
|
764
|
+
padding: 0;
|
|
765
|
+
transition: color 0.2s;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
#content-pane .collapse-btn:hover {
|
|
769
|
+
color: #c9d1d9;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
#content-pane .pane-content {
|
|
773
|
+
padding: 20px;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#content-pane pre {
|
|
777
|
+
margin: 0;
|
|
778
|
+
padding: 16px;
|
|
779
|
+
background: #0d1117;
|
|
780
|
+
border: 1px solid #30363d;
|
|
781
|
+
border-radius: 6px;
|
|
782
|
+
overflow-x: auto;
|
|
783
|
+
font-size: 12px;
|
|
784
|
+
line-height: 1.6;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
#content-pane code {
|
|
788
|
+
color: #c9d1d9;
|
|
789
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
#content-pane .directory-list {
|
|
793
|
+
list-style: none;
|
|
794
|
+
padding: 0;
|
|
795
|
+
margin: 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
#content-pane .directory-list li {
|
|
799
|
+
padding: 8px 12px;
|
|
800
|
+
margin: 4px 0;
|
|
801
|
+
background: #161b22;
|
|
802
|
+
border: 1px solid #30363d;
|
|
803
|
+
border-radius: 4px;
|
|
804
|
+
font-size: 12px;
|
|
805
|
+
display: flex;
|
|
806
|
+
align-items: center;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
#content-pane .directory-list .item-icon {
|
|
810
|
+
margin-right: 8px;
|
|
811
|
+
font-size: 14px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
#content-pane .directory-list .item-type {
|
|
815
|
+
margin-left: auto;
|
|
816
|
+
padding-left: 12px;
|
|
817
|
+
font-size: 10px;
|
|
818
|
+
color: #8b949e;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
#content-pane .import-details {
|
|
822
|
+
background: #161b22;
|
|
823
|
+
border: 1px solid #30363d;
|
|
824
|
+
border-radius: 6px;
|
|
825
|
+
padding: 16px;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
#content-pane .import-details .import-statement {
|
|
829
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
830
|
+
font-size: 12px;
|
|
831
|
+
color: #79c0ff;
|
|
832
|
+
margin-bottom: 12px;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#content-pane .import-details .detail-row {
|
|
836
|
+
font-size: 11px;
|
|
837
|
+
color: #8b949e;
|
|
838
|
+
margin: 4px 0;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
#content-pane .import-details .detail-label {
|
|
842
|
+
color: #c9d1d9;
|
|
843
|
+
font-weight: 600;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.node.highlighted circle,
|
|
847
|
+
.node.highlighted rect {
|
|
848
|
+
stroke: #f0e68c;
|
|
849
|
+
stroke-width: 3px;
|
|
850
|
+
filter: drop-shadow(0 0 8px #f0e68c);
|
|
851
|
+
}
|
|
522
852
|
</style>
|
|
523
853
|
</head>
|
|
524
854
|
<body>
|
|
@@ -534,6 +864,12 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
534
864
|
<div class="legend-item">
|
|
535
865
|
<span class="legend-color" style="background: #da3633;"></span> Subproject
|
|
536
866
|
</div>
|
|
867
|
+
<div class="legend-item">
|
|
868
|
+
<span class="legend-color" style="border: 2px dashed #79c0ff; border-radius: 50%; background: transparent;"></span> Directory
|
|
869
|
+
</div>
|
|
870
|
+
<div class="legend-item">
|
|
871
|
+
<span class="legend-color" style="border: 2px dashed #58a6ff; border-radius: 50%; background: transparent;"></span> File
|
|
872
|
+
</div>
|
|
537
873
|
<div class="legend-item">
|
|
538
874
|
<span class="legend-color" style="background: #238636;"></span> Module
|
|
539
875
|
</div>
|
|
@@ -549,6 +885,15 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
549
885
|
<div class="legend-item">
|
|
550
886
|
<span class="legend-color" style="background: #6e7681;"></span> Code
|
|
551
887
|
</div>
|
|
888
|
+
<div class="legend-item" style="font-style: italic; color: #79c0ff;">
|
|
889
|
+
<span class="legend-color" style="background: #6e7681;"></span> Import (L1)
|
|
890
|
+
</div>
|
|
891
|
+
<div class="legend-item">
|
|
892
|
+
<span class="legend-color" style="background: #8b949e; border-radius: 2px;"></span> Docstring ▢
|
|
893
|
+
</div>
|
|
894
|
+
<div class="legend-item">
|
|
895
|
+
<span class="legend-color" style="background: #6e7681; border-radius: 2px;"></span> Comment ▢
|
|
896
|
+
</div>
|
|
552
897
|
</div>
|
|
553
898
|
|
|
554
899
|
<div id="subprojects-legend" style="display: none;">
|
|
@@ -562,6 +907,15 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
562
907
|
<svg id="graph"></svg>
|
|
563
908
|
<div id="tooltip" class="tooltip"></div>
|
|
564
909
|
|
|
910
|
+
<div id="content-pane">
|
|
911
|
+
<div class="pane-header">
|
|
912
|
+
<button class="collapse-btn" onclick="closeContentPane()">×</button>
|
|
913
|
+
<div class="pane-title" id="pane-title"></div>
|
|
914
|
+
<div class="pane-meta" id="pane-meta"></div>
|
|
915
|
+
</div>
|
|
916
|
+
<div class="pane-content" id="pane-content"></div>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
565
919
|
<script>
|
|
566
920
|
const width = window.innerWidth;
|
|
567
921
|
const height = window.innerHeight;
|
|
@@ -580,6 +934,7 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
580
934
|
let allLinks = [];
|
|
581
935
|
let visibleNodes = new Set();
|
|
582
936
|
let collapsedNodes = new Set();
|
|
937
|
+
let highlightedNode = null;
|
|
583
938
|
|
|
584
939
|
function visualizeGraph(data) {
|
|
585
940
|
g.selectAll("*").remove();
|
|
@@ -587,19 +942,37 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
587
942
|
allNodes = data.nodes;
|
|
588
943
|
allLinks = data.links;
|
|
589
944
|
|
|
590
|
-
// Find root nodes
|
|
945
|
+
// Find root nodes - start with only top-level nodes
|
|
591
946
|
let rootNodes;
|
|
592
947
|
if (data.metadata && data.metadata.is_monorepo) {
|
|
593
948
|
// In monorepos, subproject nodes are roots
|
|
594
949
|
rootNodes = allNodes.filter(n => n.type === 'subproject');
|
|
595
950
|
} else {
|
|
596
|
-
// Regular projects:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
951
|
+
// Regular projects: show root-level directories AND files
|
|
952
|
+
const dirNodes = allNodes.filter(n => n.type === 'directory');
|
|
953
|
+
const fileNodes = allNodes.filter(n => n.type === 'file');
|
|
954
|
+
|
|
955
|
+
// Find minimum depth for directories and files
|
|
956
|
+
const minDirDepth = dirNodes.length > 0
|
|
957
|
+
? Math.min(...dirNodes.map(n => n.depth))
|
|
958
|
+
: Infinity;
|
|
959
|
+
const minFileDepth = fileNodes.length > 0
|
|
960
|
+
? Math.min(...fileNodes.map(n => n.depth))
|
|
961
|
+
: Infinity;
|
|
962
|
+
|
|
963
|
+
// Include both root-level directories and root-level files
|
|
964
|
+
rootNodes = [
|
|
965
|
+
...dirNodes.filter(n => n.depth === minDirDepth),
|
|
966
|
+
...fileNodes.filter(n => n.depth === minFileDepth)
|
|
967
|
+
];
|
|
968
|
+
|
|
969
|
+
// Fallback to all files if nothing found
|
|
970
|
+
if (rootNodes.length === 0) {
|
|
971
|
+
rootNodes = fileNodes;
|
|
972
|
+
}
|
|
600
973
|
}
|
|
601
974
|
|
|
602
|
-
// Start with only root nodes visible
|
|
975
|
+
// Start with only root nodes visible, all collapsed
|
|
603
976
|
visibleNodes = new Set(rootNodes.map(n => n.id));
|
|
604
977
|
collapsedNodes = new Set(rootNodes.map(n => n.id));
|
|
605
978
|
|
|
@@ -631,22 +1004,59 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
631
1004
|
.selectAll("g")
|
|
632
1005
|
.data(visibleNodesList)
|
|
633
1006
|
.join("g")
|
|
634
|
-
.attr("class", d =>
|
|
1007
|
+
.attr("class", d => {
|
|
1008
|
+
let classes = `node ${d.type}`;
|
|
1009
|
+
if (highlightedNode && d.id === highlightedNode.id) {
|
|
1010
|
+
classes += ' highlighted';
|
|
1011
|
+
}
|
|
1012
|
+
return classes;
|
|
1013
|
+
})
|
|
635
1014
|
.call(drag(simulation))
|
|
636
|
-
.on("click",
|
|
1015
|
+
.on("click", handleNodeClick)
|
|
637
1016
|
.on("mouseover", showTooltip)
|
|
638
1017
|
.on("mouseout", hideTooltip);
|
|
639
1018
|
|
|
640
|
-
// Add circles
|
|
641
|
-
|
|
1019
|
+
// Add shapes based on node type (circles for code, squares for docs)
|
|
1020
|
+
const isDocNode = d => ['docstring', 'comment'].includes(d.type);
|
|
1021
|
+
|
|
1022
|
+
// Add circles for code nodes
|
|
1023
|
+
node.filter(d => !isDocNode(d))
|
|
1024
|
+
.append("circle")
|
|
642
1025
|
.attr("r", d => {
|
|
643
1026
|
if (d.type === 'subproject') return 20;
|
|
1027
|
+
if (d.type === 'directory') return 40; // Largest for directory containers
|
|
1028
|
+
if (d.type === 'file') return 30; // Larger transparent circle for files
|
|
644
1029
|
return d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
|
|
645
1030
|
})
|
|
646
1031
|
.attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
|
|
647
1032
|
.attr("stroke-width", d => hasChildren(d) ? 2 : 0)
|
|
648
1033
|
.style("fill", d => d.color || null); // Use custom color if available
|
|
649
1034
|
|
|
1035
|
+
// Add rectangles for document nodes
|
|
1036
|
+
node.filter(d => isDocNode(d))
|
|
1037
|
+
.append("rect")
|
|
1038
|
+
.attr("width", d => {
|
|
1039
|
+
const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
|
|
1040
|
+
return size * 2;
|
|
1041
|
+
})
|
|
1042
|
+
.attr("height", d => {
|
|
1043
|
+
const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
|
|
1044
|
+
return size * 2;
|
|
1045
|
+
})
|
|
1046
|
+
.attr("x", d => {
|
|
1047
|
+
const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
|
|
1048
|
+
return -size;
|
|
1049
|
+
})
|
|
1050
|
+
.attr("y", d => {
|
|
1051
|
+
const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
|
|
1052
|
+
return -size;
|
|
1053
|
+
})
|
|
1054
|
+
.attr("rx", 2) // Rounded corners
|
|
1055
|
+
.attr("ry", 2)
|
|
1056
|
+
.attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
|
|
1057
|
+
.attr("stroke-width", d => hasChildren(d) ? 2 : 0)
|
|
1058
|
+
.style("fill", d => d.color || null);
|
|
1059
|
+
|
|
650
1060
|
// Add expand/collapse indicator
|
|
651
1061
|
node.filter(d => hasChildren(d))
|
|
652
1062
|
.append("text")
|
|
@@ -659,9 +1069,15 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
659
1069
|
.style("pointer-events", "none")
|
|
660
1070
|
.text(d => collapsedNodes.has(d.id) ? "+" : "−");
|
|
661
1071
|
|
|
662
|
-
// Add labels
|
|
1072
|
+
// Add labels (show "Import:" prefix for L1 nodes)
|
|
663
1073
|
node.append("text")
|
|
664
|
-
.text(d =>
|
|
1074
|
+
.text(d => {
|
|
1075
|
+
// L1 (depth 1) nodes are imports
|
|
1076
|
+
if (d.depth === 1 && d.type !== 'directory' && d.type !== 'file') {
|
|
1077
|
+
return `Import: ${d.name}`;
|
|
1078
|
+
}
|
|
1079
|
+
return d.name;
|
|
1080
|
+
})
|
|
665
1081
|
.attr("dy", 30);
|
|
666
1082
|
|
|
667
1083
|
simulation.on("tick", () => {
|
|
@@ -681,20 +1097,21 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
681
1097
|
return allLinks.some(l => (l.source.id || l.source) === node.id);
|
|
682
1098
|
}
|
|
683
1099
|
|
|
684
|
-
function
|
|
1100
|
+
function handleNodeClick(event, d) {
|
|
685
1101
|
event.stopPropagation();
|
|
686
1102
|
|
|
687
|
-
|
|
1103
|
+
// Always show content pane when clicking any node
|
|
1104
|
+
showContentPane(d);
|
|
688
1105
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1106
|
+
// If node has children, also toggle expansion
|
|
1107
|
+
if (hasChildren(d)) {
|
|
1108
|
+
if (collapsedNodes.has(d.id)) {
|
|
1109
|
+
expandNode(d);
|
|
1110
|
+
} else {
|
|
1111
|
+
collapseNode(d);
|
|
1112
|
+
}
|
|
1113
|
+
renderGraph();
|
|
695
1114
|
}
|
|
696
|
-
|
|
697
|
-
renderGraph();
|
|
698
1115
|
}
|
|
699
1116
|
|
|
700
1117
|
function expandNode(node) {
|
|
@@ -802,6 +1219,213 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
802
1219
|
}
|
|
803
1220
|
}
|
|
804
1221
|
|
|
1222
|
+
function showContentPane(node) {
|
|
1223
|
+
// Highlight the node
|
|
1224
|
+
highlightedNode = node;
|
|
1225
|
+
renderGraph();
|
|
1226
|
+
|
|
1227
|
+
// Populate content pane
|
|
1228
|
+
const pane = document.getElementById('content-pane');
|
|
1229
|
+
const title = document.getElementById('pane-title');
|
|
1230
|
+
const meta = document.getElementById('pane-meta');
|
|
1231
|
+
const content = document.getElementById('pane-content');
|
|
1232
|
+
|
|
1233
|
+
// Set title with "Import:" prefix for L1 nodes
|
|
1234
|
+
if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
|
|
1235
|
+
title.textContent = `Import: ${node.name}`;
|
|
1236
|
+
} else {
|
|
1237
|
+
title.textContent = node.name;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Set metadata
|
|
1241
|
+
let metaText = `${node.type} • ${node.file_path}`;
|
|
1242
|
+
if (node.start_line) {
|
|
1243
|
+
metaText += ` • Lines ${node.start_line}-${node.end_line}`;
|
|
1244
|
+
}
|
|
1245
|
+
if (node.language) {
|
|
1246
|
+
metaText += ` • ${node.language}`;
|
|
1247
|
+
}
|
|
1248
|
+
meta.textContent = metaText;
|
|
1249
|
+
|
|
1250
|
+
// Display content based on node type
|
|
1251
|
+
if (node.type === 'directory') {
|
|
1252
|
+
showDirectoryContents(node, content);
|
|
1253
|
+
} else if (node.type === 'file') {
|
|
1254
|
+
showFileContents(node, content);
|
|
1255
|
+
} else if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
|
|
1256
|
+
// L1 nodes are imports
|
|
1257
|
+
showImportDetails(node, content);
|
|
1258
|
+
} else {
|
|
1259
|
+
// Class, function, method, code nodes
|
|
1260
|
+
showCodeContent(node, content);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
pane.classList.add('visible');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function showDirectoryContents(node, container) {
|
|
1267
|
+
// Find all direct children of this directory
|
|
1268
|
+
const children = allLinks
|
|
1269
|
+
.filter(l => (l.source.id || l.source) === node.id)
|
|
1270
|
+
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
1271
|
+
.filter(n => n);
|
|
1272
|
+
|
|
1273
|
+
if (children.length === 0) {
|
|
1274
|
+
container.innerHTML = '<p style="color: #8b949e;">Empty directory</p>';
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Group by type
|
|
1279
|
+
const files = children.filter(n => n.type === 'file');
|
|
1280
|
+
const subdirs = children.filter(n => n.type === 'directory');
|
|
1281
|
+
const chunks = children.filter(n => n.type !== 'file' && n.type !== 'directory');
|
|
1282
|
+
|
|
1283
|
+
let html = '<ul class="directory-list">';
|
|
1284
|
+
|
|
1285
|
+
// Show subdirectories first
|
|
1286
|
+
subdirs.forEach(child => {
|
|
1287
|
+
html += `
|
|
1288
|
+
<li>
|
|
1289
|
+
<span class="item-icon">📁</span>
|
|
1290
|
+
${child.name}
|
|
1291
|
+
<span class="item-type">directory</span>
|
|
1292
|
+
</li>
|
|
1293
|
+
`;
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// Then files
|
|
1297
|
+
files.forEach(child => {
|
|
1298
|
+
html += `
|
|
1299
|
+
<li>
|
|
1300
|
+
<span class="item-icon">📄</span>
|
|
1301
|
+
${child.name}
|
|
1302
|
+
<span class="item-type">file</span>
|
|
1303
|
+
</li>
|
|
1304
|
+
`;
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// Then code chunks
|
|
1308
|
+
chunks.forEach(child => {
|
|
1309
|
+
const icon = child.type === 'class' ? '🔷' : child.type === 'function' ? '⚡' : '📝';
|
|
1310
|
+
html += `
|
|
1311
|
+
<li>
|
|
1312
|
+
<span class="item-icon">${icon}</span>
|
|
1313
|
+
${child.name}
|
|
1314
|
+
<span class="item-type">${child.type}</span>
|
|
1315
|
+
</li>
|
|
1316
|
+
`;
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
html += '</ul>';
|
|
1320
|
+
|
|
1321
|
+
// Add summary
|
|
1322
|
+
const summary = `<p style="color: #8b949e; font-size: 11px; margin-top: 16px;">
|
|
1323
|
+
Total: ${children.length} items (${subdirs.length} directories, ${files.length} files, ${chunks.length} code chunks)
|
|
1324
|
+
</p>`;
|
|
1325
|
+
|
|
1326
|
+
container.innerHTML = html + summary;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function showFileContents(node, container) {
|
|
1330
|
+
// Find all chunks in this file
|
|
1331
|
+
const fileChunks = allLinks
|
|
1332
|
+
.filter(l => (l.source.id || l.source) === node.id)
|
|
1333
|
+
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
1334
|
+
.filter(n => n);
|
|
1335
|
+
|
|
1336
|
+
if (fileChunks.length === 0) {
|
|
1337
|
+
container.innerHTML = '<p style="color: #8b949e;">No code chunks found in this file</p>';
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Collect all content from chunks and sort by line number
|
|
1342
|
+
const sortedChunks = fileChunks
|
|
1343
|
+
.filter(c => c.content)
|
|
1344
|
+
.sort((a, b) => a.start_line - b.start_line);
|
|
1345
|
+
|
|
1346
|
+
if (sortedChunks.length === 0) {
|
|
1347
|
+
container.innerHTML = '<p style="color: #8b949e;">File content not available</p>';
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Combine all chunks to show full file
|
|
1352
|
+
const fullContent = sortedChunks.map(c => c.content).join('\n\n');
|
|
1353
|
+
|
|
1354
|
+
container.innerHTML = `
|
|
1355
|
+
<p style="color: #8b949e; font-size: 11px; margin-bottom: 12px;">
|
|
1356
|
+
Contains ${fileChunks.length} code chunks
|
|
1357
|
+
</p>
|
|
1358
|
+
<pre><code>${escapeHtml(fullContent)}</code></pre>
|
|
1359
|
+
`;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function showImportDetails(node, container) {
|
|
1363
|
+
// L1 nodes are import statements
|
|
1364
|
+
const importHtml = `
|
|
1365
|
+
<div class="import-details">
|
|
1366
|
+
<div class="import-statement">${escapeHtml(node.name)}</div>
|
|
1367
|
+
<div class="detail-row">
|
|
1368
|
+
<span class="detail-label">File:</span> ${node.file_path}
|
|
1369
|
+
</div>
|
|
1370
|
+
${node.start_line ? `
|
|
1371
|
+
<div class="detail-row">
|
|
1372
|
+
<span class="detail-label">Location:</span> Lines ${node.start_line}-${node.end_line}
|
|
1373
|
+
</div>
|
|
1374
|
+
` : ''}
|
|
1375
|
+
${node.language ? `
|
|
1376
|
+
<div class="detail-row">
|
|
1377
|
+
<span class="detail-label">Language:</span> ${node.language}
|
|
1378
|
+
</div>
|
|
1379
|
+
` : ''}
|
|
1380
|
+
${node.content ? `
|
|
1381
|
+
<div style="margin-top: 16px;">
|
|
1382
|
+
<div class="detail-label" style="margin-bottom: 8px;">Import Statement:</div>
|
|
1383
|
+
<pre><code>${escapeHtml(node.content)}</code></pre>
|
|
1384
|
+
</div>
|
|
1385
|
+
` : ''}
|
|
1386
|
+
</div>
|
|
1387
|
+
`;
|
|
1388
|
+
|
|
1389
|
+
container.innerHTML = importHtml;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function showCodeContent(node, container) {
|
|
1393
|
+
// Show code for function, class, method, or code chunks
|
|
1394
|
+
let html = '';
|
|
1395
|
+
|
|
1396
|
+
if (node.docstring) {
|
|
1397
|
+
html += `
|
|
1398
|
+
<div style="margin-bottom: 16px; padding: 12px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;">
|
|
1399
|
+
<div style="font-size: 11px; color: #8b949e; margin-bottom: 8px; font-weight: 600;">DOCSTRING</div>
|
|
1400
|
+
<pre style="margin: 0; padding: 0; background: transparent; border: none;"><code>${escapeHtml(node.docstring)}</code></pre>
|
|
1401
|
+
</div>
|
|
1402
|
+
`;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (node.content) {
|
|
1406
|
+
html += `<pre><code>${escapeHtml(node.content)}</code></pre>`;
|
|
1407
|
+
} else {
|
|
1408
|
+
html += '<p style="color: #8b949e;">No content available</p>';
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
container.innerHTML = html;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function escapeHtml(text) {
|
|
1415
|
+
const div = document.createElement('div');
|
|
1416
|
+
div.textContent = text;
|
|
1417
|
+
return div.innerHTML;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function closeContentPane() {
|
|
1421
|
+
const pane = document.getElementById('content-pane');
|
|
1422
|
+
pane.classList.remove('visible');
|
|
1423
|
+
|
|
1424
|
+
// Remove highlight
|
|
1425
|
+
highlightedNode = null;
|
|
1426
|
+
renderGraph();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
805
1429
|
// Auto-load graph data on page load
|
|
806
1430
|
window.addEventListener('DOMContentLoaded', () => {
|
|
807
1431
|
const loadingEl = document.getElementById('loading');
|
|
@@ -827,7 +1451,7 @@ def _create_visualization_html(html_file: Path) -> None:
|
|
|
827
1451
|
});
|
|
828
1452
|
</script>
|
|
829
1453
|
</body>
|
|
830
|
-
</html>
|
|
1454
|
+
</html>"""
|
|
831
1455
|
|
|
832
1456
|
with open(html_file, "w") as f:
|
|
833
1457
|
f.write(html_content)
|