mcp-vector-search 1.0.3__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 +48 -1
- 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 +35 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -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 +74 -1
- mcp_vector_search/analysis/reporters/__init__.py +3 -1
- mcp_vector_search/analysis/reporters/console.py +424 -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 +665 -11
- mcp_vector_search/cli/commands/chat.py +193 -0
- mcp_vector_search/cli/commands/index.py +600 -2
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/search.py +194 -1
- mcp_vector_search/cli/commands/setup.py +64 -13
- mcp_vector_search/cli/commands/status.py +302 -3
- mcp_vector_search/cli/commands/visualize/cli.py +26 -10
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
- mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
- mcp_vector_search/cli/commands/visualize/server.py +304 -15
- mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
- mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
- mcp_vector_search/cli/didyoumean.py +5 -0
- mcp_vector_search/cli/main.py +16 -5
- mcp_vector_search/cli/output.py +134 -5
- mcp_vector_search/config/thresholds.py +89 -1
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/database.py +39 -2
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/indexer.py +445 -84
- mcp_vector_search/core/llm_client.py +9 -4
- mcp_vector_search/core/models.py +88 -1
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/search.py +1 -1
- mcp_vector_search/mcp/server.py +795 -4
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/gitignore.py +0 -3
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
- mcp_vector_search/cli/commands/visualize.py.original +0 -2536
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This module handles running the local HTTP server to serve the
|
|
4
4
|
D3.js visualization interface with chunked transfer for large JSON files.
|
|
5
|
+
Uses orjson for 5-10x faster JSON serialization.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
import asyncio
|
|
@@ -10,6 +11,7 @@ import webbrowser
|
|
|
10
11
|
from collections.abc import AsyncGenerator
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
14
|
+
import orjson
|
|
13
15
|
import uvicorn
|
|
14
16
|
from fastapi import FastAPI, Response
|
|
15
17
|
from fastapi.responses import FileResponse, StreamingResponse
|
|
@@ -17,15 +19,17 @@ from fastapi.staticfiles import StaticFiles
|
|
|
17
19
|
from rich.console import Console
|
|
18
20
|
from rich.panel import Panel
|
|
19
21
|
|
|
22
|
+
from mcp_vector_search import __version__
|
|
23
|
+
|
|
20
24
|
console = Console()
|
|
21
25
|
|
|
22
26
|
|
|
23
|
-
def find_free_port(start_port: int =
|
|
27
|
+
def find_free_port(start_port: int = 8501, end_port: int = 8599) -> int:
|
|
24
28
|
"""Find a free port in the given range.
|
|
25
29
|
|
|
26
30
|
Args:
|
|
27
|
-
start_port: Starting port number to check
|
|
28
|
-
end_port: Ending port number to check
|
|
31
|
+
start_port: Starting port number to check (default: 8501)
|
|
32
|
+
end_port: Ending port number to check (default: 8599)
|
|
29
33
|
|
|
30
34
|
Returns:
|
|
31
35
|
First available port in the range
|
|
@@ -80,6 +84,39 @@ def create_app(viz_dir: Path) -> FastAPI:
|
|
|
80
84
|
"""
|
|
81
85
|
app = FastAPI(title="MCP Vector Search Visualization")
|
|
82
86
|
|
|
87
|
+
@app.get("/api/graph-status")
|
|
88
|
+
async def graph_status() -> Response:
|
|
89
|
+
"""Get graph data generation status.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
JSON response with ready flag and file size
|
|
93
|
+
"""
|
|
94
|
+
graph_file = viz_dir / "chunk-graph.json"
|
|
95
|
+
|
|
96
|
+
if not graph_file.exists():
|
|
97
|
+
return Response(
|
|
98
|
+
content='{"ready": false, "size": 0}',
|
|
99
|
+
media_type="application/json",
|
|
100
|
+
headers={"Cache-Control": "no-cache"},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
size = graph_file.stat().st_size
|
|
105
|
+
# Consider graph ready if file exists and has content (>100 bytes)
|
|
106
|
+
is_ready = size > 100
|
|
107
|
+
return Response(
|
|
108
|
+
content=f'{{"ready": {str(is_ready).lower()}, "size": {size}}}',
|
|
109
|
+
media_type="application/json",
|
|
110
|
+
headers={"Cache-Control": "no-cache"},
|
|
111
|
+
)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
console.print(f"[red]Error checking graph status: {e}[/red]")
|
|
114
|
+
return Response(
|
|
115
|
+
content='{"ready": false, "size": 0}',
|
|
116
|
+
media_type="application/json",
|
|
117
|
+
headers={"Cache-Control": "no-cache"},
|
|
118
|
+
)
|
|
119
|
+
|
|
83
120
|
@app.get("/api/graph")
|
|
84
121
|
async def get_graph_data() -> Response:
|
|
85
122
|
"""Get graph data for D3 tree visualization.
|
|
@@ -97,14 +134,12 @@ def create_app(viz_dir: Path) -> FastAPI:
|
|
|
97
134
|
)
|
|
98
135
|
|
|
99
136
|
try:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
with open(graph_file) as f:
|
|
103
|
-
data = json.load(f)
|
|
137
|
+
with open(graph_file, "rb") as f:
|
|
138
|
+
data = orjson.loads(f.read())
|
|
104
139
|
|
|
105
|
-
# Return nodes and links
|
|
140
|
+
# Return nodes and links using orjson for fast serialization
|
|
106
141
|
return Response(
|
|
107
|
-
content=
|
|
142
|
+
content=orjson.dumps(
|
|
108
143
|
{"nodes": data.get("nodes", []), "links": data.get("links", [])}
|
|
109
144
|
),
|
|
110
145
|
media_type="application/json",
|
|
@@ -118,6 +153,262 @@ def create_app(viz_dir: Path) -> FastAPI:
|
|
|
118
153
|
media_type="application/json",
|
|
119
154
|
)
|
|
120
155
|
|
|
156
|
+
@app.get("/api/relationships/{chunk_id}")
|
|
157
|
+
async def get_chunk_relationships(chunk_id: str) -> Response:
|
|
158
|
+
"""Get all relationships for a chunk (semantic + callers) on-demand.
|
|
159
|
+
|
|
160
|
+
Lazy loads relationships when user expands a node, avoiding expensive
|
|
161
|
+
upfront computation. Results are cached in-memory for the session.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
chunk_id: The chunk ID to find relationships for
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
JSON response with semantic neighbors and callers
|
|
168
|
+
"""
|
|
169
|
+
graph_file = viz_dir / "chunk-graph.json"
|
|
170
|
+
|
|
171
|
+
if not graph_file.exists():
|
|
172
|
+
return Response(
|
|
173
|
+
content='{"error": "Graph data not found"}',
|
|
174
|
+
status_code=404,
|
|
175
|
+
media_type="application/json",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
import ast
|
|
180
|
+
|
|
181
|
+
with open(graph_file, "rb") as f:
|
|
182
|
+
data = orjson.loads(f.read())
|
|
183
|
+
|
|
184
|
+
# Find the target chunk
|
|
185
|
+
target_node = None
|
|
186
|
+
for node in data.get("nodes", []):
|
|
187
|
+
if node.get("id") == chunk_id:
|
|
188
|
+
target_node = node
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if not target_node:
|
|
192
|
+
return Response(
|
|
193
|
+
content='{"error": "Chunk not found"}',
|
|
194
|
+
status_code=404,
|
|
195
|
+
media_type="application/json",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
function_name = target_node.get("function_name") or target_node.get(
|
|
199
|
+
"class_name"
|
|
200
|
+
)
|
|
201
|
+
target_file = target_node.get("file_path", "")
|
|
202
|
+
target_content = target_node.get("content", "")
|
|
203
|
+
|
|
204
|
+
# Compute callers (who calls this function)
|
|
205
|
+
callers = []
|
|
206
|
+
|
|
207
|
+
def extract_calls(code: str) -> set[str]:
|
|
208
|
+
calls = set()
|
|
209
|
+
try:
|
|
210
|
+
tree = ast.parse(code)
|
|
211
|
+
for node in ast.walk(tree):
|
|
212
|
+
if isinstance(node, ast.Call):
|
|
213
|
+
if isinstance(node.func, ast.Name):
|
|
214
|
+
calls.add(node.func.id)
|
|
215
|
+
elif isinstance(node.func, ast.Attribute):
|
|
216
|
+
calls.add(node.func.attr)
|
|
217
|
+
except SyntaxError:
|
|
218
|
+
pass
|
|
219
|
+
return calls
|
|
220
|
+
|
|
221
|
+
if function_name:
|
|
222
|
+
for node in data.get("nodes", []):
|
|
223
|
+
if node.get("type") != "chunk":
|
|
224
|
+
continue
|
|
225
|
+
node_file = node.get("file_path", "")
|
|
226
|
+
if node_file == target_file:
|
|
227
|
+
continue
|
|
228
|
+
content = node.get("content", "")
|
|
229
|
+
if function_name in extract_calls(content):
|
|
230
|
+
caller_name = node.get("function_name") or node.get(
|
|
231
|
+
"class_name"
|
|
232
|
+
)
|
|
233
|
+
if caller_name == "__init__":
|
|
234
|
+
continue
|
|
235
|
+
callers.append(
|
|
236
|
+
{
|
|
237
|
+
"id": node.get("id"),
|
|
238
|
+
"name": caller_name
|
|
239
|
+
or f"chunk_{node.get('start_line', 0)}",
|
|
240
|
+
"file": node_file,
|
|
241
|
+
"type": node.get("chunk_type", "code"),
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Compute semantic neighbors (similar code)
|
|
246
|
+
# Simple approach: find chunks with similar function names or content overlap
|
|
247
|
+
semantic = []
|
|
248
|
+
target_words = set(target_content.lower().split())
|
|
249
|
+
|
|
250
|
+
for node in data.get("nodes", []):
|
|
251
|
+
if node.get("type") != "chunk" or node.get("id") == chunk_id:
|
|
252
|
+
continue
|
|
253
|
+
content = node.get("content", "")
|
|
254
|
+
node_words = set(content.lower().split())
|
|
255
|
+
# Jaccard similarity
|
|
256
|
+
if target_words and node_words:
|
|
257
|
+
intersection = len(target_words & node_words)
|
|
258
|
+
union = len(target_words | node_words)
|
|
259
|
+
similarity = intersection / union if union > 0 else 0
|
|
260
|
+
if similarity > 0.3: # 30% threshold
|
|
261
|
+
semantic.append(
|
|
262
|
+
{
|
|
263
|
+
"id": node.get("id"),
|
|
264
|
+
"name": node.get("function_name")
|
|
265
|
+
or node.get("class_name")
|
|
266
|
+
or "chunk",
|
|
267
|
+
"file": node.get("file_path", ""),
|
|
268
|
+
"similarity": round(similarity, 2),
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Sort by similarity and limit
|
|
273
|
+
semantic.sort(key=lambda x: x["similarity"], reverse=True)
|
|
274
|
+
semantic = semantic[:10]
|
|
275
|
+
|
|
276
|
+
return Response(
|
|
277
|
+
content=orjson.dumps(
|
|
278
|
+
{
|
|
279
|
+
"chunk_id": chunk_id,
|
|
280
|
+
"callers": callers,
|
|
281
|
+
"caller_count": len(callers),
|
|
282
|
+
"semantic": semantic,
|
|
283
|
+
"semantic_count": len(semantic),
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
media_type="application/json",
|
|
287
|
+
headers={"Cache-Control": "max-age=300"},
|
|
288
|
+
)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
console.print(f"[red]Error computing relationships: {e}[/red]")
|
|
291
|
+
return Response(
|
|
292
|
+
content='{"error": "Failed to compute relationships"}',
|
|
293
|
+
status_code=500,
|
|
294
|
+
media_type="application/json",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
@app.get("/api/callers/{chunk_id}")
|
|
298
|
+
async def get_chunk_callers(chunk_id: str) -> Response:
|
|
299
|
+
"""Get callers for a specific code chunk (lazy loaded on-demand).
|
|
300
|
+
|
|
301
|
+
This computes callers for a single chunk instantly instead of
|
|
302
|
+
pre-computing all relationships (which takes 20+ minutes).
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
chunk_id: The chunk ID to find callers for
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
JSON response with callers array
|
|
309
|
+
"""
|
|
310
|
+
graph_file = viz_dir / "chunk-graph.json"
|
|
311
|
+
|
|
312
|
+
if not graph_file.exists():
|
|
313
|
+
return Response(
|
|
314
|
+
content='{"error": "Graph data not found", "callers": []}',
|
|
315
|
+
status_code=404,
|
|
316
|
+
media_type="application/json",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
import ast
|
|
321
|
+
|
|
322
|
+
with open(graph_file, "rb") as f:
|
|
323
|
+
data = orjson.loads(f.read())
|
|
324
|
+
|
|
325
|
+
# Find the target chunk
|
|
326
|
+
target_node = None
|
|
327
|
+
for node in data.get("nodes", []):
|
|
328
|
+
if node.get("id") == chunk_id:
|
|
329
|
+
target_node = node
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
if not target_node:
|
|
333
|
+
return Response(
|
|
334
|
+
content='{"error": "Chunk not found", "callers": []}',
|
|
335
|
+
status_code=404,
|
|
336
|
+
media_type="application/json",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Get the function/class name from the target
|
|
340
|
+
function_name = target_node.get("function_name") or target_node.get(
|
|
341
|
+
"class_name"
|
|
342
|
+
)
|
|
343
|
+
if not function_name:
|
|
344
|
+
return Response(
|
|
345
|
+
content=orjson.dumps({"callers": [], "function_name": None}),
|
|
346
|
+
media_type="application/json",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
target_file = target_node.get("file_path", "")
|
|
350
|
+
|
|
351
|
+
# Find callers by scanning other chunks
|
|
352
|
+
callers = []
|
|
353
|
+
|
|
354
|
+
def extract_calls(code: str) -> set[str]:
|
|
355
|
+
"""Extract function calls from code using AST."""
|
|
356
|
+
calls = set()
|
|
357
|
+
try:
|
|
358
|
+
tree = ast.parse(code)
|
|
359
|
+
for node in ast.walk(tree):
|
|
360
|
+
if isinstance(node, ast.Call):
|
|
361
|
+
if isinstance(node.func, ast.Name):
|
|
362
|
+
calls.add(node.func.id)
|
|
363
|
+
elif isinstance(node.func, ast.Attribute):
|
|
364
|
+
calls.add(node.func.attr)
|
|
365
|
+
except SyntaxError:
|
|
366
|
+
pass
|
|
367
|
+
return calls
|
|
368
|
+
|
|
369
|
+
for node in data.get("nodes", []):
|
|
370
|
+
# Skip non-code chunks and same-file chunks
|
|
371
|
+
if node.get("type") != "chunk":
|
|
372
|
+
continue
|
|
373
|
+
node_file = node.get("file_path", "")
|
|
374
|
+
if node_file == target_file:
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
# Check if this chunk calls our target function
|
|
378
|
+
content = node.get("content", "")
|
|
379
|
+
if function_name in extract_calls(content):
|
|
380
|
+
caller_name = node.get("function_name") or node.get("class_name")
|
|
381
|
+
if caller_name == "__init__":
|
|
382
|
+
continue # Skip noise
|
|
383
|
+
|
|
384
|
+
callers.append(
|
|
385
|
+
{
|
|
386
|
+
"id": node.get("id"),
|
|
387
|
+
"name": caller_name or f"chunk_{node.get('start_line', 0)}",
|
|
388
|
+
"file": node_file,
|
|
389
|
+
"type": node.get("chunk_type", "code"),
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return Response(
|
|
394
|
+
content=orjson.dumps(
|
|
395
|
+
{
|
|
396
|
+
"callers": callers,
|
|
397
|
+
"function_name": function_name,
|
|
398
|
+
"count": len(callers),
|
|
399
|
+
}
|
|
400
|
+
),
|
|
401
|
+
media_type="application/json",
|
|
402
|
+
headers={"Cache-Control": "max-age=300"}, # Cache for 5 minutes
|
|
403
|
+
)
|
|
404
|
+
except Exception as e:
|
|
405
|
+
console.print(f"[red]Error computing callers: {e}[/red]")
|
|
406
|
+
return Response(
|
|
407
|
+
content='{"error": "Failed to compute callers", "callers": []}',
|
|
408
|
+
status_code=500,
|
|
409
|
+
media_type="application/json",
|
|
410
|
+
)
|
|
411
|
+
|
|
121
412
|
@app.get("/api/chunks")
|
|
122
413
|
async def get_file_chunks(file_id: str) -> Response:
|
|
123
414
|
"""Get code chunks for a specific file.
|
|
@@ -138,10 +429,8 @@ def create_app(viz_dir: Path) -> FastAPI:
|
|
|
138
429
|
)
|
|
139
430
|
|
|
140
431
|
try:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
with open(graph_file) as f:
|
|
144
|
-
data = json.load(f)
|
|
432
|
+
with open(graph_file, "rb") as f:
|
|
433
|
+
data = orjson.loads(f.read())
|
|
145
434
|
|
|
146
435
|
# Find chunks associated with this file
|
|
147
436
|
# Look for nodes that have this file as parent via containment links
|
|
@@ -159,7 +448,7 @@ def create_app(viz_dir: Path) -> FastAPI:
|
|
|
159
448
|
)
|
|
160
449
|
|
|
161
450
|
return Response(
|
|
162
|
-
content=
|
|
451
|
+
content=orjson.dumps({"chunks": chunks}),
|
|
163
452
|
media_type="application/json",
|
|
164
453
|
headers={"Cache-Control": "no-cache"},
|
|
165
454
|
)
|
|
@@ -277,7 +566,7 @@ def start_visualization_server(
|
|
|
277
566
|
f"URL: [cyan]{url}[/cyan]\n"
|
|
278
567
|
f"Directory: [dim]{viz_dir}[/dim]\n\n"
|
|
279
568
|
f"[dim]Press Ctrl+C to stop[/dim]",
|
|
280
|
-
title="Server Started",
|
|
569
|
+
title=f"Server Started v{__version__}",
|
|
281
570
|
border_style="green",
|
|
282
571
|
)
|
|
283
572
|
)
|
|
@@ -6,6 +6,8 @@ to generate the complete HTML page for the D3.js visualization.
|
|
|
6
6
|
|
|
7
7
|
import time
|
|
8
8
|
|
|
9
|
+
from mcp_vector_search import __build__, __version__
|
|
10
|
+
|
|
9
11
|
from .scripts import get_all_scripts
|
|
10
12
|
from .styles import get_all_styles
|
|
11
13
|
|
|
@@ -43,14 +45,10 @@ def generate_html_template() -> str:
|
|
|
43
45
|
<body>
|
|
44
46
|
<div id="controls">
|
|
45
47
|
<h1>🔍 Code Tree</h1>
|
|
46
|
-
|
|
47
|
-
<div class="search-container">
|
|
48
|
-
<input type="text" id="search-input" placeholder="Search nodes..." oninput="handleSearchInput(event)" onkeydown="handleSearchKeydown(event)">
|
|
49
|
-
<div id="search-results" class="search-results"></div>
|
|
50
|
-
</div>
|
|
48
|
+
<div class="version-badge">v{__version__} (build {__build__})</div>
|
|
51
49
|
|
|
52
50
|
<div class="control-group">
|
|
53
|
-
<label style="color:
|
|
51
|
+
<label style="color: var(--text-primary); margin-bottom: 8px;">Layout Mode</label>
|
|
54
52
|
<div class="toggle-switch-container">
|
|
55
53
|
<span class="toggle-label">Linear</span>
|
|
56
54
|
<label class="toggle-switch">
|
|
@@ -61,6 +59,15 @@ def generate_html_template() -> str:
|
|
|
61
59
|
</div>
|
|
62
60
|
</div>
|
|
63
61
|
|
|
62
|
+
<div class="control-group">
|
|
63
|
+
<label style="color: var(--text-primary); margin-bottom: 8px;">Show Files</label>
|
|
64
|
+
<div class="filter-buttons">
|
|
65
|
+
<button class="filter-btn active" data-filter="all" onclick="setFileFilter('all')">All</button>
|
|
66
|
+
<button class="filter-btn" data-filter="code" onclick="setFileFilter('code')">Code</button>
|
|
67
|
+
<button class="filter-btn" data-filter="docs" onclick="setFileFilter('docs')">Docs</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
64
71
|
<h3>Legend</h3>
|
|
65
72
|
<div class="legend">
|
|
66
73
|
<div class="legend-category">
|
|
@@ -127,6 +134,53 @@ def generate_html_template() -> str:
|
|
|
127
134
|
</div>
|
|
128
135
|
</div>
|
|
129
136
|
|
|
137
|
+
<!-- Search Section -->
|
|
138
|
+
<h3>🔎 Search</h3>
|
|
139
|
+
<div class="search-container">
|
|
140
|
+
<input type="text" id="search-input" placeholder="Search nodes..." oninput="handleSearchInput(event)" onkeydown="handleSearchKeydown(event)">
|
|
141
|
+
<div id="search-results" class="search-results"></div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Options Section -->
|
|
145
|
+
<h3>📋 Reports</h3>
|
|
146
|
+
<div class="legend" style="margin-top: 8px;">
|
|
147
|
+
<div class="legend-category" style="border-bottom: none;">
|
|
148
|
+
<div class="legend-item report-btn" onclick="showComplexityReport()">
|
|
149
|
+
<span class="report-icon">📊</span>
|
|
150
|
+
<span>Complexity</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="legend-item report-btn" onclick="showCodeSmells()">
|
|
153
|
+
<span class="report-icon">🔍</span>
|
|
154
|
+
<span>Code Smells</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="legend-item report-btn" onclick="showDependencies()">
|
|
157
|
+
<span class="report-icon">🔗</span>
|
|
158
|
+
<span>Dependencies</span>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="legend-item report-btn" onclick="showTrends()">
|
|
161
|
+
<span class="report-icon">📈</span>
|
|
162
|
+
<span>Trends</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="legend-item report-btn" onclick="generateRemediationReport()">
|
|
165
|
+
<span class="report-icon">📋</span>
|
|
166
|
+
<span>Remediation</span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<h3 style="margin-top: 16px;">Options</h3>
|
|
172
|
+
<div class="legend" style="margin-top: 8px;">
|
|
173
|
+
<div class="legend-category" style="border-bottom: none;">
|
|
174
|
+
<!-- Theme Toggle -->
|
|
175
|
+
<div class="legend-item" style="margin-bottom: 12px; padding: 0;">
|
|
176
|
+
<button class="theme-toggle-icon-btn" onclick="toggleTheme()" title="Toggle dark/light theme">
|
|
177
|
+
<span class="theme-icon" id="theme-icon">🌙</span>
|
|
178
|
+
</button>
|
|
179
|
+
<span style="margin-left: 8px; color: var(--text-secondary); font-size: 12px;">Theme</span>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
130
184
|
<div class="stats" id="stats"></div>
|
|
131
185
|
</div>
|
|
132
186
|
|