cortexcode 0.1.0__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.
- cortexcode/__init__.py +3 -0
- cortexcode/analysis.py +331 -0
- cortexcode/cli.py +845 -0
- cortexcode/context.py +298 -0
- cortexcode/dashboard.py +152 -0
- cortexcode/docs.py +1266 -0
- cortexcode/git_diff.py +157 -0
- cortexcode/indexer.py +1860 -0
- cortexcode/lsp_server.py +315 -0
- cortexcode/mcp_server.py +455 -0
- cortexcode/plugins.py +188 -0
- cortexcode/semantic_search.py +237 -0
- cortexcode/vuln_scan.py +241 -0
- cortexcode/watcher.py +122 -0
- cortexcode/workspace.py +180 -0
- cortexcode-0.1.0.dist-info/METADATA +448 -0
- cortexcode-0.1.0.dist-info/RECORD +21 -0
- cortexcode-0.1.0.dist-info/WHEEL +5 -0
- cortexcode-0.1.0.dist-info/entry_points.txt +2 -0
- cortexcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- cortexcode-0.1.0.dist-info/top_level.txt +1 -0
cortexcode/docs.py
ADDED
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
"""Docs Generator - Generate project documentation from index."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
D3_CDN_URL = "https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"
|
|
9
|
+
D3_LOCAL_FILE = "d3.min.js"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ensure_d3_local(output_dir: Path) -> str:
|
|
13
|
+
"""Ensure D3.js is available locally. Returns the script src to use."""
|
|
14
|
+
local_path = output_dir / D3_LOCAL_FILE
|
|
15
|
+
if local_path.exists() and local_path.stat().st_size > 100_000:
|
|
16
|
+
return D3_LOCAL_FILE
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import urllib.request
|
|
20
|
+
urllib.request.urlretrieve(D3_CDN_URL, str(local_path))
|
|
21
|
+
if local_path.exists() and local_path.stat().st_size > 100_000:
|
|
22
|
+
return D3_LOCAL_FILE
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
# Fallback to CDN if download fails
|
|
27
|
+
return D3_CDN_URL
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate_all_docs(index_path: Path, output_dir: Path) -> None:
|
|
31
|
+
"""Generate all documentation files from the index."""
|
|
32
|
+
output_dir = Path(output_dir)
|
|
33
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
index = json.loads(index_path.read_text(encoding="utf-8"))
|
|
36
|
+
|
|
37
|
+
# Bundle D3.js locally for offline support
|
|
38
|
+
d3_src = _ensure_d3_local(output_dir)
|
|
39
|
+
|
|
40
|
+
generate_readme(index, output_dir / "README.md")
|
|
41
|
+
generate_api_docs(index, output_dir / "API.md")
|
|
42
|
+
generate_structure_docs(index, output_dir / "STRUCTURE.md")
|
|
43
|
+
generate_flow_docs(index, output_dir / "FLOWS.md")
|
|
44
|
+
generate_html_docs(index, output_dir / "index.html", d3_src=d3_src)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def generate_readme(index: dict, output_path: Path) -> None:
|
|
48
|
+
"""Generate project README."""
|
|
49
|
+
files = index.get("files", {})
|
|
50
|
+
call_graph = index.get("call_graph", {})
|
|
51
|
+
|
|
52
|
+
directories = defaultdict(list)
|
|
53
|
+
for rel_path in files.keys():
|
|
54
|
+
parts = Path(rel_path).parts
|
|
55
|
+
if len(parts) > 1:
|
|
56
|
+
directories[parts[0]].append(rel_path)
|
|
57
|
+
|
|
58
|
+
lines = [
|
|
59
|
+
"# Project Documentation",
|
|
60
|
+
"",
|
|
61
|
+
"## Overview",
|
|
62
|
+
"",
|
|
63
|
+
f"**Project Root:** `{index.get('project_root', 'N/A')}`",
|
|
64
|
+
f"**Last Indexed:** {index.get('last_indexed', 'N/A')}",
|
|
65
|
+
"",
|
|
66
|
+
"## Key Modules",
|
|
67
|
+
"",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
for dir_name, dir_files in sorted(directories.items()):
|
|
71
|
+
lines.append(f"- `{dir_name}/` — {len(dir_files)} files")
|
|
72
|
+
|
|
73
|
+
lines.extend([
|
|
74
|
+
"",
|
|
75
|
+
"## Entry Points",
|
|
76
|
+
"",
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
for rel_path in files.keys():
|
|
80
|
+
if Path(rel_path).name in ("main.py", "app.py", "server.py", "cli.py", "__main__.py"):
|
|
81
|
+
lines.append(f"- `{rel_path}`")
|
|
82
|
+
|
|
83
|
+
if not any(Path(p).name in ("main.py", "app.py", "server.py", "cli.py", "__main__.py") for p in files.keys()):
|
|
84
|
+
lines.append(" (No obvious entry points found)")
|
|
85
|
+
|
|
86
|
+
lines.extend([
|
|
87
|
+
"",
|
|
88
|
+
"## Symbol Count",
|
|
89
|
+
"",
|
|
90
|
+
f"- **Files:** {len(files)}",
|
|
91
|
+
f"- **Symbols:** {len(call_graph)}",
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
output_path.write_text("\n".join(lines), encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def generate_api_docs(index: dict, output_path: Path) -> None:
|
|
98
|
+
"""Generate API documentation."""
|
|
99
|
+
files = index.get("files", {})
|
|
100
|
+
call_graph = index.get("call_graph", {})
|
|
101
|
+
|
|
102
|
+
lines = [
|
|
103
|
+
"# API Documentation",
|
|
104
|
+
"",
|
|
105
|
+
"## Symbols",
|
|
106
|
+
"",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
all_symbols = []
|
|
110
|
+
|
|
111
|
+
for rel_path, file_data in files.items():
|
|
112
|
+
symbols = file_data.get("symbols", []) if isinstance(file_data, dict) else file_data
|
|
113
|
+
|
|
114
|
+
for sym in symbols:
|
|
115
|
+
all_symbols.append(sym)
|
|
116
|
+
|
|
117
|
+
for sym in symbols:
|
|
118
|
+
name = sym.get("name", "unknown")
|
|
119
|
+
sym_type = sym.get("type", "unknown")
|
|
120
|
+
|
|
121
|
+
lines.append(f"## {name} (`{rel_path}`)")
|
|
122
|
+
lines.append("")
|
|
123
|
+
lines.append(f"**Type:** {sym_type}")
|
|
124
|
+
|
|
125
|
+
if sym_type == "function":
|
|
126
|
+
params = sym.get("params", [])
|
|
127
|
+
if params:
|
|
128
|
+
lines.append(f"**Parameters:** {', '.join(params)}")
|
|
129
|
+
|
|
130
|
+
if sym_type == "class":
|
|
131
|
+
methods = sym.get("methods", [])
|
|
132
|
+
if methods:
|
|
133
|
+
lines.append("")
|
|
134
|
+
lines.append("### Methods")
|
|
135
|
+
for method in methods:
|
|
136
|
+
m_params = method.get("params", [])
|
|
137
|
+
params_str = f"({', '.join(m_params)})" if m_params else "()"
|
|
138
|
+
lines.append(f"- `{method['name']}{params_str}`")
|
|
139
|
+
|
|
140
|
+
calls = sym.get("calls", [])
|
|
141
|
+
if calls:
|
|
142
|
+
lines.append("")
|
|
143
|
+
lines.append("### Calls")
|
|
144
|
+
for call in calls:
|
|
145
|
+
lines.append(f"- `{call}`")
|
|
146
|
+
|
|
147
|
+
callers = [s for s, c in call_graph.items() if name in c]
|
|
148
|
+
if callers:
|
|
149
|
+
lines.append("")
|
|
150
|
+
lines.append("### Called By")
|
|
151
|
+
for caller in callers:
|
|
152
|
+
lines.append(f"- `{caller}`")
|
|
153
|
+
|
|
154
|
+
lines.append("")
|
|
155
|
+
|
|
156
|
+
output_path.write_text("\n".join(lines), encoding="utf-8")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def generate_structure_docs(index: dict, output_path: Path) -> None:
|
|
160
|
+
"""Generate directory structure documentation."""
|
|
161
|
+
files = index.get("files", {})
|
|
162
|
+
|
|
163
|
+
tree = defaultdict(lambda: defaultdict(list))
|
|
164
|
+
|
|
165
|
+
for rel_path, file_data in files.items():
|
|
166
|
+
symbols = file_data.get("symbols", []) if isinstance(file_data, dict) else file_data
|
|
167
|
+
parts = Path(rel_path).parts
|
|
168
|
+
if len(parts) == 1:
|
|
169
|
+
tree["."][rel_path] = symbols
|
|
170
|
+
else:
|
|
171
|
+
tree[parts[0]]["/".join(parts[1:])] = symbols
|
|
172
|
+
|
|
173
|
+
lines = [
|
|
174
|
+
"# Directory Structure",
|
|
175
|
+
"",
|
|
176
|
+
"```",
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
all_paths = sorted(files.keys())
|
|
180
|
+
for path in all_paths:
|
|
181
|
+
parts = path.split("/")
|
|
182
|
+
indent = " " * (len(parts) - 1)
|
|
183
|
+
file_name = parts[-1]
|
|
184
|
+
sym_count = len(files[path])
|
|
185
|
+
lines.append(f"{indent}{file_name} ({sym_count} symbols)")
|
|
186
|
+
|
|
187
|
+
lines.extend([
|
|
188
|
+
"```",
|
|
189
|
+
"",
|
|
190
|
+
f"**Total files:** {len(files)}",
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
output_path.write_text("\n".join(lines), encoding="utf-8")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def generate_flow_docs(index: dict, output_path: Path) -> None:
|
|
197
|
+
"""Generate call flow documentation."""
|
|
198
|
+
call_graph = index.get("call_graph", {})
|
|
199
|
+
|
|
200
|
+
lines = [
|
|
201
|
+
"# Call Flows",
|
|
202
|
+
"",
|
|
203
|
+
"## Call Graph",
|
|
204
|
+
"",
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
for symbol, calls in sorted(call_graph.items()):
|
|
208
|
+
if calls:
|
|
209
|
+
lines.append(f"### {symbol}")
|
|
210
|
+
lines.append("")
|
|
211
|
+
for call in calls:
|
|
212
|
+
lines.append(f" → {call}")
|
|
213
|
+
lines.append("")
|
|
214
|
+
|
|
215
|
+
if not any(call_graph.values()):
|
|
216
|
+
lines.append("*No call relationships found.*")
|
|
217
|
+
|
|
218
|
+
output_path.write_text("\n".join(lines), encoding="utf-8")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def generate_html_docs(index: dict, output_path: Path, d3_src: str = D3_CDN_URL) -> None:
|
|
222
|
+
"""Generate interactive HTML documentation."""
|
|
223
|
+
files = index.get("files", {})
|
|
224
|
+
call_graph = index.get("call_graph", {})
|
|
225
|
+
file_deps = index.get("file_dependencies", {})
|
|
226
|
+
project_root = index.get("project_root", "")
|
|
227
|
+
last_indexed = index.get("last_indexed", "")
|
|
228
|
+
|
|
229
|
+
all_symbols = []
|
|
230
|
+
file_tree = {}
|
|
231
|
+
framework_counts = {
|
|
232
|
+
"react": 0, "react-native": 0, "expo": 0,
|
|
233
|
+
"angular": 0, "nextjs": 0, "nestjs": 0, "express": 0,
|
|
234
|
+
"flutter": 0, "compose": 0, "android": 0,
|
|
235
|
+
"swiftui": 0, "uikit": 0, "ios": 0,
|
|
236
|
+
"spring": 0, "fastapi": 0, "django": 0, "flask": 0, "aspnet": 0,
|
|
237
|
+
}
|
|
238
|
+
language_counts = {}
|
|
239
|
+
type_counts = {}
|
|
240
|
+
all_imports = []
|
|
241
|
+
all_exports = []
|
|
242
|
+
all_api_routes = []
|
|
243
|
+
all_entities = []
|
|
244
|
+
files_with_most_symbols = []
|
|
245
|
+
|
|
246
|
+
for rel_path, file_data in files.items():
|
|
247
|
+
symbols = file_data.get("symbols", []) if isinstance(file_data, dict) else file_data
|
|
248
|
+
imports = file_data.get("imports", []) if isinstance(file_data, dict) else []
|
|
249
|
+
exports = file_data.get("exports", []) if isinstance(file_data, dict) else []
|
|
250
|
+
api_routes = file_data.get("api_routes", []) if isinstance(file_data, dict) else []
|
|
251
|
+
entities = file_data.get("entities", []) if isinstance(file_data, dict) else []
|
|
252
|
+
|
|
253
|
+
ext = Path(rel_path).suffix
|
|
254
|
+
language_counts[ext] = language_counts.get(ext, 0) + 1
|
|
255
|
+
|
|
256
|
+
parts = rel_path.replace("\\", "/").split("/")
|
|
257
|
+
current = file_tree
|
|
258
|
+
for part in parts[:-1]:
|
|
259
|
+
if part not in current:
|
|
260
|
+
current[part] = {}
|
|
261
|
+
current = current[part]
|
|
262
|
+
if parts[-1] not in current:
|
|
263
|
+
current[parts[-1]] = symbols
|
|
264
|
+
|
|
265
|
+
all_imports.extend([{"file": rel_path, **imp} for imp in imports])
|
|
266
|
+
all_exports.extend([{"file": rel_path, **exp} for exp in exports])
|
|
267
|
+
all_api_routes.extend([{"file": rel_path, **route} for route in api_routes])
|
|
268
|
+
all_entities.extend([{"file": rel_path, **ent} for ent in entities])
|
|
269
|
+
|
|
270
|
+
files_with_most_symbols.append({"file": rel_path, "count": len(symbols)})
|
|
271
|
+
|
|
272
|
+
for sym in symbols:
|
|
273
|
+
sym["file"] = rel_path
|
|
274
|
+
all_symbols.append(sym)
|
|
275
|
+
t = sym.get("type", "unknown")
|
|
276
|
+
type_counts[t] = type_counts.get(t, 0) + 1
|
|
277
|
+
|
|
278
|
+
fw = sym.get("framework")
|
|
279
|
+
if fw:
|
|
280
|
+
for key in framework_counts:
|
|
281
|
+
if key in fw:
|
|
282
|
+
framework_counts[key] += 1
|
|
283
|
+
break
|
|
284
|
+
|
|
285
|
+
files_with_most_symbols.sort(key=lambda x: x["count"], reverse=True)
|
|
286
|
+
|
|
287
|
+
# Compute graph stats
|
|
288
|
+
non_empty_calls = sum(1 for v in call_graph.values() if v)
|
|
289
|
+
total_call_edges = sum(len(v) for v in call_graph.values() if v)
|
|
290
|
+
top_callers = sorted(
|
|
291
|
+
[(k, len(v)) for k, v in call_graph.items() if v],
|
|
292
|
+
key=lambda x: x[1], reverse=True
|
|
293
|
+
)[:10]
|
|
294
|
+
|
|
295
|
+
# Type colors for charts
|
|
296
|
+
type_colors = {
|
|
297
|
+
"function": "#38bdf8",
|
|
298
|
+
"class": "#a78bfa",
|
|
299
|
+
"method": "#34d399",
|
|
300
|
+
"interface": "#fbbf24",
|
|
301
|
+
"type": "#f472b6",
|
|
302
|
+
"enum": "#fb923c",
|
|
303
|
+
}
|
|
304
|
+
lang_colors = {
|
|
305
|
+
".ts": "#3178c6", ".tsx": "#3178c6",
|
|
306
|
+
".js": "#f7df1e", ".jsx": "#f7df1e",
|
|
307
|
+
".py": "#3572A5",
|
|
308
|
+
".go": "#00ADD8",
|
|
309
|
+
".rs": "#dea584",
|
|
310
|
+
".java": "#b07219",
|
|
311
|
+
".cs": "#178600",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
project_name = Path(project_root).name
|
|
315
|
+
|
|
316
|
+
# Pre-build HTML fragments that have complex quoting
|
|
317
|
+
fw_cards_html = ""
|
|
318
|
+
if any(framework_counts.values()):
|
|
319
|
+
fw_items = "".join(
|
|
320
|
+
f'<div style="background:var(--bg);padding:12px 20px;border-radius:var(--radius-sm);text-align:center;">'
|
|
321
|
+
f'<div style="font-size:24px;font-weight:700;color:var(--accent);">{c}</div>'
|
|
322
|
+
f'<div style="font-size:12px;color:var(--text3);margin-top:2px;">{fw}</div></div>'
|
|
323
|
+
for fw, c in framework_counts.items() if c > 0
|
|
324
|
+
)
|
|
325
|
+
fw_cards_html = (
|
|
326
|
+
'<div class="card"><div class="card-title">Detected Frameworks</div>'
|
|
327
|
+
'<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px;">'
|
|
328
|
+
f'{fw_items}</div></div>'
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
filter_tabs_html = "".join(
|
|
332
|
+
f'<div class="filter-tab" onclick="filterByType(\'{t}\', this)">{t.title()} ({c})</div>'
|
|
333
|
+
for t, c in sorted(type_counts.items(), key=lambda x: -x[1])
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
top_files_rows = "".join(
|
|
337
|
+
f'<tr><td style="color:var(--text2);font-size:12px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="{f["file"]}">{f["file"]}</td>'
|
|
338
|
+
f'<td style="font-weight:600;color:var(--accent);">{f["count"]}</td>'
|
|
339
|
+
f'<td><div class="bar" style="width:{min(100, f["count"] * 100 // max(1, files_with_most_symbols[0]["count"] if files_with_most_symbols else 1))}%;"></div></td></tr>'
|
|
340
|
+
for f in files_with_most_symbols[:8]
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
top_callers_rows = "".join(
|
|
344
|
+
f'<tr><td style="color:var(--accent);font-size:13px;cursor:pointer;" onclick="highlightGraphNode(\'{name}\')">{name}</td>'
|
|
345
|
+
f'<td style="font-weight:600;">{count}</td>'
|
|
346
|
+
f'<td><div class="bar" style="width:{min(100, count * 100 // max(1, top_callers[0][1] if top_callers else 1))}%;background:var(--accent2);"></div></td></tr>'
|
|
347
|
+
for name, count in top_callers[:8]
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Pre-build JSON data for JavaScript
|
|
351
|
+
search_data_json = json.dumps([
|
|
352
|
+
{"name": s.get("name",""), "type": s.get("type",""), "file": s.get("file",""), "line": s.get("line",0), "doc": (s.get("doc","") or "")[:60], "params": s.get("params",[])[:3]}
|
|
353
|
+
for s in all_symbols if isinstance(s, dict)
|
|
354
|
+
][:600])
|
|
355
|
+
call_graph_json = json.dumps(call_graph)
|
|
356
|
+
file_deps_json = json.dumps(file_deps)
|
|
357
|
+
type_counts_json = json.dumps(type_counts)
|
|
358
|
+
lang_counts_json = json.dumps(language_counts)
|
|
359
|
+
type_colors_json = json.dumps(type_colors)
|
|
360
|
+
lang_colors_json = json.dumps(lang_colors)
|
|
361
|
+
|
|
362
|
+
html = f"""<!DOCTYPE html>
|
|
363
|
+
<html lang="en">
|
|
364
|
+
<head>
|
|
365
|
+
<meta charset="UTF-8">
|
|
366
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
367
|
+
<title>{project_name} — CortexCode Report</title>
|
|
368
|
+
<script src="{d3_src}"></script>
|
|
369
|
+
<style>
|
|
370
|
+
:root {{
|
|
371
|
+
--bg: #0f172a; --bg2: #1e293b; --bg3: #334155;
|
|
372
|
+
--text: #e2e8f0; --text2: #94a3b8; --text3: #64748b;
|
|
373
|
+
--accent: #38bdf8; --accent2: #818cf8; --green: #34d399;
|
|
374
|
+
--yellow: #fbbf24; --red: #f87171; --pink: #f472b6;
|
|
375
|
+
--radius: 12px; --radius-sm: 8px;
|
|
376
|
+
}}
|
|
377
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
378
|
+
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }}
|
|
379
|
+
|
|
380
|
+
.sidebar {{
|
|
381
|
+
position: fixed; left: 0; top: 0; width: 260px; height: 100vh;
|
|
382
|
+
background: var(--bg2); border-right: 1px solid var(--bg3); overflow-y: auto;
|
|
383
|
+
padding: 20px; z-index: 50;
|
|
384
|
+
}}
|
|
385
|
+
.sidebar::-webkit-scrollbar {{ width: 4px; }}
|
|
386
|
+
.sidebar::-webkit-scrollbar-thumb {{ background: var(--bg3); border-radius: 4px; }}
|
|
387
|
+
|
|
388
|
+
.logo {{ font-size: 20px; font-weight: 700; color: var(--accent); margin-bottom: 24px; display: flex; align-items: center; gap: 10px; }}
|
|
389
|
+
.logo-icon {{ width: 32px; height: 32px; background: linear-gradient(135deg, #38bdf8, #818cf8); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-weight: 900; color: white; font-size: 14px; }}
|
|
390
|
+
|
|
391
|
+
.sidebar-stats {{ display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 20px; }}
|
|
392
|
+
.sidebar-stat {{ background: var(--bg); padding: 12px 10px; border-radius: var(--radius-sm); text-align: center; }}
|
|
393
|
+
.sidebar-stat-val {{ font-size: 22px; font-weight: 700; color: var(--accent); }}
|
|
394
|
+
.sidebar-stat-lbl {{ font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }}
|
|
395
|
+
|
|
396
|
+
.nav-section {{ margin-bottom: 16px; }}
|
|
397
|
+
.nav-title {{ font-size: 10px; text-transform: uppercase; color: var(--text3); letter-spacing: 1px; margin-bottom: 8px; font-weight: 600; }}
|
|
398
|
+
.nav-item {{ display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: var(--radius-sm); cursor: pointer; transition: all 0.15s; color: var(--text2); font-size: 13px; }}
|
|
399
|
+
.nav-item:hover {{ background: var(--bg3); color: var(--text); }}
|
|
400
|
+
.nav-item.active {{ background: var(--accent); color: white; font-weight: 600; }}
|
|
401
|
+
.nav-item .badge {{ margin-left: auto; background: var(--bg); color: var(--text3); padding: 2px 8px; border-radius: 10px; font-size: 11px; }}
|
|
402
|
+
.nav-item.active .badge {{ background: rgba(255,255,255,0.2); color: white; }}
|
|
403
|
+
|
|
404
|
+
.main {{ margin-left: 260px; padding: 24px 30px; min-height: 100vh; }}
|
|
405
|
+
|
|
406
|
+
.header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; gap: 20px; }}
|
|
407
|
+
.project-name {{ font-size: 26px; font-weight: 700; }}
|
|
408
|
+
.last-indexed {{ color: var(--text3); font-size: 13px; margin-top: 4px; }}
|
|
409
|
+
|
|
410
|
+
.search-wrapper {{ position: relative; flex-shrink: 0; }}
|
|
411
|
+
.search-box {{ background: var(--bg2); border: 1px solid var(--bg3); border-radius: var(--radius-sm); padding: 10px 14px 10px 36px; color: white; width: 360px; font-size: 13px; transition: border 0.15s; }}
|
|
412
|
+
.search-box:focus {{ outline: none; border-color: var(--accent); }}
|
|
413
|
+
.search-icon {{ position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text3); font-size: 14px; pointer-events: none; }}
|
|
414
|
+
.search-results {{ display: none; position: absolute; top: 100%; right: 0; width: 460px; max-height: 400px; overflow-y: auto; background: var(--bg2); border: 1px solid var(--bg3); border-radius: var(--radius-sm); z-index: 100; margin-top: 4px; box-shadow: 0 8px 30px rgba(0,0,0,0.4); }}
|
|
415
|
+
.search-result-item {{ display: flex; align-items: center; gap: 10px; padding: 10px 14px; cursor: pointer; border-bottom: 1px solid var(--bg3); font-size: 13px; }}
|
|
416
|
+
.search-result-item:hover {{ background: var(--bg3); }}
|
|
417
|
+
.search-result-item:last-child {{ border-bottom: none; }}
|
|
418
|
+
|
|
419
|
+
.tab-content {{ display: none; }}
|
|
420
|
+
.tab-content.active {{ display: block; }}
|
|
421
|
+
|
|
422
|
+
.card {{ background: var(--bg2); border-radius: var(--radius); padding: 24px; margin-bottom: 16px; border: 1px solid var(--bg3); }}
|
|
423
|
+
.card-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }}
|
|
424
|
+
.card-title {{ font-size: 16px; font-weight: 600; }}
|
|
425
|
+
.card-subtitle {{ font-size: 12px; color: var(--text3); margin-top: 4px; }}
|
|
426
|
+
|
|
427
|
+
/* Dashboard stat cards */
|
|
428
|
+
.dash-stats {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; }}
|
|
429
|
+
.dash-stat {{ background: var(--bg2); border: 1px solid var(--bg3); border-radius: var(--radius); padding: 20px; position: relative; overflow: hidden; }}
|
|
430
|
+
.dash-stat::after {{ content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; }}
|
|
431
|
+
.dash-stat:nth-child(1)::after {{ background: var(--accent); }}
|
|
432
|
+
.dash-stat:nth-child(2)::after {{ background: var(--green); }}
|
|
433
|
+
.dash-stat:nth-child(3)::after {{ background: var(--yellow); }}
|
|
434
|
+
.dash-stat:nth-child(4)::after {{ background: var(--accent2); }}
|
|
435
|
+
.dash-stat-icon {{ font-size: 28px; margin-bottom: 8px; }}
|
|
436
|
+
.dash-stat-val {{ font-size: 32px; font-weight: 700; }}
|
|
437
|
+
.dash-stat:nth-child(1) .dash-stat-val {{ color: var(--accent); }}
|
|
438
|
+
.dash-stat:nth-child(2) .dash-stat-val {{ color: var(--green); }}
|
|
439
|
+
.dash-stat:nth-child(3) .dash-stat-val {{ color: var(--yellow); }}
|
|
440
|
+
.dash-stat:nth-child(4) .dash-stat-val {{ color: var(--accent2); }}
|
|
441
|
+
.dash-stat-lbl {{ font-size: 12px; color: var(--text3); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }}
|
|
442
|
+
|
|
443
|
+
/* Charts row */
|
|
444
|
+
.charts-row {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }}
|
|
445
|
+
.chart-container {{ display: flex; align-items: center; justify-content: center; gap: 24px; padding: 10px 0; }}
|
|
446
|
+
.chart-svg {{ flex-shrink: 0; }}
|
|
447
|
+
.chart-legend {{ display: flex; flex-direction: column; gap: 6px; }}
|
|
448
|
+
.chart-legend-item {{ display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text2); }}
|
|
449
|
+
.chart-legend-dot {{ width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }}
|
|
450
|
+
.chart-legend-val {{ margin-left: auto; font-weight: 600; color: var(--text); min-width: 28px; text-align: right; }}
|
|
451
|
+
|
|
452
|
+
/* Top files / top callers table */
|
|
453
|
+
.mini-table {{ width: 100%; }}
|
|
454
|
+
.mini-table th {{ text-align: left; font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 0; border-bottom: 1px solid var(--bg3); }}
|
|
455
|
+
.mini-table td {{ padding: 8px 0; font-size: 13px; border-bottom: 1px solid rgba(51,65,85,0.5); }}
|
|
456
|
+
.mini-table tr:last-child td {{ border-bottom: none; }}
|
|
457
|
+
.mini-table .bar {{ height: 6px; background: var(--accent); border-radius: 3px; min-width: 4px; }}
|
|
458
|
+
|
|
459
|
+
/* File tree */
|
|
460
|
+
.tree-view {{ font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 13px; }}
|
|
461
|
+
.tree-item {{ padding: 6px 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: background 0.1s; }}
|
|
462
|
+
.tree-item:hover {{ background: var(--bg3); }}
|
|
463
|
+
.tree-item.folder {{ color: var(--yellow); }}
|
|
464
|
+
.tree-item.file {{ color: var(--text2); }}
|
|
465
|
+
.tree-count {{ margin-left: auto; color: var(--text3); font-size: 11px; }}
|
|
466
|
+
|
|
467
|
+
/* Symbols */
|
|
468
|
+
.symbol-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }}
|
|
469
|
+
.symbol-card {{ background: var(--bg); border-radius: var(--radius-sm); padding: 16px; border: 1px solid var(--bg3); transition: all 0.15s; cursor: pointer; }}
|
|
470
|
+
.symbol-card:hover {{ border-color: var(--accent); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(56,189,248,0.1); }}
|
|
471
|
+
.symbol-name {{ font-size: 14px; font-weight: 600; color: var(--accent); margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
472
|
+
.symbol-meta {{ display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }}
|
|
473
|
+
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; }}
|
|
474
|
+
.badge-type {{ background: var(--bg3); color: var(--text2); }}
|
|
475
|
+
.badge-fw {{ background: #7c3aed; color: white; }}
|
|
476
|
+
.badge-doc {{ background: rgba(52,211,153,0.2); color: var(--green); }}
|
|
477
|
+
.symbol-file {{ font-size: 11px; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
478
|
+
.symbol-params {{ font-size: 12px; color: var(--text2); font-family: 'Fira Code', monospace; margin-top: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
479
|
+
|
|
480
|
+
.filter-tabs {{ display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }}
|
|
481
|
+
.filter-tab {{ padding: 6px 14px; background: var(--bg); border: 1px solid var(--bg3); border-radius: 20px; cursor: pointer; color: var(--text2); font-size: 12px; transition: all 0.15s; }}
|
|
482
|
+
.filter-tab:hover {{ border-color: var(--accent); color: var(--text); }}
|
|
483
|
+
.filter-tab.active {{ background: var(--accent); color: white; border-color: var(--accent); }}
|
|
484
|
+
|
|
485
|
+
/* Graph */
|
|
486
|
+
.graph-controls {{ display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }}
|
|
487
|
+
.graph-btn {{ padding: 6px 14px; background: var(--bg); border: 1px solid var(--bg3); border-radius: var(--radius-sm); cursor: pointer; color: var(--text2); font-size: 12px; transition: all 0.15s; }}
|
|
488
|
+
.graph-btn:hover {{ border-color: var(--accent); color: var(--text); }}
|
|
489
|
+
.graph-btn.active {{ background: var(--accent); color: white; border-color: var(--accent); }}
|
|
490
|
+
.graph-search {{ background: var(--bg); border: 1px solid var(--bg3); border-radius: var(--radius-sm); padding: 6px 12px; color: white; font-size: 12px; width: 200px; }}
|
|
491
|
+
.graph-search:focus {{ outline: none; border-color: var(--accent); }}
|
|
492
|
+
.graph-container {{ width: 100%; height: 600px; background: var(--bg); border-radius: var(--radius-sm); position: relative; overflow: hidden; }}
|
|
493
|
+
.graph-container svg {{ width: 100%; height: 100%; }}
|
|
494
|
+
.graph-tooltip {{ position: absolute; background: var(--bg2); border: 1px solid var(--bg3); border-radius: var(--radius-sm); padding: 12px; font-size: 12px; pointer-events: none; z-index: 10; display: none; max-width: 300px; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }}
|
|
495
|
+
.graph-info {{ margin-top: 12px; padding: 16px; background: var(--bg); border-radius: var(--radius-sm); display: none; }}
|
|
496
|
+
.graph-info.active {{ display: block; }}
|
|
497
|
+
|
|
498
|
+
.link {{ stroke-opacity: 0.4; }}
|
|
499
|
+
.link.highlighted {{ stroke-opacity: 1; stroke: var(--accent); stroke-width: 2.5; }}
|
|
500
|
+
.node circle {{ transition: r 0.2s; }}
|
|
501
|
+
.node.dimmed circle {{ opacity: 0.15; }}
|
|
502
|
+
.node.dimmed text {{ opacity: 0.15; }}
|
|
503
|
+
.link.dimmed {{ stroke-opacity: 0.05; }}
|
|
504
|
+
|
|
505
|
+
/* Deps graph */
|
|
506
|
+
.deps-container {{ width: 100%; height: 500px; background: var(--bg); border-radius: var(--radius-sm); position: relative; overflow: hidden; }}
|
|
507
|
+
.deps-container svg {{ width: 100%; height: 100%; }}
|
|
508
|
+
|
|
509
|
+
/* Modal */
|
|
510
|
+
.modal {{ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; backdrop-filter: blur(4px); }}
|
|
511
|
+
.modal.active {{ display: flex; align-items: center; justify-content: center; }}
|
|
512
|
+
.modal-content {{ background: var(--bg2); border-radius: var(--radius); padding: 28px; max-width: 640px; width: 90%; max-height: 80vh; overflow-y: auto; border: 1px solid var(--bg3); box-shadow: 0 20px 60px rgba(0,0,0,0.5); }}
|
|
513
|
+
.modal-header {{ display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }}
|
|
514
|
+
.modal-title {{ font-size: 22px; font-weight: 700; color: var(--accent); }}
|
|
515
|
+
.modal-close {{ background: none; border: none; color: var(--text3); font-size: 22px; cursor: pointer; padding: 4px 8px; border-radius: 6px; }}
|
|
516
|
+
.modal-close:hover {{ background: var(--bg3); color: var(--text); }}
|
|
517
|
+
.modal-section {{ margin-bottom: 16px; }}
|
|
518
|
+
.modal-section-title {{ font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text3); margin-bottom: 8px; font-weight: 600; }}
|
|
519
|
+
.modal-code {{ background: var(--bg); padding: 10px 14px; border-radius: var(--radius-sm); font-family: 'Fira Code', monospace; font-size: 13px; }}
|
|
520
|
+
.modal-tag {{ display: inline-block; background: var(--bg); padding: 4px 10px; border-radius: 6px; margin: 2px; font-size: 12px; color: var(--text2); border: 1px solid var(--bg3); cursor: pointer; }}
|
|
521
|
+
.modal-tag:hover {{ border-color: var(--accent); color: var(--accent); }}
|
|
522
|
+
|
|
523
|
+
/* Responsive */
|
|
524
|
+
@media (max-width: 1200px) {{
|
|
525
|
+
.dash-stats {{ grid-template-columns: repeat(2, 1fr); }}
|
|
526
|
+
.charts-row {{ grid-template-columns: 1fr; }}
|
|
527
|
+
}}
|
|
528
|
+
</style>
|
|
529
|
+
</head>
|
|
530
|
+
<body>
|
|
531
|
+
<div class="sidebar">
|
|
532
|
+
<div class="logo"><div class="logo-icon">C</div> CortexCode</div>
|
|
533
|
+
|
|
534
|
+
<div class="sidebar-stats">
|
|
535
|
+
<div class="sidebar-stat"><div class="sidebar-stat-val">{len(files)}</div><div class="sidebar-stat-lbl">Files</div></div>
|
|
536
|
+
<div class="sidebar-stat"><div class="sidebar-stat-val">{len(all_symbols)}</div><div class="sidebar-stat-lbl">Symbols</div></div>
|
|
537
|
+
<div class="sidebar-stat"><div class="sidebar-stat-val">{non_empty_calls}</div><div class="sidebar-stat-lbl">Linked</div></div>
|
|
538
|
+
<div class="sidebar-stat"><div class="sidebar-stat-val">{len(file_deps)}</div><div class="sidebar-stat-lbl">Deps</div></div>
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<div class="nav-section">
|
|
542
|
+
<div class="nav-title">Dashboard</div>
|
|
543
|
+
<div class="nav-item active" onclick="showTab('overview', this)">📊 Overview</div>
|
|
544
|
+
<div class="nav-item" onclick="showTab('symbols', this)">⚡ Symbols <span class="badge">{len(all_symbols)}</span></div>
|
|
545
|
+
<div class="nav-item" onclick="showTab('graph', this)">🔗 Call Graph <span class="badge">{non_empty_calls}</span></div>
|
|
546
|
+
<div class="nav-item" onclick="showTab('deps', this)">🌐 File Deps <span class="badge">{len(file_deps)}</span></div>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="nav-section">
|
|
549
|
+
<div class="nav-title">Explore</div>
|
|
550
|
+
<div class="nav-item" onclick="showTab('structure', this)">📁 Files <span class="badge">{len(files)}</span></div>
|
|
551
|
+
<div class="nav-item" onclick="showTab('imports', this)">📥 Imports <span class="badge">{len(all_imports)}</span></div>
|
|
552
|
+
<div class="nav-item" onclick="showTab('exports', this)">📤 Exports <span class="badge">{len(all_exports)}</span></div>
|
|
553
|
+
<div class="nav-item" onclick="showTab('routes', this)">🔌 API Routes <span class="badge">{len(all_api_routes)}</span></div>
|
|
554
|
+
<div class="nav-item" onclick="showTab('entities', this)">🗄️ Entities <span class="badge">{len(all_entities)}</span></div>
|
|
555
|
+
</div>
|
|
556
|
+
<div class="nav-section">
|
|
557
|
+
<div class="nav-title">Docs</div>
|
|
558
|
+
<div class="nav-item" onclick="window.open('README.md','_blank')">📖 README</div>
|
|
559
|
+
<div class="nav-item" onclick="window.open('API.md','_blank')">📑 API Docs</div>
|
|
560
|
+
<div class="nav-item" onclick="window.open('FLOWS.md','_blank')">🔀 Call Flows</div>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<div class="main">
|
|
565
|
+
<div class="header">
|
|
566
|
+
<div>
|
|
567
|
+
<h1 class="project-name">{project_name}</h1>
|
|
568
|
+
<p class="last-indexed">Indexed {last_indexed[:19] if last_indexed else 'N/A'} · {len(index.get("languages", []))} languages</p>
|
|
569
|
+
</div>
|
|
570
|
+
<div class="search-wrapper">
|
|
571
|
+
<span class="search-icon">🔍</span>
|
|
572
|
+
<input type="text" id="globalSearchInput" class="search-box" placeholder="Search symbols, files..." onkeyup="doGlobalSearch(this.value)" autocomplete="off">
|
|
573
|
+
<div class="search-results" id="searchResults"></div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<!-- OVERVIEW TAB -->
|
|
578
|
+
<div id="overview" class="tab-content active">
|
|
579
|
+
<div class="dash-stats">
|
|
580
|
+
<div class="dash-stat">
|
|
581
|
+
<div class="dash-stat-icon">📄</div>
|
|
582
|
+
<div class="dash-stat-val">{len(files)}</div>
|
|
583
|
+
<div class="dash-stat-lbl">Source Files</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="dash-stat">
|
|
586
|
+
<div class="dash-stat-icon">⚡</div>
|
|
587
|
+
<div class="dash-stat-val">{len(all_symbols)}</div>
|
|
588
|
+
<div class="dash-stat-lbl">Symbols</div>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="dash-stat">
|
|
591
|
+
<div class="dash-stat-icon">🔗</div>
|
|
592
|
+
<div class="dash-stat-val">{total_call_edges}</div>
|
|
593
|
+
<div class="dash-stat-lbl">Call Edges</div>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="dash-stat">
|
|
596
|
+
<div class="dash-stat-icon">🌐</div>
|
|
597
|
+
<div class="dash-stat-val">{len(file_deps)}</div>
|
|
598
|
+
<div class="dash-stat-lbl">File Dependencies</div>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
<div class="charts-row">
|
|
603
|
+
<div class="card">
|
|
604
|
+
<div class="card-title">Symbol Types</div>
|
|
605
|
+
<div class="card-subtitle">{len(all_symbols)} symbols across {len(type_counts)} types</div>
|
|
606
|
+
<div class="chart-container">
|
|
607
|
+
<svg id="typeDonut" class="chart-svg" width="160" height="160"></svg>
|
|
608
|
+
<div class="chart-legend" id="typeLegend"></div>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
<div class="card">
|
|
612
|
+
<div class="card-title">Languages</div>
|
|
613
|
+
<div class="card-subtitle">{len(files)} files across {len(language_counts)} extensions</div>
|
|
614
|
+
<div class="chart-container">
|
|
615
|
+
<svg id="langDonut" class="chart-svg" width="160" height="160"></svg>
|
|
616
|
+
<div class="chart-legend" id="langLegend"></div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<div class="charts-row">
|
|
622
|
+
<div class="card">
|
|
623
|
+
<div class="card-title">Top Files by Symbols</div>
|
|
624
|
+
<table class="mini-table">
|
|
625
|
+
<thead><tr><th>File</th><th style="width:50px;">Count</th><th style="width:120px;"></th></tr></thead>
|
|
626
|
+
<tbody>
|
|
627
|
+
{top_files_rows}
|
|
628
|
+
</tbody>
|
|
629
|
+
</table>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="card">
|
|
632
|
+
<div class="card-title">Top Callers</div>
|
|
633
|
+
<div class="card-subtitle">Functions that call the most other functions</div>
|
|
634
|
+
<table class="mini-table">
|
|
635
|
+
<thead><tr><th>Symbol</th><th style="width:50px;">Calls</th><th style="width:120px;"></th></tr></thead>
|
|
636
|
+
<tbody>
|
|
637
|
+
{top_callers_rows}
|
|
638
|
+
</tbody>
|
|
639
|
+
</table>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
{fw_cards_html}
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<!-- SYMBOLS TAB -->
|
|
647
|
+
<div id="symbols" class="tab-content">
|
|
648
|
+
<div class="card">
|
|
649
|
+
<div class="card-header">
|
|
650
|
+
<div>
|
|
651
|
+
<div class="card-title">All Symbols</div>
|
|
652
|
+
<div class="card-subtitle">{len(all_symbols)} symbols extracted from {len(files)} files</div>
|
|
653
|
+
</div>
|
|
654
|
+
<input type="text" class="search-box" style="width:260px;padding-left:14px;" placeholder="Filter symbols..." onkeyup="filterSymbols(this.value)">
|
|
655
|
+
</div>
|
|
656
|
+
<div class="filter-tabs" id="symbolFilterTabs">
|
|
657
|
+
<div class="filter-tab active" onclick="filterByType('all', this)">All ({len(all_symbols)})</div>
|
|
658
|
+
{filter_tabs_html}
|
|
659
|
+
</div>
|
|
660
|
+
<div class="symbol-grid" id="symbolGrid">
|
|
661
|
+
{generate_symbols_html(all_symbols[:200])}
|
|
662
|
+
</div>
|
|
663
|
+
{f'<p style="color:var(--text3);margin-top:16px;font-size:13px;">Showing 200 of {len(all_symbols)} symbols — use search to find more</p>' if len(all_symbols) > 200 else ''}
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<!-- CALL GRAPH TAB -->
|
|
668
|
+
<div id="graph" class="tab-content">
|
|
669
|
+
<div class="card">
|
|
670
|
+
<div class="card-header">
|
|
671
|
+
<div>
|
|
672
|
+
<div class="card-title">Call Graph</div>
|
|
673
|
+
<div class="card-subtitle">{non_empty_calls} symbols with {total_call_edges} call edges — click a node to explore</div>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
<div class="graph-controls">
|
|
677
|
+
<input type="text" class="graph-search" id="graphSearch" placeholder="Search node..." oninput="searchGraphNode(this.value)">
|
|
678
|
+
<div class="graph-btn" onclick="resetGraph()">Reset</div>
|
|
679
|
+
<div class="graph-btn" onclick="zoomIn()">Zoom +</div>
|
|
680
|
+
<div class="graph-btn" onclick="zoomOut()">Zoom −</div>
|
|
681
|
+
<div class="graph-btn" id="toggleLabels" onclick="toggleLabels()">Hide Labels</div>
|
|
682
|
+
<span style="margin-left:auto;font-size:12px;color:var(--text3);" id="graphNodeCount"></span>
|
|
683
|
+
</div>
|
|
684
|
+
<div class="graph-container" id="graphContainer">
|
|
685
|
+
<div class="graph-tooltip" id="graphTooltip"></div>
|
|
686
|
+
</div>
|
|
687
|
+
<div class="graph-info" id="graphInfo"></div>
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
|
|
691
|
+
<!-- FILE DEPS TAB -->
|
|
692
|
+
<div id="deps" class="tab-content">
|
|
693
|
+
<div class="card">
|
|
694
|
+
<div class="card-header">
|
|
695
|
+
<div>
|
|
696
|
+
<div class="card-title">File Dependency Graph</div>
|
|
697
|
+
<div class="card-subtitle">{len(file_deps)} files with tracked import relationships</div>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
<div class="graph-controls">
|
|
701
|
+
<input type="text" class="graph-search" id="depsSearch" placeholder="Search file..." oninput="searchDepsNode(this.value)">
|
|
702
|
+
<div class="graph-btn" onclick="resetDeps()">Reset</div>
|
|
703
|
+
</div>
|
|
704
|
+
<div class="deps-container" id="depsContainer"></div>
|
|
705
|
+
<div class="graph-info" id="depsInfo"></div>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
|
|
709
|
+
<!-- STRUCTURE TAB -->
|
|
710
|
+
<div id="structure" class="tab-content">
|
|
711
|
+
<div class="card">
|
|
712
|
+
<div class="card-header">
|
|
713
|
+
<div class="card-title">File Tree</div>
|
|
714
|
+
<input type="text" class="search-box" style="width:260px;padding-left:14px;" placeholder="Filter files..." onkeyup="filterTree(this.value)">
|
|
715
|
+
</div>
|
|
716
|
+
<div class="tree-view" id="fileTree">{generate_tree_html(file_tree, 0)}</div>
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
|
|
720
|
+
<!-- IMPORTS TAB -->
|
|
721
|
+
<div id="imports" class="tab-content">
|
|
722
|
+
<div class="card">
|
|
723
|
+
<div class="card-header">
|
|
724
|
+
<div class="card-title">Imports ({len(all_imports)})</div>
|
|
725
|
+
</div>
|
|
726
|
+
<div class="filter-tabs">
|
|
727
|
+
<div class="filter-tab active" onclick="filterImports('all', this)">All</div>
|
|
728
|
+
<div class="filter-tab" onclick="filterImports('external', this)">External</div>
|
|
729
|
+
<div class="filter-tab" onclick="filterImports('internal', this)">Internal</div>
|
|
730
|
+
</div>
|
|
731
|
+
<div class="symbol-grid">{generate_imports_html(all_imports[:80])}</div>
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
|
|
735
|
+
<!-- EXPORTS TAB -->
|
|
736
|
+
<div id="exports" class="tab-content">
|
|
737
|
+
<div class="card">
|
|
738
|
+
<div class="card-title">Exports ({len(all_exports)})</div>
|
|
739
|
+
<div class="symbol-grid" style="margin-top:16px;">{generate_exports_html(all_exports[:80])}</div>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
|
|
743
|
+
<!-- API ROUTES TAB -->
|
|
744
|
+
<div id="routes" class="tab-content">
|
|
745
|
+
<div class="card">
|
|
746
|
+
<div class="card-title">API Routes ({len(all_api_routes)})</div>
|
|
747
|
+
<div class="symbol-grid" style="margin-top:16px;">{generate_routes_html(all_api_routes[:80])}</div>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
|
|
751
|
+
<!-- ENTITIES TAB -->
|
|
752
|
+
<div id="entities" class="tab-content">
|
|
753
|
+
<div class="card">
|
|
754
|
+
<div class="card-title">Database Entities ({len(all_entities)})</div>
|
|
755
|
+
<div class="symbol-grid" style="margin-top:16px;">{generate_entities_html(all_entities[:80])}</div>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
|
|
760
|
+
<!-- Symbol Detail Modal -->
|
|
761
|
+
<div class="modal" id="symbolModal">
|
|
762
|
+
<div class="modal-content">
|
|
763
|
+
<div class="modal-header">
|
|
764
|
+
<div class="modal-title" id="modalTitle"></div>
|
|
765
|
+
<button class="modal-close" onclick="closeModal()">×</button>
|
|
766
|
+
</div>
|
|
767
|
+
<div id="modalBody"></div>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<script>
|
|
772
|
+
// ─── DATA ───
|
|
773
|
+
const callGraphData = {call_graph_json};
|
|
774
|
+
const fileDepsData = {file_deps_json};
|
|
775
|
+
const typeCountsData = {type_counts_json};
|
|
776
|
+
const langCountsData = {lang_counts_json};
|
|
777
|
+
const typeColors = {type_colors_json};
|
|
778
|
+
const langColors = {lang_colors_json};
|
|
779
|
+
const searchData = {search_data_json};
|
|
780
|
+
|
|
781
|
+
// ─── TABS ───
|
|
782
|
+
function showTab(id, el) {{
|
|
783
|
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
784
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
785
|
+
document.getElementById(id).classList.add('active');
|
|
786
|
+
if (el) el.classList.add('active');
|
|
787
|
+
if (id === 'graph') initGraph();
|
|
788
|
+
if (id === 'deps') initDeps();
|
|
789
|
+
}}
|
|
790
|
+
|
|
791
|
+
// ─── DONUT CHARTS ───
|
|
792
|
+
function drawDonut(svgId, legendId, data, colorMap, defaultColor) {{
|
|
793
|
+
const entries = Object.entries(data).sort((a,b) => b[1] - a[1]);
|
|
794
|
+
const total = entries.reduce((s, e) => s + e[1], 0);
|
|
795
|
+
if (!total) return;
|
|
796
|
+
|
|
797
|
+
const svg = d3.select('#' + svgId);
|
|
798
|
+
const w = 160, h = 160, r = 65, inner = 40;
|
|
799
|
+
const g = svg.append('g').attr('transform', `translate(${{w/2}},${{h/2}})`);
|
|
800
|
+
|
|
801
|
+
const pie = d3.pie().value(d => d[1]).sort(null).padAngle(0.02);
|
|
802
|
+
const arc = d3.arc().innerRadius(inner).outerRadius(r);
|
|
803
|
+
|
|
804
|
+
g.selectAll('path').data(pie(entries)).join('path')
|
|
805
|
+
.attr('d', arc)
|
|
806
|
+
.attr('fill', d => colorMap[d.data[0]] || defaultColor || '#475569')
|
|
807
|
+
.attr('stroke', 'var(--bg2)')
|
|
808
|
+
.attr('stroke-width', 2)
|
|
809
|
+
.style('cursor', 'pointer')
|
|
810
|
+
.on('mouseover', function() {{ d3.select(this).attr('opacity', 0.8); }})
|
|
811
|
+
.on('mouseout', function() {{ d3.select(this).attr('opacity', 1); }});
|
|
812
|
+
|
|
813
|
+
g.append('text').text(total).attr('text-anchor','middle').attr('dy','-0.1em').attr('fill','white').attr('font-size','22px').attr('font-weight','700');
|
|
814
|
+
g.append('text').text('total').attr('text-anchor','middle').attr('dy','1.2em').attr('fill','var(--text3)').attr('font-size','11px');
|
|
815
|
+
|
|
816
|
+
const legend = document.getElementById(legendId);
|
|
817
|
+
legend.innerHTML = entries.slice(0, 8).map(([k, v]) => `
|
|
818
|
+
<div class="chart-legend-item">
|
|
819
|
+
<div class="chart-legend-dot" style="background:${{colorMap[k] || defaultColor || '#475569'}}"></div>
|
|
820
|
+
<span>${{k}}</span>
|
|
821
|
+
<span class="chart-legend-val">${{v}}</span>
|
|
822
|
+
</div>
|
|
823
|
+
`).join('');
|
|
824
|
+
}}
|
|
825
|
+
|
|
826
|
+
drawDonut('typeDonut', 'typeLegend', typeCountsData, typeColors, '#475569');
|
|
827
|
+
drawDonut('langDonut', 'langLegend', langCountsData, langColors, '#475569');
|
|
828
|
+
|
|
829
|
+
// ─── GLOBAL SEARCH ───
|
|
830
|
+
function doGlobalSearch(q) {{
|
|
831
|
+
const box = document.getElementById('searchResults');
|
|
832
|
+
if (!q || q.length < 2) {{ box.style.display = 'none'; return; }}
|
|
833
|
+
q = q.toLowerCase();
|
|
834
|
+
const results = searchData.filter(s => s.name.toLowerCase().includes(q) || s.file.toLowerCase().includes(q)).slice(0, 15);
|
|
835
|
+
if (!results.length) {{ box.innerHTML = '<div style="padding:14px;color:var(--text3);">No results</div>'; }}
|
|
836
|
+
else {{
|
|
837
|
+
box.innerHTML = results.map(s => `
|
|
838
|
+
<div class="search-result-item" onclick="showTab('symbols',document.querySelectorAll('.nav-item')[1]);filterSymbols('${{s.name}}');document.getElementById('searchResults').style.display='none';">
|
|
839
|
+
<span class="badge badge-type">${{s.type}}</span>
|
|
840
|
+
<span style="color:var(--accent);font-weight:600;">${{s.name}}</span>
|
|
841
|
+
<span style="color:var(--text3);font-size:11px;margin-left:auto;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${{s.file}}:${{s.line}}</span>
|
|
842
|
+
</div>
|
|
843
|
+
`).join('');
|
|
844
|
+
}}
|
|
845
|
+
box.style.display = 'block';
|
|
846
|
+
}}
|
|
847
|
+
document.addEventListener('click', e => {{
|
|
848
|
+
if (!e.target.closest('.search-wrapper')) document.getElementById('searchResults').style.display = 'none';
|
|
849
|
+
}});
|
|
850
|
+
|
|
851
|
+
// ─── SYMBOL FILTERS ───
|
|
852
|
+
function filterSymbols(q) {{
|
|
853
|
+
document.querySelectorAll('.symbol-card').forEach(c => {{
|
|
854
|
+
const name = c.querySelector('.symbol-name').textContent.toLowerCase();
|
|
855
|
+
const file = c.querySelector('.symbol-file')?.textContent?.toLowerCase() || '';
|
|
856
|
+
c.style.display = (name.includes(q.toLowerCase()) || file.includes(q.toLowerCase())) ? '' : 'none';
|
|
857
|
+
}});
|
|
858
|
+
}}
|
|
859
|
+
function filterByType(type, btn) {{
|
|
860
|
+
document.querySelectorAll('#symbolFilterTabs .filter-tab').forEach(t => t.classList.remove('active'));
|
|
861
|
+
btn.classList.add('active');
|
|
862
|
+
document.querySelectorAll('.symbol-card').forEach(c => {{
|
|
863
|
+
c.style.display = (type === 'all' || c.dataset.type === type) ? '' : 'none';
|
|
864
|
+
}});
|
|
865
|
+
}}
|
|
866
|
+
function filterTree(q) {{
|
|
867
|
+
document.querySelectorAll('.tree-item').forEach(i => {{
|
|
868
|
+
i.style.display = i.textContent.toLowerCase().includes(q.toLowerCase()) ? 'flex' : 'none';
|
|
869
|
+
}});
|
|
870
|
+
}}
|
|
871
|
+
function filterImports(type, btn) {{
|
|
872
|
+
btn.parentElement.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
|
|
873
|
+
btn.classList.add('active');
|
|
874
|
+
btn.closest('.card').querySelectorAll('.symbol-card').forEach(c => {{
|
|
875
|
+
if (type === 'all') c.style.display = '';
|
|
876
|
+
else c.style.display = (c.dataset.external === (type === 'external' ? 'True' : 'False')) ? '' : 'none';
|
|
877
|
+
}});
|
|
878
|
+
}}
|
|
879
|
+
|
|
880
|
+
// ─── SYMBOL MODAL ───
|
|
881
|
+
function showSymbolDetail(sym) {{
|
|
882
|
+
document.getElementById('modalTitle').textContent = sym.name;
|
|
883
|
+
let b = `<div class="modal-section"><div style="display:flex;gap:6px;flex-wrap:wrap;">`;
|
|
884
|
+
b += `<span class="badge badge-type">${{sym.type}}</span>`;
|
|
885
|
+
if (sym.framework) b += `<span class="badge badge-fw">${{sym.framework}}</span>`;
|
|
886
|
+
if (sym.doc) b += `<span class="badge badge-doc">documented</span>`;
|
|
887
|
+
b += `</div></div>`;
|
|
888
|
+
b += `<div class="modal-section"><div class="modal-section-title">Location</div><div class="modal-code">${{sym.file}}:${{sym.line}}</div></div>`;
|
|
889
|
+
if (sym.doc) b += `<div class="modal-section"><div class="modal-section-title">Documentation</div><p style="color:var(--text2);font-size:13px;line-height:1.6;">${{sym.doc}}</p></div>`;
|
|
890
|
+
if (sym.params?.length) b += `<div class="modal-section"><div class="modal-section-title">Parameters</div><div class="modal-code">${{sym.params.join(', ')}}</div></div>`;
|
|
891
|
+
if (sym.return_type) b += `<div class="modal-section"><div class="modal-section-title">Returns</div><div class="modal-code">${{sym.return_type}}</div></div>`;
|
|
892
|
+
if (sym.calls?.length) {{
|
|
893
|
+
b += `<div class="modal-section"><div class="modal-section-title">Calls (${{sym.calls.length}})</div><div style="display:flex;flex-wrap:wrap;gap:4px;">`;
|
|
894
|
+
sym.calls.forEach(c => {{ b += `<span class="modal-tag" onclick="closeModal();highlightGraphNode('${{c}}')">${{c}}</span>`; }});
|
|
895
|
+
b += `</div></div>`;
|
|
896
|
+
}}
|
|
897
|
+
// Find callers
|
|
898
|
+
const callers = Object.entries(callGraphData).filter(([k,v]) => v.includes(sym.name)).map(([k]) => k);
|
|
899
|
+
if (callers.length) {{
|
|
900
|
+
b += `<div class="modal-section"><div class="modal-section-title">Called By (${{callers.length}})</div><div style="display:flex;flex-wrap:wrap;gap:4px;">`;
|
|
901
|
+
callers.slice(0, 15).forEach(c => {{ b += `<span class="modal-tag" onclick="closeModal();highlightGraphNode('${{c}}')">${{c}}</span>`; }});
|
|
902
|
+
b += `</div></div>`;
|
|
903
|
+
}}
|
|
904
|
+
if (sym.methods?.length) {{
|
|
905
|
+
b += `<div class="modal-section"><div class="modal-section-title">Methods (${{sym.methods.length}})</div>`;
|
|
906
|
+
sym.methods.forEach(m => {{ b += `<div style="padding:6px 0;border-bottom:1px solid var(--bg3);font-size:13px;font-family:monospace;">${{m.name}}(${{(m.params||[]).join(', ')}})</div>`; }});
|
|
907
|
+
b += `</div>`;
|
|
908
|
+
}}
|
|
909
|
+
document.getElementById('modalBody').innerHTML = b;
|
|
910
|
+
document.getElementById('symbolModal').classList.add('active');
|
|
911
|
+
}}
|
|
912
|
+
function closeModal() {{ document.getElementById('symbolModal').classList.remove('active'); }}
|
|
913
|
+
document.getElementById('symbolModal').addEventListener('click', function(e) {{ if (e.target === this) closeModal(); }});
|
|
914
|
+
|
|
915
|
+
// ─── CALL GRAPH (D3 Force) ───
|
|
916
|
+
let graphSim, graphSvg, graphG, graphZoom, graphNodes, graphLinks, labelsVisible = true;
|
|
917
|
+
let graphInited = false;
|
|
918
|
+
|
|
919
|
+
function initGraph() {{
|
|
920
|
+
if (graphInited) return;
|
|
921
|
+
graphInited = true;
|
|
922
|
+
|
|
923
|
+
const allIds = new Set(Object.keys(callGraphData));
|
|
924
|
+
Object.values(callGraphData).forEach(ts => ts.forEach(t => allIds.add(t)));
|
|
925
|
+
|
|
926
|
+
// Limit to nodes with connections
|
|
927
|
+
const connected = new Set();
|
|
928
|
+
Object.entries(callGraphData).forEach(([s, ts]) => {{
|
|
929
|
+
if (ts.length) {{ connected.add(s); ts.forEach(t => connected.add(t)); }}
|
|
930
|
+
}});
|
|
931
|
+
const nodeIds = Array.from(connected).slice(0, 120);
|
|
932
|
+
const nodeSet = new Set(nodeIds);
|
|
933
|
+
|
|
934
|
+
const nodes = nodeIds.map(id => ({{
|
|
935
|
+
id,
|
|
936
|
+
calls: (callGraphData[id] || []).length,
|
|
937
|
+
calledBy: Object.values(callGraphData).filter(v => v.includes(id)).length,
|
|
938
|
+
isCaller: !!callGraphData[id]?.length
|
|
939
|
+
}}));
|
|
940
|
+
|
|
941
|
+
const links = [];
|
|
942
|
+
Object.entries(callGraphData).forEach(([s, ts]) => {{
|
|
943
|
+
ts.forEach(t => {{
|
|
944
|
+
if (nodeSet.has(s) && nodeSet.has(t)) links.push({{ source: s, target: t }});
|
|
945
|
+
}});
|
|
946
|
+
}});
|
|
947
|
+
|
|
948
|
+
document.getElementById('graphNodeCount').textContent = `${{nodes.length}} nodes · ${{links.length}} edges`;
|
|
949
|
+
|
|
950
|
+
const container = document.getElementById('graphContainer');
|
|
951
|
+
const W = container.clientWidth || 900, H = 600;
|
|
952
|
+
|
|
953
|
+
graphSvg = d3.select('#graphContainer').append('svg').attr('width', W).attr('height', H);
|
|
954
|
+
graphG = graphSvg.append('g');
|
|
955
|
+
|
|
956
|
+
graphZoom = d3.zoom().scaleExtent([0.2, 5]).on('zoom', e => graphG.attr('transform', e.transform));
|
|
957
|
+
graphSvg.call(graphZoom);
|
|
958
|
+
|
|
959
|
+
// Arrow markers
|
|
960
|
+
graphSvg.append('defs').append('marker').attr('id','arrowhead').attr('viewBox','0 -5 10 10').attr('refX',20).attr('refY',0).attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto').append('path').attr('d','M0,-5L10,0L0,5').attr('fill','#475569');
|
|
961
|
+
|
|
962
|
+
graphLinks = graphG.append('g').selectAll('line').data(links).join('line')
|
|
963
|
+
.attr('class', 'link').attr('stroke', '#475569').attr('stroke-width', 1).attr('marker-end', 'url(#arrowhead)');
|
|
964
|
+
|
|
965
|
+
const nodeG = graphG.append('g').selectAll('g').data(nodes).join('g')
|
|
966
|
+
.attr('class', 'node').style('cursor', 'pointer');
|
|
967
|
+
|
|
968
|
+
nodeG.append('circle')
|
|
969
|
+
.attr('r', d => 5 + Math.min(d.calls + d.calledBy, 15))
|
|
970
|
+
.attr('fill', d => d.isCaller ? 'var(--accent)' : 'var(--accent2)')
|
|
971
|
+
.attr('stroke', '#fff').attr('stroke-width', 1.5);
|
|
972
|
+
|
|
973
|
+
nodeG.append('text')
|
|
974
|
+
.text(d => d.id.length > 18 ? d.id.substring(0, 18) + '…' : d.id)
|
|
975
|
+
.attr('x', d => 8 + Math.min(d.calls + d.calledBy, 15))
|
|
976
|
+
.attr('y', 4).attr('fill', 'var(--text)').attr('font-size', '11px')
|
|
977
|
+
.attr('class', 'node-label');
|
|
978
|
+
|
|
979
|
+
graphNodes = nodeG;
|
|
980
|
+
|
|
981
|
+
// Hover tooltip
|
|
982
|
+
const tooltip = document.getElementById('graphTooltip');
|
|
983
|
+
nodeG.on('mouseover', (e, d) => {{
|
|
984
|
+
tooltip.innerHTML = `<strong style="color:var(--accent)">${{d.id}}</strong><br><span style="color:var(--text3)">Calls: ${{d.calls}} · Called by: ${{d.calledBy}}</span>`;
|
|
985
|
+
tooltip.style.display = 'block';
|
|
986
|
+
tooltip.style.left = (e.offsetX + 12) + 'px';
|
|
987
|
+
tooltip.style.top = (e.offsetY - 10) + 'px';
|
|
988
|
+
}}).on('mouseout', () => {{ tooltip.style.display = 'none'; }});
|
|
989
|
+
|
|
990
|
+
// Click to highlight
|
|
991
|
+
nodeG.on('click', (e, d) => {{
|
|
992
|
+
e.stopPropagation();
|
|
993
|
+
highlightNode(d.id, nodes, links);
|
|
994
|
+
}});
|
|
995
|
+
|
|
996
|
+
graphSvg.on('click', () => {{ resetHighlight(); }});
|
|
997
|
+
|
|
998
|
+
nodeG.call(d3.drag()
|
|
999
|
+
.on('start', (e, d) => {{ if (!e.active) graphSim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }})
|
|
1000
|
+
.on('drag', (e, d) => {{ d.fx = e.x; d.fy = e.y; }})
|
|
1001
|
+
.on('end', (e, d) => {{ if (!e.active) graphSim.alphaTarget(0); d.fx = null; d.fy = null; }}));
|
|
1002
|
+
|
|
1003
|
+
graphSim = d3.forceSimulation(nodes)
|
|
1004
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(70))
|
|
1005
|
+
.force('charge', d3.forceManyBody().strength(-150))
|
|
1006
|
+
.force('center', d3.forceCenter(W / 2, H / 2))
|
|
1007
|
+
.force('collision', d3.forceCollide().radius(d => 10 + Math.min(d.calls + d.calledBy, 15)));
|
|
1008
|
+
|
|
1009
|
+
graphSim.on('tick', () => {{
|
|
1010
|
+
graphLinks.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
1011
|
+
graphNodes.attr('transform', d => `translate(${{d.x}},${{d.y}})`);
|
|
1012
|
+
}});
|
|
1013
|
+
}}
|
|
1014
|
+
|
|
1015
|
+
function highlightNode(id, nodes, links) {{
|
|
1016
|
+
const calls = callGraphData[id] || [];
|
|
1017
|
+
const callers = Object.entries(callGraphData).filter(([k,v]) => v.includes(id)).map(([k]) => k);
|
|
1018
|
+
const related = new Set([id, ...calls, ...callers]);
|
|
1019
|
+
|
|
1020
|
+
graphNodes.classed('dimmed', d => !related.has(d.id));
|
|
1021
|
+
graphLinks.classed('dimmed', d => d.source.id !== id && d.target.id !== id);
|
|
1022
|
+
graphLinks.classed('highlighted', d => d.source.id === id || d.target.id === id);
|
|
1023
|
+
|
|
1024
|
+
const info = document.getElementById('graphInfo');
|
|
1025
|
+
info.classList.add('active');
|
|
1026
|
+
info.innerHTML = `
|
|
1027
|
+
<strong style="color:var(--accent);font-size:16px;">${{id}}</strong>
|
|
1028
|
+
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
1029
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Calls (${{calls.length}})</div>
|
|
1030
|
+
${{calls.length ? calls.map(c => `<span class="modal-tag" onclick="highlightGraphNode('${{c}}')">${{c}}</span>`).join('') : '<span style="color:var(--text3)">none</span>'}}</div>
|
|
1031
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Called By (${{callers.length}})</div>
|
|
1032
|
+
${{callers.length ? callers.slice(0,10).map(c => `<span class="modal-tag" onclick="highlightGraphNode('${{c}}')">${{c}}</span>`).join('') : '<span style="color:var(--text3)">none</span>'}}</div>
|
|
1033
|
+
</div>`;
|
|
1034
|
+
}}
|
|
1035
|
+
|
|
1036
|
+
function resetHighlight() {{
|
|
1037
|
+
if (!graphNodes) return;
|
|
1038
|
+
graphNodes.classed('dimmed', false);
|
|
1039
|
+
graphLinks.classed('dimmed', false).classed('highlighted', false);
|
|
1040
|
+
document.getElementById('graphInfo').classList.remove('active');
|
|
1041
|
+
}}
|
|
1042
|
+
|
|
1043
|
+
function highlightGraphNode(name) {{
|
|
1044
|
+
showTab('graph', document.querySelectorAll('.nav-item')[2]);
|
|
1045
|
+
setTimeout(() => {{
|
|
1046
|
+
const nd = graphNodes?.data()?.find(d => d.id === name);
|
|
1047
|
+
if (nd) highlightNode(name, graphNodes.data(), graphLinks.data());
|
|
1048
|
+
}}, 100);
|
|
1049
|
+
}}
|
|
1050
|
+
|
|
1051
|
+
function searchGraphNode(q) {{
|
|
1052
|
+
if (!graphNodes) return;
|
|
1053
|
+
if (!q) {{ resetHighlight(); return; }}
|
|
1054
|
+
q = q.toLowerCase();
|
|
1055
|
+
graphNodes.classed('dimmed', d => !d.id.toLowerCase().includes(q));
|
|
1056
|
+
graphLinks.classed('dimmed', true);
|
|
1057
|
+
}}
|
|
1058
|
+
|
|
1059
|
+
function resetGraph() {{ resetHighlight(); if (graphSvg) graphSvg.transition().call(graphZoom.transform, d3.zoomIdentity); }}
|
|
1060
|
+
function zoomIn() {{ if (graphSvg) graphSvg.transition().call(graphZoom.scaleBy, 1.4); }}
|
|
1061
|
+
function zoomOut() {{ if (graphSvg) graphSvg.transition().call(graphZoom.scaleBy, 0.7); }}
|
|
1062
|
+
function toggleLabels() {{
|
|
1063
|
+
labelsVisible = !labelsVisible;
|
|
1064
|
+
d3.selectAll('.node-label').style('display', labelsVisible ? 'block' : 'none');
|
|
1065
|
+
document.getElementById('toggleLabels').textContent = labelsVisible ? 'Hide Labels' : 'Show Labels';
|
|
1066
|
+
}}
|
|
1067
|
+
|
|
1068
|
+
// ─── FILE DEPS GRAPH ───
|
|
1069
|
+
let depsInited = false;
|
|
1070
|
+
function initDeps() {{
|
|
1071
|
+
if (depsInited) return;
|
|
1072
|
+
depsInited = true;
|
|
1073
|
+
|
|
1074
|
+
const allFiles = new Set();
|
|
1075
|
+
Object.entries(fileDepsData).forEach(([f, deps]) => {{
|
|
1076
|
+
allFiles.add(f); deps.forEach(d => allFiles.add(d));
|
|
1077
|
+
}});
|
|
1078
|
+
const fileArr = Array.from(allFiles).slice(0, 80);
|
|
1079
|
+
const fileSet = new Set(fileArr);
|
|
1080
|
+
const nodes = fileArr.map(f => ({{ id: f, short: f.split('/').pop() }}));
|
|
1081
|
+
const links = [];
|
|
1082
|
+
Object.entries(fileDepsData).forEach(([s, deps]) => {{
|
|
1083
|
+
deps.forEach(t => {{ if (fileSet.has(s) && fileSet.has(t)) links.push({{ source: s, target: t }}); }});
|
|
1084
|
+
}});
|
|
1085
|
+
|
|
1086
|
+
const container = document.getElementById('depsContainer');
|
|
1087
|
+
const W = container.clientWidth || 900, H = 500;
|
|
1088
|
+
const svg = d3.select('#depsContainer').append('svg').attr('width', W).attr('height', H);
|
|
1089
|
+
const g = svg.append('g');
|
|
1090
|
+
|
|
1091
|
+
const zoom = d3.zoom().scaleExtent([0.2, 4]).on('zoom', e => g.attr('transform', e.transform));
|
|
1092
|
+
svg.call(zoom);
|
|
1093
|
+
|
|
1094
|
+
svg.append('defs').append('marker').attr('id','depArrow').attr('viewBox','0 -5 10 10').attr('refX',14).attr('refY',0).attr('markerWidth',5).attr('markerHeight',5).attr('orient','auto').append('path').attr('d','M0,-5L10,0L0,5').attr('fill','var(--green)');
|
|
1095
|
+
|
|
1096
|
+
const link = g.append('g').selectAll('line').data(links).join('line')
|
|
1097
|
+
.attr('stroke', 'var(--green)').attr('stroke-opacity', 0.3).attr('stroke-width', 1).attr('marker-end', 'url(#depArrow)');
|
|
1098
|
+
|
|
1099
|
+
const node = g.append('g').selectAll('g').data(nodes).join('g').style('cursor','pointer');
|
|
1100
|
+
node.append('circle').attr('r', 6).attr('fill', 'var(--green)').attr('stroke', '#fff').attr('stroke-width', 1);
|
|
1101
|
+
node.append('text').text(d => d.short.length > 20 ? d.short.substring(0,20)+'…' : d.short).attr('x',10).attr('y',4).attr('fill','var(--text2)').attr('font-size','10px');
|
|
1102
|
+
|
|
1103
|
+
node.on('click', (e, d) => {{
|
|
1104
|
+
e.stopPropagation();
|
|
1105
|
+
const imports = fileDepsData[d.id] || [];
|
|
1106
|
+
const importedBy = Object.entries(fileDepsData).filter(([k,v]) => v.includes(d.id)).map(([k]) => k);
|
|
1107
|
+
const related = new Set([d.id, ...imports, ...importedBy]);
|
|
1108
|
+
node.selectAll('circle').attr('opacity', dd => related.has(dd.id) ? 1 : 0.1);
|
|
1109
|
+
node.selectAll('text').attr('opacity', dd => related.has(dd.id) ? 1 : 0.1);
|
|
1110
|
+
link.attr('stroke-opacity', dd => dd.source.id === d.id || dd.target.id === d.id ? 0.8 : 0.05);
|
|
1111
|
+
|
|
1112
|
+
const info = document.getElementById('depsInfo');
|
|
1113
|
+
info.classList.add('active');
|
|
1114
|
+
info.innerHTML = `<strong style="color:var(--green)">${{d.id}}</strong>
|
|
1115
|
+
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
1116
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Imports (${{imports.length}})</div>${{imports.map(f=>`<div style="font-size:12px;color:var(--text2);padding:2px 0;">${{f}}</div>`).join('')||'<span style="color:var(--text3)">none</span>'}}</div>
|
|
1117
|
+
<div><div style="font-size:11px;color:var(--text3);text-transform:uppercase;margin-bottom:6px;">Imported By (${{importedBy.length}})</div>${{importedBy.map(f=>`<div style="font-size:12px;color:var(--text2);padding:2px 0;">${{f}}</div>`).join('')||'<span style="color:var(--text3)">none</span>'}}</div></div>`;
|
|
1118
|
+
}});
|
|
1119
|
+
svg.on('click', () => {{
|
|
1120
|
+
node.selectAll('circle').attr('opacity', 1); node.selectAll('text').attr('opacity', 1);
|
|
1121
|
+
link.attr('stroke-opacity', 0.3);
|
|
1122
|
+
document.getElementById('depsInfo').classList.remove('active');
|
|
1123
|
+
}});
|
|
1124
|
+
|
|
1125
|
+
node.call(d3.drag()
|
|
1126
|
+
.on('start', (e, d) => {{ if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }})
|
|
1127
|
+
.on('drag', (e, d) => {{ d.fx = e.x; d.fy = e.y; }})
|
|
1128
|
+
.on('end', (e, d) => {{ if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }}));
|
|
1129
|
+
|
|
1130
|
+
const sim = d3.forceSimulation(nodes)
|
|
1131
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(60))
|
|
1132
|
+
.force('charge', d3.forceManyBody().strength(-100))
|
|
1133
|
+
.force('center', d3.forceCenter(W/2, H/2));
|
|
1134
|
+
|
|
1135
|
+
sim.on('tick', () => {{
|
|
1136
|
+
link.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
|
|
1137
|
+
node.attr('transform', d => `translate(${{d.x}},${{d.y}})`);
|
|
1138
|
+
}});
|
|
1139
|
+
}}
|
|
1140
|
+
|
|
1141
|
+
function searchDepsNode(q) {{
|
|
1142
|
+
// simple text-based highlight
|
|
1143
|
+
}}
|
|
1144
|
+
function resetDeps() {{
|
|
1145
|
+
const svg = d3.select('#depsContainer svg');
|
|
1146
|
+
if (svg.node()) svg.transition().call(d3.zoom().transform, d3.zoomIdentity);
|
|
1147
|
+
}}
|
|
1148
|
+
</script>
|
|
1149
|
+
</body>
|
|
1150
|
+
</html>"""
|
|
1151
|
+
|
|
1152
|
+
output_path.write_text(html, encoding="utf-8")
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def generate_tree_html(tree: dict, depth: int) -> str:
|
|
1156
|
+
"""Generate HTML for file tree."""
|
|
1157
|
+
html = ""
|
|
1158
|
+
for name, content in sorted(tree.items()):
|
|
1159
|
+
if isinstance(content, dict):
|
|
1160
|
+
html += f'<div class="tree-item folder" style="padding-left: {depth * 20}px;">📁 {name}/</div>'
|
|
1161
|
+
html += generate_tree_html(content, depth + 1)
|
|
1162
|
+
else:
|
|
1163
|
+
sym_count = len(content) if isinstance(content, list) else 0
|
|
1164
|
+
html += f'<div class="tree-item file" style="padding-left: {depth * 20}px;">📄 {name} <span style="color: #64748b;">({sym_count})</span></div>'
|
|
1165
|
+
return html
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def generate_symbols_html(symbols: list) -> str:
|
|
1169
|
+
"""Generate HTML for symbol cards."""
|
|
1170
|
+
html = ""
|
|
1171
|
+
for sym in symbols:
|
|
1172
|
+
params_str = ", ".join(sym.get("params", [])[:3])
|
|
1173
|
+
if len(sym.get("params", [])) > 3:
|
|
1174
|
+
params_str += "..."
|
|
1175
|
+
|
|
1176
|
+
fw = sym.get("framework", "")
|
|
1177
|
+
fw_html = f'<span class="badge badge-fw">{fw}</span>' if fw else ""
|
|
1178
|
+
doc = sym.get("doc", "")
|
|
1179
|
+
doc_html = '<span class="badge badge-doc">doc</span>' if doc else ""
|
|
1180
|
+
|
|
1181
|
+
# Escape the JSON for safe embedding in onclick
|
|
1182
|
+
sym_json = json.dumps(sym).replace("'", "'").replace('"', """)
|
|
1183
|
+
|
|
1184
|
+
html += f"""<div class="symbol-card" data-type="{sym.get('type', 'function')}" onclick='showSymbolDetail({json.dumps(sym)})'>
|
|
1185
|
+
<div class="symbol-name">{sym.get('name', 'unknown')}</div>
|
|
1186
|
+
<div class="symbol-meta"><span class="badge badge-type">{sym.get('type', 'function')}</span>{fw_html}{doc_html}</div>
|
|
1187
|
+
<div class="symbol-file">{sym.get('file', 'unknown')}:{sym.get('line', 0)}</div>
|
|
1188
|
+
{f'<div class="symbol-params">{params_str}</div>' if params_str else ''}
|
|
1189
|
+
</div>"""
|
|
1190
|
+
return html
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def generate_imports_html(imports: list) -> str:
|
|
1194
|
+
"""Generate HTML for import cards."""
|
|
1195
|
+
html = ""
|
|
1196
|
+
for imp in imports:
|
|
1197
|
+
module = imp.get("module", "unknown")
|
|
1198
|
+
imported = imp.get("imported", [])
|
|
1199
|
+
file = imp.get("file", "unknown")
|
|
1200
|
+
|
|
1201
|
+
is_external = not module.startswith(".")
|
|
1202
|
+
|
|
1203
|
+
html += f"""<div class="symbol-card" data-external="{is_external}">
|
|
1204
|
+
<div class="symbol-name">{module}</div>
|
|
1205
|
+
<span class="symbol-type">{'external' if is_external else 'internal'}</span>
|
|
1206
|
+
<div class="symbol-file">{file}</div>
|
|
1207
|
+
<div class="symbol-params">{', '.join(imported) if imported else 'default'}</div>
|
|
1208
|
+
</div>"""
|
|
1209
|
+
return html
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def generate_exports_html(exports: list) -> str:
|
|
1213
|
+
"""Generate HTML for export cards."""
|
|
1214
|
+
html = ""
|
|
1215
|
+
for exp in exports:
|
|
1216
|
+
name = exp.get("name", "unknown")
|
|
1217
|
+
exp_type = exp.get("type", "unknown")
|
|
1218
|
+
file = exp.get("file", "unknown")
|
|
1219
|
+
|
|
1220
|
+
html += f"""<div class="symbol-card">
|
|
1221
|
+
<div class="symbol-name">{name}</div>
|
|
1222
|
+
<span class="symbol-type">{exp_type}</span>
|
|
1223
|
+
<div class="symbol-file">{file}</div>
|
|
1224
|
+
</div>"""
|
|
1225
|
+
return html
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def generate_routes_html(routes: list) -> str:
|
|
1229
|
+
"""Generate HTML for API route cards."""
|
|
1230
|
+
html = ""
|
|
1231
|
+
for route in routes:
|
|
1232
|
+
method = route.get("method", "GET")
|
|
1233
|
+
path = route.get("path", "/")
|
|
1234
|
+
framework = route.get("framework", "unknown")
|
|
1235
|
+
file = route.get("file", "unknown")
|
|
1236
|
+
|
|
1237
|
+
method_colors = {"GET": "#10b981", "POST": "#3b82f6", "PUT": "#f59e0b", "DELETE": "#ef4444", "PATCH": "#8b5cf6"}
|
|
1238
|
+
color = method_colors.get(method, "#6b7280")
|
|
1239
|
+
|
|
1240
|
+
html += f"""<div class="symbol-card">
|
|
1241
|
+
<div class="symbol-name" style="display: flex; align-items: center; gap: 8px;">
|
|
1242
|
+
<span style="background: {color}; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600;">{method}</span>
|
|
1243
|
+
{path}
|
|
1244
|
+
</div>
|
|
1245
|
+
<span class="symbol-type">{framework}</span>
|
|
1246
|
+
<div class="symbol-file">{file}</div>
|
|
1247
|
+
</div>"""
|
|
1248
|
+
return html
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def generate_entities_html(entities: list) -> str:
|
|
1252
|
+
"""Generate HTML for entity cards."""
|
|
1253
|
+
html = ""
|
|
1254
|
+
for ent in entities:
|
|
1255
|
+
name = ent.get("name", "unknown")
|
|
1256
|
+
ent_type = ent.get("type", "unknown")
|
|
1257
|
+
fields = ent.get("fields", [])
|
|
1258
|
+
file = ent.get("file", "unknown")
|
|
1259
|
+
|
|
1260
|
+
html += f"""<div class="symbol-card">
|
|
1261
|
+
<div class="symbol-name">{name}</div>
|
|
1262
|
+
<span class="symbol-type">{ent_type}</span>
|
|
1263
|
+
<div class="symbol-file">{file}</div>
|
|
1264
|
+
<div class="symbol-params">{', '.join(fields[:5])}{'...' if len(fields) > 5 else ''}</div>
|
|
1265
|
+
</div>"""
|
|
1266
|
+
return html
|