mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +111 -0
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +74 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +414 -0
- mcp_vector_search/analysis/reporters/__init__.py +7 -0
- mcp_vector_search/analysis/reporters/console.py +646 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +1062 -0
- mcp_vector_search/cli/commands/chat.py +1455 -0
- mcp_vector_search/cli/commands/index.py +621 -5
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +224 -8
- mcp_vector_search/cli/commands/setup.py +1184 -0
- mcp_vector_search/cli/commands/status.py +339 -5
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +292 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +600 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
- mcp_vector_search/cli/didyoumean.py +27 -2
- mcp_vector_search/cli/main.py +127 -160
- mcp_vector_search/cli/output.py +158 -13
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +273 -0
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +406 -94
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +632 -54
- mcp_vector_search/core/llm_client.py +756 -0
- mcp_vector_search/core/models.py +91 -1
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +819 -9
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore.py +0 -3
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
- mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,1467 +0,0 @@
|
|
|
1
|
-
"""Visualization commands for MCP Vector Search."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import shutil
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import typer
|
|
9
|
-
from loguru import logger
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.panel import Panel
|
|
12
|
-
|
|
13
|
-
from ...core.database import ChromaVectorDatabase
|
|
14
|
-
from ...core.embeddings import create_embedding_function
|
|
15
|
-
from ...core.project import ProjectManager
|
|
16
|
-
|
|
17
|
-
app = typer.Typer(
|
|
18
|
-
help="Visualize code chunk relationships",
|
|
19
|
-
no_args_is_help=True,
|
|
20
|
-
)
|
|
21
|
-
console = Console()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@app.command()
|
|
25
|
-
def export(
|
|
26
|
-
output: Path = typer.Option(
|
|
27
|
-
Path("chunk-graph.json"),
|
|
28
|
-
"--output",
|
|
29
|
-
"-o",
|
|
30
|
-
help="Output file for chunk relationship data",
|
|
31
|
-
),
|
|
32
|
-
file_path: str | None = typer.Option(
|
|
33
|
-
None,
|
|
34
|
-
"--file",
|
|
35
|
-
"-f",
|
|
36
|
-
help="Export only chunks from specific file (supports wildcards)",
|
|
37
|
-
),
|
|
38
|
-
) -> None:
|
|
39
|
-
"""Export chunk relationships as JSON for D3.js visualization.
|
|
40
|
-
|
|
41
|
-
Examples:
|
|
42
|
-
# Export all chunks
|
|
43
|
-
mcp-vector-search visualize export
|
|
44
|
-
|
|
45
|
-
# Export from specific file
|
|
46
|
-
mcp-vector-search visualize export --file src/main.py
|
|
47
|
-
|
|
48
|
-
# Custom output location
|
|
49
|
-
mcp-vector-search visualize export -o graph.json
|
|
50
|
-
"""
|
|
51
|
-
asyncio.run(_export_chunks(output, file_path))
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
async def _export_chunks(output: Path, file_filter: str | None) -> None:
|
|
55
|
-
"""Export chunk relationship data."""
|
|
56
|
-
try:
|
|
57
|
-
# Load project
|
|
58
|
-
project_manager = ProjectManager(Path.cwd())
|
|
59
|
-
|
|
60
|
-
if not project_manager.is_initialized():
|
|
61
|
-
console.print(
|
|
62
|
-
"[red]Project not initialized. Run 'mcp-vector-search init' first.[/red]"
|
|
63
|
-
)
|
|
64
|
-
raise typer.Exit(1)
|
|
65
|
-
|
|
66
|
-
config = project_manager.load_config()
|
|
67
|
-
|
|
68
|
-
# Get database
|
|
69
|
-
embedding_function, _ = create_embedding_function(config.embedding_model)
|
|
70
|
-
database = ChromaVectorDatabase(
|
|
71
|
-
persist_directory=config.index_path,
|
|
72
|
-
embedding_function=embedding_function,
|
|
73
|
-
)
|
|
74
|
-
await database.initialize()
|
|
75
|
-
|
|
76
|
-
# Get all chunks with metadata
|
|
77
|
-
console.print("[cyan]Fetching chunks from database...[/cyan]")
|
|
78
|
-
chunks = await database.get_all_chunks()
|
|
79
|
-
|
|
80
|
-
if len(chunks) == 0:
|
|
81
|
-
console.print(
|
|
82
|
-
"[yellow]No chunks found in index. Run 'mcp-vector-search index' first.[/yellow]"
|
|
83
|
-
)
|
|
84
|
-
raise typer.Exit(1)
|
|
85
|
-
|
|
86
|
-
console.print(f"[green]✓[/green] Retrieved {len(chunks)} chunks")
|
|
87
|
-
|
|
88
|
-
# Apply file filter if specified
|
|
89
|
-
if file_filter:
|
|
90
|
-
from fnmatch import fnmatch
|
|
91
|
-
|
|
92
|
-
chunks = [c for c in chunks if fnmatch(str(c.file_path), file_filter)]
|
|
93
|
-
console.print(
|
|
94
|
-
f"[cyan]Filtered to {len(chunks)} chunks matching '{file_filter}'[/cyan]"
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
# Collect subprojects for monorepo support
|
|
98
|
-
subprojects = {}
|
|
99
|
-
for chunk in chunks:
|
|
100
|
-
if chunk.subproject_name and chunk.subproject_name not in subprojects:
|
|
101
|
-
subprojects[chunk.subproject_name] = {
|
|
102
|
-
"name": chunk.subproject_name,
|
|
103
|
-
"path": chunk.subproject_path,
|
|
104
|
-
"color": _get_subproject_color(
|
|
105
|
-
chunk.subproject_name, len(subprojects)
|
|
106
|
-
),
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
# Build graph data structure
|
|
110
|
-
nodes = []
|
|
111
|
-
links = []
|
|
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
|
|
115
|
-
|
|
116
|
-
# Add subproject root nodes for monorepos
|
|
117
|
-
if subprojects:
|
|
118
|
-
console.print(
|
|
119
|
-
f"[cyan]Detected monorepo with {len(subprojects)} subprojects[/cyan]"
|
|
120
|
-
)
|
|
121
|
-
for sp_name, sp_data in subprojects.items():
|
|
122
|
-
node = {
|
|
123
|
-
"id": f"subproject_{sp_name}",
|
|
124
|
-
"name": sp_name,
|
|
125
|
-
"type": "subproject",
|
|
126
|
-
"file_path": sp_data["path"] or "",
|
|
127
|
-
"start_line": 0,
|
|
128
|
-
"end_line": 0,
|
|
129
|
-
"complexity": 0,
|
|
130
|
-
"color": sp_data["color"],
|
|
131
|
-
"depth": 0,
|
|
132
|
-
}
|
|
133
|
-
nodes.append(node)
|
|
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
|
-
|
|
218
|
-
# Add chunk nodes
|
|
219
|
-
for chunk in chunks:
|
|
220
|
-
node = {
|
|
221
|
-
"id": chunk.chunk_id or chunk.id,
|
|
222
|
-
"name": chunk.function_name
|
|
223
|
-
or chunk.class_name
|
|
224
|
-
or f"L{chunk.start_line}",
|
|
225
|
-
"type": chunk.chunk_type,
|
|
226
|
-
"file_path": str(chunk.file_path),
|
|
227
|
-
"start_line": chunk.start_line,
|
|
228
|
-
"end_line": chunk.end_line,
|
|
229
|
-
"complexity": chunk.complexity_score,
|
|
230
|
-
"parent_id": chunk.parent_chunk_id,
|
|
231
|
-
"depth": chunk.chunk_depth,
|
|
232
|
-
"content": chunk.content, # Add content for code viewer
|
|
233
|
-
"docstring": chunk.docstring,
|
|
234
|
-
"language": chunk.language,
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
# Add subproject info for monorepos
|
|
238
|
-
if chunk.subproject_name:
|
|
239
|
-
node["subproject"] = chunk.subproject_name
|
|
240
|
-
node["color"] = subprojects[chunk.subproject_name]["color"]
|
|
241
|
-
|
|
242
|
-
nodes.append(node)
|
|
243
|
-
chunk_id_map[node["id"]] = len(nodes) - 1
|
|
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
|
-
|
|
285
|
-
# Build hierarchical links from parent-child relationships
|
|
286
|
-
for chunk in chunks:
|
|
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
|
-
)
|
|
299
|
-
|
|
300
|
-
# Link to subproject root if in monorepo
|
|
301
|
-
if chunk.subproject_name and not chunk.parent_chunk_id:
|
|
302
|
-
links.append(
|
|
303
|
-
{
|
|
304
|
-
"source": f"subproject_{chunk.subproject_name}",
|
|
305
|
-
"target": chunk_id,
|
|
306
|
-
}
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
# Link to parent chunk
|
|
310
|
-
if chunk.parent_chunk_id and chunk.parent_chunk_id in chunk_id_map:
|
|
311
|
-
links.append(
|
|
312
|
-
{
|
|
313
|
-
"source": chunk.parent_chunk_id,
|
|
314
|
-
"target": chunk_id,
|
|
315
|
-
}
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
# Parse inter-project dependencies for monorepos
|
|
319
|
-
if subprojects:
|
|
320
|
-
console.print("[cyan]Parsing inter-project dependencies...[/cyan]")
|
|
321
|
-
dep_links = _parse_project_dependencies(
|
|
322
|
-
project_manager.project_root, subprojects
|
|
323
|
-
)
|
|
324
|
-
links.extend(dep_links)
|
|
325
|
-
if dep_links:
|
|
326
|
-
console.print(
|
|
327
|
-
f"[green]✓[/green] Found {len(dep_links)} inter-project dependencies"
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
# Get stats
|
|
331
|
-
stats = await database.get_stats()
|
|
332
|
-
|
|
333
|
-
# Build final graph data
|
|
334
|
-
graph_data = {
|
|
335
|
-
"nodes": nodes,
|
|
336
|
-
"links": links,
|
|
337
|
-
"metadata": {
|
|
338
|
-
"total_chunks": len(chunks),
|
|
339
|
-
"total_files": stats.total_files,
|
|
340
|
-
"languages": stats.languages,
|
|
341
|
-
"is_monorepo": len(subprojects) > 0,
|
|
342
|
-
"subprojects": list(subprojects.keys()) if subprojects else [],
|
|
343
|
-
},
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
# Write to file
|
|
347
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
348
|
-
with open(output, "w") as f:
|
|
349
|
-
json.dump(graph_data, f, indent=2)
|
|
350
|
-
|
|
351
|
-
await database.close()
|
|
352
|
-
|
|
353
|
-
console.print()
|
|
354
|
-
console.print(
|
|
355
|
-
Panel.fit(
|
|
356
|
-
f"[green]✓[/green] Exported graph data to [cyan]{output}[/cyan]\n\n"
|
|
357
|
-
f"Nodes: {len(graph_data['nodes'])}\n"
|
|
358
|
-
f"Links: {len(graph_data['links'])}\n"
|
|
359
|
-
f"{'Subprojects: ' + str(len(subprojects)) if subprojects else ''}\n\n"
|
|
360
|
-
f"[dim]Next: Run 'mcp-vector-search visualize serve' to view[/dim]",
|
|
361
|
-
title="Export Complete",
|
|
362
|
-
border_style="green",
|
|
363
|
-
)
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
except Exception as e:
|
|
367
|
-
logger.error(f"Export failed: {e}")
|
|
368
|
-
console.print(f"[red]✗ Export failed: {e}[/red]")
|
|
369
|
-
raise typer.Exit(1)
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
def _get_subproject_color(subproject_name: str, index: int) -> str:
|
|
373
|
-
"""Get a consistent color for a subproject."""
|
|
374
|
-
# Color palette for subprojects (GitHub-style colors)
|
|
375
|
-
colors = [
|
|
376
|
-
"#238636", # Green
|
|
377
|
-
"#1f6feb", # Blue
|
|
378
|
-
"#d29922", # Yellow
|
|
379
|
-
"#8957e5", # Purple
|
|
380
|
-
"#da3633", # Red
|
|
381
|
-
"#bf8700", # Orange
|
|
382
|
-
"#1a7f37", # Dark green
|
|
383
|
-
"#0969da", # Dark blue
|
|
384
|
-
]
|
|
385
|
-
return colors[index % len(colors)]
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[dict]:
|
|
389
|
-
"""Parse package.json files to find inter-project dependencies.
|
|
390
|
-
|
|
391
|
-
Args:
|
|
392
|
-
project_root: Root directory of the monorepo
|
|
393
|
-
subprojects: Dictionary of subproject information
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
List of dependency links between subprojects
|
|
397
|
-
"""
|
|
398
|
-
dependency_links = []
|
|
399
|
-
|
|
400
|
-
for sp_name, sp_data in subprojects.items():
|
|
401
|
-
package_json = project_root / sp_data["path"] / "package.json"
|
|
402
|
-
|
|
403
|
-
if not package_json.exists():
|
|
404
|
-
continue
|
|
405
|
-
|
|
406
|
-
try:
|
|
407
|
-
with open(package_json) as f:
|
|
408
|
-
package_data = json.load(f)
|
|
409
|
-
|
|
410
|
-
# Check all dependency types
|
|
411
|
-
all_deps = {}
|
|
412
|
-
for dep_type in ["dependencies", "devDependencies", "peerDependencies"]:
|
|
413
|
-
if dep_type in package_data:
|
|
414
|
-
all_deps.update(package_data[dep_type])
|
|
415
|
-
|
|
416
|
-
# Find dependencies on other subprojects
|
|
417
|
-
for dep_name in all_deps.keys():
|
|
418
|
-
# Check if this dependency is another subproject
|
|
419
|
-
for other_sp_name in subprojects.keys():
|
|
420
|
-
if other_sp_name != sp_name and dep_name == other_sp_name:
|
|
421
|
-
# Found inter-project dependency
|
|
422
|
-
dependency_links.append(
|
|
423
|
-
{
|
|
424
|
-
"source": f"subproject_{sp_name}",
|
|
425
|
-
"target": f"subproject_{other_sp_name}",
|
|
426
|
-
"type": "dependency",
|
|
427
|
-
}
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
except Exception as e:
|
|
431
|
-
logger.debug(f"Failed to parse {package_json}: {e}")
|
|
432
|
-
continue
|
|
433
|
-
|
|
434
|
-
return dependency_links
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
@app.command()
|
|
438
|
-
def serve(
|
|
439
|
-
port: int = typer.Option(
|
|
440
|
-
8080, "--port", "-p", help="Port for visualization server"
|
|
441
|
-
),
|
|
442
|
-
graph_file: Path = typer.Option(
|
|
443
|
-
Path("chunk-graph.json"),
|
|
444
|
-
"--graph",
|
|
445
|
-
"-g",
|
|
446
|
-
help="Graph JSON file to visualize",
|
|
447
|
-
),
|
|
448
|
-
) -> None:
|
|
449
|
-
"""Start local HTTP server for D3.js visualization.
|
|
450
|
-
|
|
451
|
-
Examples:
|
|
452
|
-
# Start server on default port 8080
|
|
453
|
-
mcp-vector-search visualize serve
|
|
454
|
-
|
|
455
|
-
# Custom port
|
|
456
|
-
mcp-vector-search visualize serve --port 3000
|
|
457
|
-
|
|
458
|
-
# Custom graph file
|
|
459
|
-
mcp-vector-search visualize serve --graph my-graph.json
|
|
460
|
-
"""
|
|
461
|
-
import http.server
|
|
462
|
-
import os
|
|
463
|
-
import socket
|
|
464
|
-
import socketserver
|
|
465
|
-
import webbrowser
|
|
466
|
-
|
|
467
|
-
# Find free port in range 8080-8099
|
|
468
|
-
def find_free_port(start_port: int = 8080, end_port: int = 8099) -> int:
|
|
469
|
-
"""Find a free port in the given range."""
|
|
470
|
-
for test_port in range(start_port, end_port + 1):
|
|
471
|
-
try:
|
|
472
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
473
|
-
s.bind(("", test_port))
|
|
474
|
-
return test_port
|
|
475
|
-
except OSError:
|
|
476
|
-
continue
|
|
477
|
-
raise OSError(f"No free ports available in range {start_port}-{end_port}")
|
|
478
|
-
|
|
479
|
-
# Use specified port or find free one
|
|
480
|
-
if port == 8080: # Default port, try to find free one
|
|
481
|
-
try:
|
|
482
|
-
port = find_free_port(8080, 8099)
|
|
483
|
-
except OSError as e:
|
|
484
|
-
console.print(f"[red]✗ {e}[/red]")
|
|
485
|
-
raise typer.Exit(1)
|
|
486
|
-
|
|
487
|
-
# Get visualization directory
|
|
488
|
-
viz_dir = Path(__file__).parent.parent.parent / "visualization"
|
|
489
|
-
|
|
490
|
-
if not viz_dir.exists():
|
|
491
|
-
console.print(
|
|
492
|
-
f"[yellow]Visualization directory not found. Creating at {viz_dir}...[/yellow]"
|
|
493
|
-
)
|
|
494
|
-
viz_dir.mkdir(parents=True, exist_ok=True)
|
|
495
|
-
|
|
496
|
-
# Create index.html if it doesn't exist
|
|
497
|
-
html_file = viz_dir / "index.html"
|
|
498
|
-
if not html_file.exists():
|
|
499
|
-
console.print("[yellow]Creating visualization HTML file...[/yellow]")
|
|
500
|
-
_create_visualization_html(html_file)
|
|
501
|
-
|
|
502
|
-
# Copy graph file to visualization directory if it exists
|
|
503
|
-
if graph_file.exists():
|
|
504
|
-
dest = viz_dir / "chunk-graph.json"
|
|
505
|
-
shutil.copy(graph_file, dest)
|
|
506
|
-
console.print(f"[green]✓[/green] Copied graph data to {dest}")
|
|
507
|
-
else:
|
|
508
|
-
# Auto-generate graph file if it doesn't exist
|
|
509
|
-
console.print(
|
|
510
|
-
f"[yellow]Graph file {graph_file} not found. Generating it now...[/yellow]"
|
|
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}")
|
|
520
|
-
|
|
521
|
-
# Change to visualization directory
|
|
522
|
-
os.chdir(viz_dir)
|
|
523
|
-
|
|
524
|
-
# Start server
|
|
525
|
-
handler = http.server.SimpleHTTPRequestHandler
|
|
526
|
-
try:
|
|
527
|
-
with socketserver.TCPServer(("", port), handler) as httpd:
|
|
528
|
-
url = f"http://localhost:{port}"
|
|
529
|
-
console.print()
|
|
530
|
-
console.print(
|
|
531
|
-
Panel.fit(
|
|
532
|
-
f"[green]✓[/green] Visualization server running\n\n"
|
|
533
|
-
f"URL: [cyan]{url}[/cyan]\n"
|
|
534
|
-
f"Directory: [dim]{viz_dir}[/dim]\n\n"
|
|
535
|
-
f"[dim]Press Ctrl+C to stop[/dim]",
|
|
536
|
-
title="Server Started",
|
|
537
|
-
border_style="green",
|
|
538
|
-
)
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
# Open browser
|
|
542
|
-
webbrowser.open(url)
|
|
543
|
-
|
|
544
|
-
try:
|
|
545
|
-
httpd.serve_forever()
|
|
546
|
-
except KeyboardInterrupt:
|
|
547
|
-
console.print("\n[yellow]Stopping server...[/yellow]")
|
|
548
|
-
|
|
549
|
-
except OSError as e:
|
|
550
|
-
if "Address already in use" in str(e):
|
|
551
|
-
console.print(
|
|
552
|
-
f"[red]✗ Port {port} is already in use. Try a different port with --port[/red]"
|
|
553
|
-
)
|
|
554
|
-
else:
|
|
555
|
-
console.print(f"[red]✗ Server error: {e}[/red]")
|
|
556
|
-
raise typer.Exit(1)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
def _create_visualization_html(html_file: Path) -> None:
|
|
560
|
-
"""Create the D3.js visualization HTML file."""
|
|
561
|
-
html_content = """<!DOCTYPE html>
|
|
562
|
-
<html>
|
|
563
|
-
<head>
|
|
564
|
-
<meta charset="utf-8">
|
|
565
|
-
<title>Code Chunk Relationship Graph</title>
|
|
566
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
567
|
-
<style>
|
|
568
|
-
body {
|
|
569
|
-
margin: 0;
|
|
570
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
571
|
-
background: #0d1117;
|
|
572
|
-
color: #c9d1d9;
|
|
573
|
-
overflow: hidden;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
#controls {
|
|
577
|
-
position: absolute;
|
|
578
|
-
top: 20px;
|
|
579
|
-
left: 20px;
|
|
580
|
-
background: rgba(13, 17, 23, 0.95);
|
|
581
|
-
border: 1px solid #30363d;
|
|
582
|
-
border-radius: 6px;
|
|
583
|
-
padding: 16px;
|
|
584
|
-
min-width: 250px;
|
|
585
|
-
max-height: 80vh;
|
|
586
|
-
overflow-y: auto;
|
|
587
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
h1 { margin: 0 0 16px 0; font-size: 18px; }
|
|
591
|
-
h3 { margin: 16px 0 8px 0; font-size: 14px; color: #8b949e; }
|
|
592
|
-
|
|
593
|
-
.control-group {
|
|
594
|
-
margin-bottom: 12px;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
label {
|
|
598
|
-
display: block;
|
|
599
|
-
margin-bottom: 4px;
|
|
600
|
-
font-size: 12px;
|
|
601
|
-
color: #8b949e;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
input[type="file"] {
|
|
605
|
-
width: 100%;
|
|
606
|
-
padding: 6px;
|
|
607
|
-
background: #161b22;
|
|
608
|
-
border: 1px solid #30363d;
|
|
609
|
-
border-radius: 6px;
|
|
610
|
-
color: #c9d1d9;
|
|
611
|
-
font-size: 12px;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
.legend {
|
|
615
|
-
font-size: 12px;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
.legend-item {
|
|
619
|
-
margin: 4px 0;
|
|
620
|
-
display: flex;
|
|
621
|
-
align-items: center;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
.legend-color {
|
|
625
|
-
width: 12px;
|
|
626
|
-
height: 12px;
|
|
627
|
-
border-radius: 50%;
|
|
628
|
-
margin-right: 8px;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
#graph {
|
|
632
|
-
width: 100vw;
|
|
633
|
-
height: 100vh;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
.node circle {
|
|
637
|
-
cursor: pointer;
|
|
638
|
-
stroke: #c9d1d9;
|
|
639
|
-
stroke-width: 1.5px;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
.node.module circle { fill: #238636; }
|
|
643
|
-
.node.class circle { fill: #1f6feb; }
|
|
644
|
-
.node.function circle { fill: #d29922; }
|
|
645
|
-
.node.method circle { fill: #8957e5; }
|
|
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
|
-
}
|
|
661
|
-
.node.subproject circle { fill: #da3633; stroke-width: 3px; }
|
|
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
|
-
|
|
671
|
-
.node text {
|
|
672
|
-
font-size: 11px;
|
|
673
|
-
fill: #c9d1d9;
|
|
674
|
-
text-anchor: middle;
|
|
675
|
-
pointer-events: none;
|
|
676
|
-
user-select: none;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
.link {
|
|
680
|
-
stroke: #30363d;
|
|
681
|
-
stroke-opacity: 0.6;
|
|
682
|
-
stroke-width: 1.5px;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
.link.dependency {
|
|
686
|
-
stroke: #d29922;
|
|
687
|
-
stroke-opacity: 0.8;
|
|
688
|
-
stroke-width: 2px;
|
|
689
|
-
stroke-dasharray: 5,5;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
.tooltip {
|
|
693
|
-
position: absolute;
|
|
694
|
-
padding: 12px;
|
|
695
|
-
background: rgba(13, 17, 23, 0.95);
|
|
696
|
-
border: 1px solid #30363d;
|
|
697
|
-
border-radius: 6px;
|
|
698
|
-
pointer-events: none;
|
|
699
|
-
display: none;
|
|
700
|
-
font-size: 12px;
|
|
701
|
-
max-width: 300px;
|
|
702
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
.stats {
|
|
706
|
-
margin-top: 16px;
|
|
707
|
-
padding-top: 16px;
|
|
708
|
-
border-top: 1px solid #30363d;
|
|
709
|
-
font-size: 12px;
|
|
710
|
-
color: #8b949e;
|
|
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
|
-
}
|
|
852
|
-
</style>
|
|
853
|
-
</head>
|
|
854
|
-
<body>
|
|
855
|
-
<div id="controls">
|
|
856
|
-
<h1>🔍 Code Graph</h1>
|
|
857
|
-
|
|
858
|
-
<div class="control-group" id="loading">
|
|
859
|
-
<label>⏳ Loading graph data...</label>
|
|
860
|
-
</div>
|
|
861
|
-
|
|
862
|
-
<h3>Legend</h3>
|
|
863
|
-
<div class="legend">
|
|
864
|
-
<div class="legend-item">
|
|
865
|
-
<span class="legend-color" style="background: #da3633;"></span> Subproject
|
|
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>
|
|
873
|
-
<div class="legend-item">
|
|
874
|
-
<span class="legend-color" style="background: #238636;"></span> Module
|
|
875
|
-
</div>
|
|
876
|
-
<div class="legend-item">
|
|
877
|
-
<span class="legend-color" style="background: #1f6feb;"></span> Class
|
|
878
|
-
</div>
|
|
879
|
-
<div class="legend-item">
|
|
880
|
-
<span class="legend-color" style="background: #d29922;"></span> Function
|
|
881
|
-
</div>
|
|
882
|
-
<div class="legend-item">
|
|
883
|
-
<span class="legend-color" style="background: #8957e5;"></span> Method
|
|
884
|
-
</div>
|
|
885
|
-
<div class="legend-item">
|
|
886
|
-
<span class="legend-color" style="background: #6e7681;"></span> Code
|
|
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>
|
|
897
|
-
</div>
|
|
898
|
-
|
|
899
|
-
<div id="subprojects-legend" style="display: none;">
|
|
900
|
-
<h3>Subprojects</h3>
|
|
901
|
-
<div class="legend" id="subprojects-list"></div>
|
|
902
|
-
</div>
|
|
903
|
-
|
|
904
|
-
<div class="stats" id="stats"></div>
|
|
905
|
-
</div>
|
|
906
|
-
|
|
907
|
-
<svg id="graph"></svg>
|
|
908
|
-
<div id="tooltip" class="tooltip"></div>
|
|
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
|
-
|
|
919
|
-
<script>
|
|
920
|
-
const width = window.innerWidth;
|
|
921
|
-
const height = window.innerHeight;
|
|
922
|
-
|
|
923
|
-
const svg = d3.select("#graph")
|
|
924
|
-
.attr("width", width)
|
|
925
|
-
.attr("height", height)
|
|
926
|
-
.call(d3.zoom().on("zoom", (event) => {
|
|
927
|
-
g.attr("transform", event.transform);
|
|
928
|
-
}));
|
|
929
|
-
|
|
930
|
-
const g = svg.append("g");
|
|
931
|
-
const tooltip = d3.select("#tooltip");
|
|
932
|
-
let simulation;
|
|
933
|
-
let allNodes = [];
|
|
934
|
-
let allLinks = [];
|
|
935
|
-
let visibleNodes = new Set();
|
|
936
|
-
let collapsedNodes = new Set();
|
|
937
|
-
let highlightedNode = null;
|
|
938
|
-
|
|
939
|
-
function visualizeGraph(data) {
|
|
940
|
-
g.selectAll("*").remove();
|
|
941
|
-
|
|
942
|
-
allNodes = data.nodes;
|
|
943
|
-
allLinks = data.links;
|
|
944
|
-
|
|
945
|
-
// Find root nodes - start with only top-level nodes
|
|
946
|
-
let rootNodes;
|
|
947
|
-
if (data.metadata && data.metadata.is_monorepo) {
|
|
948
|
-
// In monorepos, subproject nodes are roots
|
|
949
|
-
rootNodes = allNodes.filter(n => n.type === 'subproject');
|
|
950
|
-
} else {
|
|
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
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Start with only root nodes visible, all collapsed
|
|
976
|
-
visibleNodes = new Set(rootNodes.map(n => n.id));
|
|
977
|
-
collapsedNodes = new Set(rootNodes.map(n => n.id));
|
|
978
|
-
|
|
979
|
-
renderGraph();
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
function renderGraph() {
|
|
983
|
-
const visibleNodesList = allNodes.filter(n => visibleNodes.has(n.id));
|
|
984
|
-
const visibleLinks = allLinks.filter(l =>
|
|
985
|
-
visibleNodes.has(l.source.id || l.source) &&
|
|
986
|
-
visibleNodes.has(l.target.id || l.target)
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
simulation = d3.forceSimulation(visibleNodesList)
|
|
990
|
-
.force("link", d3.forceLink(visibleLinks).id(d => d.id).distance(100))
|
|
991
|
-
.force("charge", d3.forceManyBody().strength(-400))
|
|
992
|
-
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
993
|
-
.force("collision", d3.forceCollide().radius(40));
|
|
994
|
-
|
|
995
|
-
g.selectAll("*").remove();
|
|
996
|
-
|
|
997
|
-
const link = g.append("g")
|
|
998
|
-
.selectAll("line")
|
|
999
|
-
.data(visibleLinks)
|
|
1000
|
-
.join("line")
|
|
1001
|
-
.attr("class", d => d.type === "dependency" ? "link dependency" : "link");
|
|
1002
|
-
|
|
1003
|
-
const node = g.append("g")
|
|
1004
|
-
.selectAll("g")
|
|
1005
|
-
.data(visibleNodesList)
|
|
1006
|
-
.join("g")
|
|
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
|
-
})
|
|
1014
|
-
.call(drag(simulation))
|
|
1015
|
-
.on("click", handleNodeClick)
|
|
1016
|
-
.on("mouseover", showTooltip)
|
|
1017
|
-
.on("mouseout", hideTooltip);
|
|
1018
|
-
|
|
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")
|
|
1025
|
-
.attr("r", d => {
|
|
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
|
|
1029
|
-
return d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
|
|
1030
|
-
})
|
|
1031
|
-
.attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
|
|
1032
|
-
.attr("stroke-width", d => hasChildren(d) ? 2 : 0)
|
|
1033
|
-
.style("fill", d => d.color || null); // Use custom color if available
|
|
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
|
-
|
|
1060
|
-
// Add expand/collapse indicator
|
|
1061
|
-
node.filter(d => hasChildren(d))
|
|
1062
|
-
.append("text")
|
|
1063
|
-
.attr("class", "expand-indicator")
|
|
1064
|
-
.attr("text-anchor", "middle")
|
|
1065
|
-
.attr("dy", 5)
|
|
1066
|
-
.style("font-size", "16px")
|
|
1067
|
-
.style("font-weight", "bold")
|
|
1068
|
-
.style("fill", "#ffffff")
|
|
1069
|
-
.style("pointer-events", "none")
|
|
1070
|
-
.text(d => collapsedNodes.has(d.id) ? "+" : "−");
|
|
1071
|
-
|
|
1072
|
-
// Add labels (show actual import statement for L1 nodes)
|
|
1073
|
-
node.append("text")
|
|
1074
|
-
.text(d => {
|
|
1075
|
-
// L1 (depth 1) nodes are imports
|
|
1076
|
-
if (d.depth === 1 && d.type !== 'directory' && d.type !== 'file') {
|
|
1077
|
-
if (d.content) {
|
|
1078
|
-
// Extract first line of import statement
|
|
1079
|
-
const importLine = d.content.split('\n')[0].trim();
|
|
1080
|
-
// Truncate if too long (max 60 chars)
|
|
1081
|
-
return importLine.length > 60 ? importLine.substring(0, 57) + '...' : importLine;
|
|
1082
|
-
}
|
|
1083
|
-
return d.name; // Fallback to name if no content
|
|
1084
|
-
}
|
|
1085
|
-
return d.name;
|
|
1086
|
-
})
|
|
1087
|
-
.attr("dy", 30);
|
|
1088
|
-
|
|
1089
|
-
simulation.on("tick", () => {
|
|
1090
|
-
link
|
|
1091
|
-
.attr("x1", d => d.source.x)
|
|
1092
|
-
.attr("y1", d => d.source.y)
|
|
1093
|
-
.attr("x2", d => d.target.x)
|
|
1094
|
-
.attr("y2", d => d.target.y);
|
|
1095
|
-
|
|
1096
|
-
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
updateStats({nodes: visibleNodesList, links: visibleLinks, metadata: {total_files: allNodes.length}});
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function hasChildren(node) {
|
|
1103
|
-
return allLinks.some(l => (l.source.id || l.source) === node.id);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
function handleNodeClick(event, d) {
|
|
1107
|
-
event.stopPropagation();
|
|
1108
|
-
|
|
1109
|
-
// Always show content pane when clicking any node
|
|
1110
|
-
showContentPane(d);
|
|
1111
|
-
|
|
1112
|
-
// If node has children, also toggle expansion
|
|
1113
|
-
if (hasChildren(d)) {
|
|
1114
|
-
if (collapsedNodes.has(d.id)) {
|
|
1115
|
-
expandNode(d);
|
|
1116
|
-
} else {
|
|
1117
|
-
collapseNode(d);
|
|
1118
|
-
}
|
|
1119
|
-
renderGraph();
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function expandNode(node) {
|
|
1124
|
-
collapsedNodes.delete(node.id);
|
|
1125
|
-
|
|
1126
|
-
// Find direct children
|
|
1127
|
-
const children = allLinks
|
|
1128
|
-
.filter(l => (l.source.id || l.source) === node.id)
|
|
1129
|
-
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
1130
|
-
.filter(n => n);
|
|
1131
|
-
|
|
1132
|
-
children.forEach(child => {
|
|
1133
|
-
visibleNodes.add(child.id);
|
|
1134
|
-
collapsedNodes.add(child.id); // Children start collapsed
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
function collapseNode(node) {
|
|
1139
|
-
collapsedNodes.add(node.id);
|
|
1140
|
-
|
|
1141
|
-
// Hide all descendants recursively
|
|
1142
|
-
function hideDescendants(parentId) {
|
|
1143
|
-
const children = allLinks
|
|
1144
|
-
.filter(l => (l.source.id || l.source) === parentId)
|
|
1145
|
-
.map(l => l.target.id || l.target);
|
|
1146
|
-
|
|
1147
|
-
children.forEach(childId => {
|
|
1148
|
-
visibleNodes.delete(childId);
|
|
1149
|
-
collapsedNodes.delete(childId);
|
|
1150
|
-
hideDescendants(childId);
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
hideDescendants(node.id);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function showTooltip(event, d) {
|
|
1158
|
-
tooltip
|
|
1159
|
-
.style("display", "block")
|
|
1160
|
-
.style("left", (event.pageX + 10) + "px")
|
|
1161
|
-
.style("top", (event.pageY + 10) + "px")
|
|
1162
|
-
.html(`
|
|
1163
|
-
<div><strong>${d.name}</strong></div>
|
|
1164
|
-
<div>Type: ${d.type}</div>
|
|
1165
|
-
${d.complexity ? `<div>Complexity: ${d.complexity.toFixed(1)}</div>` : ''}
|
|
1166
|
-
${d.start_line ? `<div>Lines: ${d.start_line}-${d.end_line}</div>` : ''}
|
|
1167
|
-
<div>File: ${d.file_path}</div>
|
|
1168
|
-
`);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function hideTooltip() {
|
|
1172
|
-
tooltip.style("display", "none");
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
function drag(simulation) {
|
|
1176
|
-
function dragstarted(event) {
|
|
1177
|
-
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
1178
|
-
event.subject.fx = event.subject.x;
|
|
1179
|
-
event.subject.fy = event.subject.y;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function dragged(event) {
|
|
1183
|
-
event.subject.fx = event.x;
|
|
1184
|
-
event.subject.fy = event.y;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
function dragended(event) {
|
|
1188
|
-
if (!event.active) simulation.alphaTarget(0);
|
|
1189
|
-
event.subject.fx = null;
|
|
1190
|
-
event.subject.fy = null;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
return d3.drag()
|
|
1194
|
-
.on("start", dragstarted)
|
|
1195
|
-
.on("drag", dragged)
|
|
1196
|
-
.on("end", dragended);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
function updateStats(data) {
|
|
1200
|
-
const stats = d3.select("#stats");
|
|
1201
|
-
stats.html(`
|
|
1202
|
-
<div>Nodes: ${data.nodes.length}</div>
|
|
1203
|
-
<div>Links: ${data.links.length}</div>
|
|
1204
|
-
${data.metadata ? `<div>Files: ${data.metadata.total_files || 'N/A'}</div>` : ''}
|
|
1205
|
-
${data.metadata && data.metadata.is_monorepo ? `<div>Monorepo: ${data.metadata.subprojects.length} subprojects</div>` : ''}
|
|
1206
|
-
`);
|
|
1207
|
-
|
|
1208
|
-
// Show subproject legend if monorepo
|
|
1209
|
-
if (data.metadata && data.metadata.is_monorepo && data.metadata.subprojects.length > 0) {
|
|
1210
|
-
const subprojectsLegend = d3.select("#subprojects-legend");
|
|
1211
|
-
const subprojectsList = d3.select("#subprojects-list");
|
|
1212
|
-
|
|
1213
|
-
subprojectsLegend.style("display", "block");
|
|
1214
|
-
|
|
1215
|
-
// Get subproject nodes with colors
|
|
1216
|
-
const subprojectNodes = allNodes.filter(n => n.type === 'subproject');
|
|
1217
|
-
|
|
1218
|
-
subprojectsList.html(
|
|
1219
|
-
subprojectNodes.map(sp =>
|
|
1220
|
-
`<div class="legend-item">
|
|
1221
|
-
<span class="legend-color" style="background: ${sp.color};"></span> ${sp.name}
|
|
1222
|
-
</div>`
|
|
1223
|
-
).join('')
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
function showContentPane(node) {
|
|
1229
|
-
// Highlight the node
|
|
1230
|
-
highlightedNode = node;
|
|
1231
|
-
renderGraph();
|
|
1232
|
-
|
|
1233
|
-
// Populate content pane
|
|
1234
|
-
const pane = document.getElementById('content-pane');
|
|
1235
|
-
const title = document.getElementById('pane-title');
|
|
1236
|
-
const meta = document.getElementById('pane-meta');
|
|
1237
|
-
const content = document.getElementById('pane-content');
|
|
1238
|
-
|
|
1239
|
-
// Set title with actual import statement for L1 nodes
|
|
1240
|
-
if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
|
|
1241
|
-
if (node.content) {
|
|
1242
|
-
const importLine = node.content.split('\n')[0].trim();
|
|
1243
|
-
title.textContent = importLine;
|
|
1244
|
-
} else {
|
|
1245
|
-
title.textContent = `Import: ${node.name}`;
|
|
1246
|
-
}
|
|
1247
|
-
} else {
|
|
1248
|
-
title.textContent = node.name;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
// Set metadata
|
|
1252
|
-
let metaText = `${node.type} • ${node.file_path}`;
|
|
1253
|
-
if (node.start_line) {
|
|
1254
|
-
metaText += ` • Lines ${node.start_line}-${node.end_line}`;
|
|
1255
|
-
}
|
|
1256
|
-
if (node.language) {
|
|
1257
|
-
metaText += ` • ${node.language}`;
|
|
1258
|
-
}
|
|
1259
|
-
meta.textContent = metaText;
|
|
1260
|
-
|
|
1261
|
-
// Display content based on node type
|
|
1262
|
-
if (node.type === 'directory') {
|
|
1263
|
-
showDirectoryContents(node, content);
|
|
1264
|
-
} else if (node.type === 'file') {
|
|
1265
|
-
showFileContents(node, content);
|
|
1266
|
-
} else if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
|
|
1267
|
-
// L1 nodes are imports
|
|
1268
|
-
showImportDetails(node, content);
|
|
1269
|
-
} else {
|
|
1270
|
-
// Class, function, method, code nodes
|
|
1271
|
-
showCodeContent(node, content);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
pane.classList.add('visible');
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
function showDirectoryContents(node, container) {
|
|
1278
|
-
// Find all direct children of this directory
|
|
1279
|
-
const children = allLinks
|
|
1280
|
-
.filter(l => (l.source.id || l.source) === node.id)
|
|
1281
|
-
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
1282
|
-
.filter(n => n);
|
|
1283
|
-
|
|
1284
|
-
if (children.length === 0) {
|
|
1285
|
-
container.innerHTML = '<p style="color: #8b949e;">Empty directory</p>';
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// Group by type
|
|
1290
|
-
const files = children.filter(n => n.type === 'file');
|
|
1291
|
-
const subdirs = children.filter(n => n.type === 'directory');
|
|
1292
|
-
const chunks = children.filter(n => n.type !== 'file' && n.type !== 'directory');
|
|
1293
|
-
|
|
1294
|
-
let html = '<ul class="directory-list">';
|
|
1295
|
-
|
|
1296
|
-
// Show subdirectories first
|
|
1297
|
-
subdirs.forEach(child => {
|
|
1298
|
-
html += `
|
|
1299
|
-
<li>
|
|
1300
|
-
<span class="item-icon">📁</span>
|
|
1301
|
-
${child.name}
|
|
1302
|
-
<span class="item-type">directory</span>
|
|
1303
|
-
</li>
|
|
1304
|
-
`;
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
// Then files
|
|
1308
|
-
files.forEach(child => {
|
|
1309
|
-
html += `
|
|
1310
|
-
<li>
|
|
1311
|
-
<span class="item-icon">📄</span>
|
|
1312
|
-
${child.name}
|
|
1313
|
-
<span class="item-type">file</span>
|
|
1314
|
-
</li>
|
|
1315
|
-
`;
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
// Then code chunks
|
|
1319
|
-
chunks.forEach(child => {
|
|
1320
|
-
const icon = child.type === 'class' ? '🔷' : child.type === 'function' ? '⚡' : '📝';
|
|
1321
|
-
html += `
|
|
1322
|
-
<li>
|
|
1323
|
-
<span class="item-icon">${icon}</span>
|
|
1324
|
-
${child.name}
|
|
1325
|
-
<span class="item-type">${child.type}</span>
|
|
1326
|
-
</li>
|
|
1327
|
-
`;
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
html += '</ul>';
|
|
1331
|
-
|
|
1332
|
-
// Add summary
|
|
1333
|
-
const summary = `<p style="color: #8b949e; font-size: 11px; margin-top: 16px;">
|
|
1334
|
-
Total: ${children.length} items (${subdirs.length} directories, ${files.length} files, ${chunks.length} code chunks)
|
|
1335
|
-
</p>`;
|
|
1336
|
-
|
|
1337
|
-
container.innerHTML = html + summary;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
function showFileContents(node, container) {
|
|
1341
|
-
// Find all chunks in this file
|
|
1342
|
-
const fileChunks = allLinks
|
|
1343
|
-
.filter(l => (l.source.id || l.source) === node.id)
|
|
1344
|
-
.map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
|
|
1345
|
-
.filter(n => n);
|
|
1346
|
-
|
|
1347
|
-
if (fileChunks.length === 0) {
|
|
1348
|
-
container.innerHTML = '<p style="color: #8b949e;">No code chunks found in this file</p>';
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
// Collect all content from chunks and sort by line number
|
|
1353
|
-
const sortedChunks = fileChunks
|
|
1354
|
-
.filter(c => c.content)
|
|
1355
|
-
.sort((a, b) => a.start_line - b.start_line);
|
|
1356
|
-
|
|
1357
|
-
if (sortedChunks.length === 0) {
|
|
1358
|
-
container.innerHTML = '<p style="color: #8b949e;">File content not available</p>';
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// Combine all chunks to show full file
|
|
1363
|
-
const fullContent = sortedChunks.map(c => c.content).join('\n\n');
|
|
1364
|
-
|
|
1365
|
-
container.innerHTML = `
|
|
1366
|
-
<p style="color: #8b949e; font-size: 11px; margin-bottom: 12px;">
|
|
1367
|
-
Contains ${fileChunks.length} code chunks
|
|
1368
|
-
</p>
|
|
1369
|
-
<pre><code>${escapeHtml(fullContent)}</code></pre>
|
|
1370
|
-
`;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
function showImportDetails(node, container) {
|
|
1374
|
-
// L1 nodes are import statements - show import content prominently
|
|
1375
|
-
const importHtml = `
|
|
1376
|
-
<div class="import-details">
|
|
1377
|
-
${node.content ? `
|
|
1378
|
-
<div style="margin-bottom: 16px;">
|
|
1379
|
-
<div class="detail-label" style="margin-bottom: 8px;">Import Statement:</div>
|
|
1380
|
-
<pre><code>${escapeHtml(node.content)}</code></pre>
|
|
1381
|
-
</div>
|
|
1382
|
-
` : '<p style="color: #8b949e;">No import content available</p>'}
|
|
1383
|
-
<div class="detail-row">
|
|
1384
|
-
<span class="detail-label">File:</span> ${node.file_path}
|
|
1385
|
-
</div>
|
|
1386
|
-
${node.start_line ? `
|
|
1387
|
-
<div class="detail-row">
|
|
1388
|
-
<span class="detail-label">Location:</span> Lines ${node.start_line}-${node.end_line}
|
|
1389
|
-
</div>
|
|
1390
|
-
` : ''}
|
|
1391
|
-
${node.language ? `
|
|
1392
|
-
<div class="detail-row">
|
|
1393
|
-
<span class="detail-label">Language:</span> ${node.language}
|
|
1394
|
-
</div>
|
|
1395
|
-
` : ''}
|
|
1396
|
-
</div>
|
|
1397
|
-
`;
|
|
1398
|
-
|
|
1399
|
-
container.innerHTML = importHtml;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
function showCodeContent(node, container) {
|
|
1403
|
-
// Show code for function, class, method, or code chunks
|
|
1404
|
-
let html = '';
|
|
1405
|
-
|
|
1406
|
-
if (node.docstring) {
|
|
1407
|
-
html += `
|
|
1408
|
-
<div style="margin-bottom: 16px; padding: 12px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;">
|
|
1409
|
-
<div style="font-size: 11px; color: #8b949e; margin-bottom: 8px; font-weight: 600;">DOCSTRING</div>
|
|
1410
|
-
<pre style="margin: 0; padding: 0; background: transparent; border: none;"><code>${escapeHtml(node.docstring)}</code></pre>
|
|
1411
|
-
</div>
|
|
1412
|
-
`;
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
if (node.content) {
|
|
1416
|
-
html += `<pre><code>${escapeHtml(node.content)}</code></pre>`;
|
|
1417
|
-
} else {
|
|
1418
|
-
html += '<p style="color: #8b949e;">No content available</p>';
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
container.innerHTML = html;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
function escapeHtml(text) {
|
|
1425
|
-
const div = document.createElement('div');
|
|
1426
|
-
div.textContent = text;
|
|
1427
|
-
return div.innerHTML;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
function closeContentPane() {
|
|
1431
|
-
const pane = document.getElementById('content-pane');
|
|
1432
|
-
pane.classList.remove('visible');
|
|
1433
|
-
|
|
1434
|
-
// Remove highlight
|
|
1435
|
-
highlightedNode = null;
|
|
1436
|
-
renderGraph();
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// Auto-load graph data on page load
|
|
1440
|
-
window.addEventListener('DOMContentLoaded', () => {
|
|
1441
|
-
const loadingEl = document.getElementById('loading');
|
|
1442
|
-
|
|
1443
|
-
fetch("chunk-graph.json")
|
|
1444
|
-
.then(response => {
|
|
1445
|
-
if (!response.ok) {
|
|
1446
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1447
|
-
}
|
|
1448
|
-
return response.json();
|
|
1449
|
-
})
|
|
1450
|
-
.then(data => {
|
|
1451
|
-
loadingEl.innerHTML = '<label style="color: #238636;">✓ Graph loaded successfully</label>';
|
|
1452
|
-
setTimeout(() => loadingEl.style.display = 'none', 2000);
|
|
1453
|
-
visualizeGraph(data);
|
|
1454
|
-
})
|
|
1455
|
-
.catch(err => {
|
|
1456
|
-
loadingEl.innerHTML = `<label style="color: #f85149;">✗ Failed to load graph data</label><br>` +
|
|
1457
|
-
`<small style="color: #8b949e;">${err.message}</small><br>` +
|
|
1458
|
-
`<small style="color: #8b949e;">Run: mcp-vector-search visualize export</small>`;
|
|
1459
|
-
console.error("Failed to load graph:", err);
|
|
1460
|
-
});
|
|
1461
|
-
});
|
|
1462
|
-
</script>
|
|
1463
|
-
</body>
|
|
1464
|
-
</html>"""
|
|
1465
|
-
|
|
1466
|
-
with open(html_file, "w") as f:
|
|
1467
|
-
f.write(html_content)
|