codegraph-cli 2.1.0__py3-none-any.whl → 2.1.2__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.
- codegraph_cli/__init__.py +1 -1
- codegraph_cli/agents.py +59 -3
- codegraph_cli/chat_agent.py +58 -11
- codegraph_cli/cli.py +569 -54
- codegraph_cli/cli_chat.py +204 -94
- codegraph_cli/cli_diagnose.py +13 -2
- codegraph_cli/cli_docs.py +207 -0
- codegraph_cli/cli_explore.py +1053 -0
- codegraph_cli/cli_export.py +941 -0
- codegraph_cli/cli_groups.py +33 -0
- codegraph_cli/cli_health.py +316 -0
- codegraph_cli/cli_history.py +213 -0
- codegraph_cli/cli_onboard.py +380 -0
- codegraph_cli/cli_quickstart.py +256 -0
- codegraph_cli/cli_refactor.py +17 -3
- codegraph_cli/cli_setup.py +12 -12
- codegraph_cli/cli_suggestions.py +90 -0
- codegraph_cli/cli_test.py +17 -3
- codegraph_cli/cli_tui.py +210 -0
- codegraph_cli/cli_v2.py +24 -4
- codegraph_cli/cli_watch.py +158 -0
- codegraph_cli/cli_workflows.py +255 -0
- codegraph_cli/codegen_agent.py +15 -1
- codegraph_cli/config.py +18 -5
- codegraph_cli/context_manager.py +117 -15
- codegraph_cli/crew_agents.py +32 -8
- codegraph_cli/crew_chat.py +146 -13
- codegraph_cli/crew_tools.py +30 -2
- codegraph_cli/embeddings.py +95 -5
- codegraph_cli/llm.py +42 -55
- codegraph_cli/project_context.py +64 -1
- codegraph_cli/rag.py +282 -19
- codegraph_cli/storage.py +310 -14
- codegraph_cli/vector_store.py +110 -8
- {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/METADATA +75 -21
- codegraph_cli-2.1.2.dist-info/RECORD +55 -0
- codegraph_cli-2.1.2.dist-info/entry_points.txt +2 -0
- codegraph_cli-2.1.0.dist-info/RECORD +0 -43
- codegraph_cli-2.1.0.dist-info/entry_points.txt +0 -2
- {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/WHEEL +0 -0
- {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
"""Visual Code Explorer — browser-based UI for navigating indexed projects.
|
|
2
|
+
|
|
3
|
+
Launches a local web server serving a self-contained HTML page with:
|
|
4
|
+
- Directory tree sidebar with expandable folders
|
|
5
|
+
- File analysis: AI explanations, dependencies, syntax-highlighted code
|
|
6
|
+
- Mermaid diagrams (file-level and system-level)
|
|
7
|
+
- Export features (Mermaid, Excalidraw link, HTML)
|
|
8
|
+
|
|
9
|
+
Uses Starlette + Uvicorn (already installed) — zero extra dependencies.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import urllib.parse
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
explore_app = typer.Typer(
|
|
25
|
+
help="🌐 Visual code explorer in your browser.",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
rich_markup_mode="rich",
|
|
28
|
+
add_completion=False,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ===================================================================
|
|
33
|
+
# Backend helpers — build data from GraphStore
|
|
34
|
+
# ===================================================================
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _build_api_tree(store: Any) -> Dict:
|
|
38
|
+
"""Build JSON directory tree from indexed nodes."""
|
|
39
|
+
nodes_by_file = store.all_by_file()
|
|
40
|
+
root: Dict[str, Any] = {"name": "root", "type": "dir", "path": "", "children": []}
|
|
41
|
+
dir_cache: Dict[str, Dict] = {"": root}
|
|
42
|
+
|
|
43
|
+
all_paths: set[str] = set()
|
|
44
|
+
for fp in nodes_by_file:
|
|
45
|
+
parts = Path(fp).parts
|
|
46
|
+
for i in range(len(parts) - 1):
|
|
47
|
+
all_paths.add(str(Path(*parts[: i + 1])))
|
|
48
|
+
all_paths.add(fp)
|
|
49
|
+
|
|
50
|
+
# Create directory entries
|
|
51
|
+
for p in sorted(all_paths):
|
|
52
|
+
pp = Path(p)
|
|
53
|
+
parent_key = str(pp.parent) if str(pp.parent) != "." else ""
|
|
54
|
+
if p in nodes_by_file:
|
|
55
|
+
# It's a file
|
|
56
|
+
entry = {"name": pp.name, "type": "file", "path": p, "children": []}
|
|
57
|
+
else:
|
|
58
|
+
entry = {"name": pp.name, "type": "dir", "path": p, "children": []}
|
|
59
|
+
dir_cache[p] = entry
|
|
60
|
+
|
|
61
|
+
parent = dir_cache.get(parent_key, root)
|
|
62
|
+
parent["children"].append(entry)
|
|
63
|
+
|
|
64
|
+
return root
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _analyze_file(store: Any, file_path: str) -> Dict:
|
|
68
|
+
"""Deep analysis of a single file: code, deps, dependents, symbols."""
|
|
69
|
+
nodes_by_file = store.all_by_file()
|
|
70
|
+
file_nodes = nodes_by_file.get(file_path, [])
|
|
71
|
+
|
|
72
|
+
# Read source code from the first node or reconstruct
|
|
73
|
+
content_lines: list[str] = []
|
|
74
|
+
functions: list[dict] = []
|
|
75
|
+
classes: list[dict] = []
|
|
76
|
+
|
|
77
|
+
for node in sorted(file_nodes, key=lambda n: n.get("start_line", 0)):
|
|
78
|
+
ntype = node.get("node_type", "")
|
|
79
|
+
name = node.get("name", "")
|
|
80
|
+
qualname = node.get("qualname", name)
|
|
81
|
+
start = node.get("start_line", 0)
|
|
82
|
+
end = node.get("end_line", 0)
|
|
83
|
+
code = node.get("code", "")
|
|
84
|
+
|
|
85
|
+
if ntype == "function":
|
|
86
|
+
functions.append({"name": name, "qualname": qualname, "line": start, "end_line": end})
|
|
87
|
+
elif ntype == "class":
|
|
88
|
+
classes.append({"name": name, "qualname": qualname, "line": start, "end_line": end})
|
|
89
|
+
|
|
90
|
+
if code:
|
|
91
|
+
content_lines.append(code)
|
|
92
|
+
|
|
93
|
+
# Build full content — try reading the actual file first
|
|
94
|
+
content = ""
|
|
95
|
+
metadata = store.get_metadata()
|
|
96
|
+
source_root = metadata.get("source_path", "")
|
|
97
|
+
if source_root:
|
|
98
|
+
actual_file = Path(source_root) / file_path
|
|
99
|
+
if actual_file.exists():
|
|
100
|
+
try:
|
|
101
|
+
content = actual_file.read_text(encoding="utf-8", errors="replace")
|
|
102
|
+
except Exception:
|
|
103
|
+
content = "\n\n".join(content_lines)
|
|
104
|
+
if not content:
|
|
105
|
+
content = "\n\n".join(content_lines)
|
|
106
|
+
|
|
107
|
+
# Dependencies — symbols this file calls (outgoing edges)
|
|
108
|
+
deps: list[str] = []
|
|
109
|
+
dependents: list[str] = []
|
|
110
|
+
node_ids = {n.get("node_id") for n in file_nodes}
|
|
111
|
+
for nid in node_ids:
|
|
112
|
+
for edge in store.neighbors(nid):
|
|
113
|
+
dst = edge["dst"] if isinstance(edge, dict) else edge[1]
|
|
114
|
+
dst_node = store.get_node(dst)
|
|
115
|
+
if dst_node:
|
|
116
|
+
deps.append(dst_node["qualname"])
|
|
117
|
+
for edge in store.reverse_neighbors(nid):
|
|
118
|
+
src = edge["src"] if isinstance(edge, dict) else edge[0]
|
|
119
|
+
src_node = store.get_node(src)
|
|
120
|
+
if src_node:
|
|
121
|
+
dependents.append(src_node["qualname"])
|
|
122
|
+
|
|
123
|
+
deps = sorted(set(deps))
|
|
124
|
+
dependents = sorted(set(dependents))
|
|
125
|
+
|
|
126
|
+
# Generate Mermaid diagram for this file
|
|
127
|
+
mermaid = _generate_file_mermaid(store, file_path, file_nodes)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"name": Path(file_path).name,
|
|
131
|
+
"path": file_path,
|
|
132
|
+
"content": content,
|
|
133
|
+
"explanation": None, # filled by /api/explain
|
|
134
|
+
"deps": deps,
|
|
135
|
+
"dependents": dependents,
|
|
136
|
+
"mermaid": mermaid,
|
|
137
|
+
"functions": functions,
|
|
138
|
+
"classes": classes,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _generate_file_mermaid(store: Any, file_path: str, file_nodes: list) -> Optional[str]:
|
|
143
|
+
"""Generate a Mermaid diagram for a single file's internal structure."""
|
|
144
|
+
if not file_nodes:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
lines = ["graph TD"]
|
|
148
|
+
node_ids_in_file = set()
|
|
149
|
+
id_to_label: dict[str, str] = {}
|
|
150
|
+
|
|
151
|
+
for node in file_nodes:
|
|
152
|
+
nid = node.get("node_id", "")
|
|
153
|
+
name = node.get("name", "unknown")
|
|
154
|
+
ntype = node.get("node_type", "")
|
|
155
|
+
safe_id = nid.replace(".", "_").replace("/", "_").replace("-", "_").replace(":", "_")
|
|
156
|
+
node_ids_in_file.add(nid)
|
|
157
|
+
id_to_label[nid] = safe_id
|
|
158
|
+
|
|
159
|
+
if ntype == "class":
|
|
160
|
+
lines.append(f' {safe_id}["{name} (class)"]')
|
|
161
|
+
elif ntype == "function":
|
|
162
|
+
lines.append(f' {safe_id}("{name}()")')
|
|
163
|
+
else:
|
|
164
|
+
lines.append(f' {safe_id}["{name}"]')
|
|
165
|
+
|
|
166
|
+
# Add edges between nodes in this file
|
|
167
|
+
edge_count = 0
|
|
168
|
+
for nid in node_ids_in_file:
|
|
169
|
+
for edge in store.neighbors(nid):
|
|
170
|
+
dst = edge["dst"] if isinstance(edge, dict) else edge[1]
|
|
171
|
+
if dst in node_ids_in_file and edge_count < 50:
|
|
172
|
+
src_safe = id_to_label.get(nid, "")
|
|
173
|
+
dst_safe = id_to_label.get(dst, "")
|
|
174
|
+
if src_safe and dst_safe:
|
|
175
|
+
lines.append(f" {src_safe} --> {dst_safe}")
|
|
176
|
+
edge_count += 1
|
|
177
|
+
|
|
178
|
+
if len(lines) <= 1:
|
|
179
|
+
return None
|
|
180
|
+
return "\n".join(lines)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _generate_system_mermaid(store: Any) -> str:
|
|
184
|
+
"""Generate a system-level architecture Mermaid diagram (file-to-file deps)."""
|
|
185
|
+
nodes_by_file = store.all_by_file()
|
|
186
|
+
edges = store.get_edges()
|
|
187
|
+
|
|
188
|
+
# Map node_id -> file_path
|
|
189
|
+
nid_to_file: dict[str, str] = {}
|
|
190
|
+
for fp, nodes in nodes_by_file.items():
|
|
191
|
+
for n in nodes:
|
|
192
|
+
nid_to_file[n.get("node_id", "")] = fp
|
|
193
|
+
|
|
194
|
+
# Build file-level edges
|
|
195
|
+
file_edges: set[tuple[str, str]] = set()
|
|
196
|
+
for edge in edges:
|
|
197
|
+
src = edge["src"] if isinstance(edge, dict) else edge[0]
|
|
198
|
+
dst = edge["dst"] if isinstance(edge, dict) else edge[1]
|
|
199
|
+
src_file = nid_to_file.get(src, "")
|
|
200
|
+
dst_file = nid_to_file.get(dst, "")
|
|
201
|
+
if src_file and dst_file and src_file != dst_file:
|
|
202
|
+
file_edges.add((src_file, dst_file))
|
|
203
|
+
|
|
204
|
+
lines = ["graph LR"]
|
|
205
|
+
file_ids: dict[str, str] = {}
|
|
206
|
+
for fp in sorted(nodes_by_file.keys()):
|
|
207
|
+
safe = fp.replace("/", "_").replace(".", "_").replace("-", "_").replace(" ", "_")
|
|
208
|
+
file_ids[fp] = safe
|
|
209
|
+
short = Path(fp).name
|
|
210
|
+
lines.append(f' {safe}["{short}"]')
|
|
211
|
+
|
|
212
|
+
for src_f, dst_f in sorted(file_edges):
|
|
213
|
+
src_id = file_ids.get(src_f)
|
|
214
|
+
dst_id = file_ids.get(dst_f)
|
|
215
|
+
if src_id and dst_id:
|
|
216
|
+
lines.append(f" {src_id} --> {dst_id}")
|
|
217
|
+
|
|
218
|
+
return "\n".join(lines)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _explain_file(store: Any, file_path: str, llm_provider: str, llm_model: str, llm_api_key: str) -> Optional[str]:
|
|
222
|
+
"""Use LLM to generate a plain-English explanation of a file."""
|
|
223
|
+
try:
|
|
224
|
+
from .llm import LocalLLM
|
|
225
|
+
llm = LocalLLM(model=llm_model, provider=llm_provider, api_key=llm_api_key)
|
|
226
|
+
|
|
227
|
+
# Get file content
|
|
228
|
+
metadata = store.get_metadata()
|
|
229
|
+
source_root = metadata.get("source_path", "")
|
|
230
|
+
content = ""
|
|
231
|
+
if source_root:
|
|
232
|
+
actual = Path(source_root) / file_path
|
|
233
|
+
if actual.exists():
|
|
234
|
+
content = actual.read_text(encoding="utf-8", errors="replace")[:4000]
|
|
235
|
+
|
|
236
|
+
if not content:
|
|
237
|
+
nodes = store.all_by_file().get(file_path, [])
|
|
238
|
+
content = "\n".join(n.get("code", "")[:500] for n in nodes[:5])
|
|
239
|
+
|
|
240
|
+
if not content:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
prompt = (
|
|
244
|
+
f"Explain what this file does in 2-3 concise sentences. "
|
|
245
|
+
f"Focus on its purpose and key functionality.\n\n"
|
|
246
|
+
f"File: {file_path}\n```\n{content[:3000]}\n```"
|
|
247
|
+
)
|
|
248
|
+
return llm.explain(prompt)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.warning("LLM explanation failed: %s", e)
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ===================================================================
|
|
255
|
+
# HTML Template — complete self-contained SPA
|
|
256
|
+
# ===================================================================
|
|
257
|
+
|
|
258
|
+
HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
259
|
+
<html lang="en" class="dark">
|
|
260
|
+
<head>
|
|
261
|
+
<meta charset="UTF-8">
|
|
262
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
263
|
+
<title>CodeGraph Explorer</title>
|
|
264
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
265
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
266
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
267
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
|
268
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
269
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
|
|
270
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
|
271
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js"></script>
|
|
272
|
+
<script src="https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js"></script>
|
|
273
|
+
<script>
|
|
274
|
+
tailwind.config = {
|
|
275
|
+
darkMode: 'class',
|
|
276
|
+
theme: { extend: { colors: { 'cg-bg': '#0d1117', 'cg-sidebar': '#161b22', 'cg-card': '#1c2128', 'cg-border': '#30363d', 'cg-accent': '#58a6ff', 'cg-green': '#3fb950', 'cg-purple': '#bc8cff', 'cg-orange': '#d29922' } } }
|
|
277
|
+
}
|
|
278
|
+
</script>
|
|
279
|
+
<style>
|
|
280
|
+
html, body { margin:0; padding:0; height:100%; overflow:hidden; background:#0d1117; color:#c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; }
|
|
281
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
282
|
+
::-webkit-scrollbar-track { background: #161b22; }
|
|
283
|
+
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
|
|
284
|
+
::-webkit-scrollbar-thumb:hover { background: #484f58; }
|
|
285
|
+
.tree-item { transition: background 0.1s; }
|
|
286
|
+
.tree-item:hover { background: #1c2128; }
|
|
287
|
+
.tree-item.active { background: #1c2128; border-right: 2px solid #58a6ff; }
|
|
288
|
+
.fade-in { animation: fadeIn 0.2s ease-in; }
|
|
289
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
290
|
+
.slide-down { overflow: hidden; transition: max-height 0.25s ease-out; }
|
|
291
|
+
.toast { animation: toastIn 0.3s ease-out, toastOut 0.3s ease-in 1.7s; }
|
|
292
|
+
@keyframes toastIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
|
|
293
|
+
@keyframes toastOut { from { opacity:1; } to { opacity:0; } }
|
|
294
|
+
pre code.hljs { background: transparent !important; padding: 0 !important; }
|
|
295
|
+
.mermaid svg { max-width: 100%; }
|
|
296
|
+
</style>
|
|
297
|
+
</head>
|
|
298
|
+
<body x-data="explorer()" x-init="init()" @keydown.escape.window="selectedFile = null">
|
|
299
|
+
|
|
300
|
+
<!-- Toast -->
|
|
301
|
+
<div x-show="toast" x-transition x-text="toast" class="fixed bottom-6 right-6 z-50 bg-cg-green text-black px-4 py-2 rounded-lg font-medium shadow-lg toast"></div>
|
|
302
|
+
|
|
303
|
+
<!-- Export Modal -->
|
|
304
|
+
<div x-show="showExportModal" x-transition class="fixed inset-0 z-40 flex items-center justify-center bg-black/60" @click.self="showExportModal = false" @keydown.escape.window="showExportModal = false">
|
|
305
|
+
<div class="bg-cg-sidebar border border-cg-border rounded-xl shadow-2xl w-[480px] max-w-[95vw] p-6 fade-in">
|
|
306
|
+
<div class="flex items-center justify-between mb-5">
|
|
307
|
+
<h2 class="text-lg font-bold text-white">📄 Export to DOCX</h2>
|
|
308
|
+
<button @click="showExportModal = false" class="text-gray-500 hover:text-white text-xl">×</button>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div class="space-y-4">
|
|
312
|
+
<!-- Include Code -->
|
|
313
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
314
|
+
<input type="checkbox" x-model="exportOpts.includeCode" class="rounded border-cg-border bg-cg-bg text-cg-accent focus:ring-cg-accent">
|
|
315
|
+
<div>
|
|
316
|
+
<div class="text-sm text-gray-200">Include source code</div>
|
|
317
|
+
<div class="text-xs text-gray-500">Adds full file content (larger file)</div>
|
|
318
|
+
</div>
|
|
319
|
+
</label>
|
|
320
|
+
|
|
321
|
+
<!-- Include Diagram -->
|
|
322
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
323
|
+
<input type="checkbox" x-model="exportOpts.includeDiagram" class="rounded border-cg-border bg-cg-bg text-cg-accent focus:ring-cg-accent">
|
|
324
|
+
<div>
|
|
325
|
+
<div class="text-sm text-gray-200">Include architecture diagram</div>
|
|
326
|
+
<div class="text-xs text-gray-500">Embeds system diagram as image</div>
|
|
327
|
+
</div>
|
|
328
|
+
</label>
|
|
329
|
+
|
|
330
|
+
<!-- Enhanced (LLM) -->
|
|
331
|
+
<label class="flex items-center gap-3 cursor-pointer" :class="!exportStatus.llm_available && 'opacity-50'">
|
|
332
|
+
<input type="checkbox" x-model="exportOpts.enhanced" :disabled="!exportStatus.llm_available" class="rounded border-cg-border bg-cg-bg text-cg-purple focus:ring-cg-purple">
|
|
333
|
+
<div>
|
|
334
|
+
<div class="text-sm text-gray-200">AI-enhanced explanations</div>
|
|
335
|
+
<div class="text-xs text-gray-500" x-text="exportStatus.llm_available ? 'Using ' + (exportStatus.provider || '') + '/' + (exportStatus.model || '') : 'No LLM configured — run cg config setup'"></div>
|
|
336
|
+
</div>
|
|
337
|
+
</label>
|
|
338
|
+
|
|
339
|
+
<!-- Depth (only if enhanced) -->
|
|
340
|
+
<div x-show="exportOpts.enhanced && exportStatus.llm_available" x-transition class="pl-8">
|
|
341
|
+
<div class="text-sm text-gray-300 mb-2">Explanation depth:</div>
|
|
342
|
+
<div class="flex gap-2">
|
|
343
|
+
<template x-for="d in ['overview', 'modules', 'files']" :key="d">
|
|
344
|
+
<button @click="exportOpts.depth = d"
|
|
345
|
+
:class="exportOpts.depth === d ? 'bg-cg-accent text-black' : 'bg-cg-bg text-gray-400 border border-cg-border'"
|
|
346
|
+
class="px-3 py-1.5 text-xs rounded transition capitalize" x-text="d"></button>
|
|
347
|
+
</template>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="text-xs text-gray-600 mt-1" x-text="exportOpts.depth === 'overview' ? 'Project-level summary only' : exportOpts.depth === 'modules' ? 'Per-module summaries' : 'Per-file explanations (slowest)'"></div>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<!-- Actions -->
|
|
354
|
+
<div class="mt-6 flex items-center justify-between">
|
|
355
|
+
<button @click="showExportModal = false" class="px-4 py-2 text-sm text-gray-400 hover:text-white transition">Cancel</button>
|
|
356
|
+
<button @click="startExport()" :disabled="exporting" class="px-5 py-2 text-sm bg-cg-accent text-black font-medium rounded-lg hover:bg-blue-400 transition disabled:opacity-50 flex items-center gap-2">
|
|
357
|
+
<span x-show="exporting" class="animate-spin">⏳</span>
|
|
358
|
+
<span x-text="exporting ? 'Exporting...' : '📄 Export DOCX'"></span>
|
|
359
|
+
</button>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<div class="flex h-screen">
|
|
365
|
+
|
|
366
|
+
<!-- Sidebar -->
|
|
367
|
+
<div class="w-[300px] min-w-[300px] bg-cg-sidebar border-r border-cg-border flex flex-col">
|
|
368
|
+
<!-- Header -->
|
|
369
|
+
<div class="p-4 border-b border-cg-border">
|
|
370
|
+
<div class="flex items-center gap-2 mb-2">
|
|
371
|
+
<span class="text-xl">🧠</span>
|
|
372
|
+
<h1 class="text-sm font-bold text-white">CodeGraph Explorer</h1>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="text-xs text-gray-500" x-text="projectName"></div>
|
|
375
|
+
<div class="text-xs text-gray-600" x-text="projectStats"></div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<!-- Search -->
|
|
379
|
+
<div class="p-3 border-b border-cg-border">
|
|
380
|
+
<input type="text" x-model="treeFilter" placeholder="Filter files..."
|
|
381
|
+
class="w-full bg-cg-bg border border-cg-border rounded px-3 py-1.5 text-sm text-gray-300 placeholder-gray-600 focus:border-cg-accent focus:outline-none">
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<!-- File Tree -->
|
|
385
|
+
<div class="flex-1 overflow-y-auto py-2">
|
|
386
|
+
<template x-if="tree && tree.children">
|
|
387
|
+
<div>
|
|
388
|
+
<template x-for="child in filteredTree(tree.children)" :key="child.path">
|
|
389
|
+
<div x-data="{ open: false }">
|
|
390
|
+
<div @click="child.type === 'dir' ? open = !open : loadFile(child.path)"
|
|
391
|
+
:class="{'active': selectedFile === child.path}"
|
|
392
|
+
class="tree-item flex items-center gap-1.5 px-3 py-1 cursor-pointer text-sm select-none">
|
|
393
|
+
<span class="w-4 text-center text-xs text-gray-500" x-show="child.type === 'dir'" x-text="open ? '▾' : '▸'"></span>
|
|
394
|
+
<span class="w-4 text-center" x-show="child.type !== 'dir'"></span>
|
|
395
|
+
<span x-text="child.type === 'dir' ? '📁' : fileIcon(child.name)" class="text-sm"></span>
|
|
396
|
+
<span class="truncate" :class="child.type === 'dir' ? 'text-gray-300 font-medium' : 'text-gray-400'" x-text="child.name"></span>
|
|
397
|
+
</div>
|
|
398
|
+
<!-- Children -->
|
|
399
|
+
<div x-show="open && child.type === 'dir'" x-transition class="pl-4">
|
|
400
|
+
<template x-for="sub in filteredTree(child.children || [])" :key="sub.path">
|
|
401
|
+
<div x-data="{ subOpen: false }">
|
|
402
|
+
<div @click="sub.type === 'dir' ? subOpen = !subOpen : loadFile(sub.path)"
|
|
403
|
+
:class="{'active': selectedFile === sub.path}"
|
|
404
|
+
class="tree-item flex items-center gap-1.5 px-3 py-1 cursor-pointer text-sm select-none">
|
|
405
|
+
<span class="w-4 text-center text-xs text-gray-500" x-show="sub.type === 'dir'" x-text="subOpen ? '▾' : '▸'"></span>
|
|
406
|
+
<span class="w-4 text-center" x-show="sub.type !== 'dir'"></span>
|
|
407
|
+
<span x-text="sub.type === 'dir' ? '📁' : fileIcon(sub.name)" class="text-sm"></span>
|
|
408
|
+
<span class="truncate" :class="sub.type === 'dir' ? 'text-gray-300 font-medium' : 'text-gray-400'" x-text="sub.name"></span>
|
|
409
|
+
</div>
|
|
410
|
+
<div x-show="subOpen && sub.type === 'dir'" x-transition class="pl-4">
|
|
411
|
+
<template x-for="deep in filteredTree(sub.children || [])" :key="deep.path">
|
|
412
|
+
<div @click="deep.type === 'dir' ? null : loadFile(deep.path)"
|
|
413
|
+
:class="{'active': selectedFile === deep.path}"
|
|
414
|
+
class="tree-item flex items-center gap-1.5 px-3 py-1 cursor-pointer text-sm select-none">
|
|
415
|
+
<span class="w-4 text-center" x-show="deep.type === 'dir'">📁</span>
|
|
416
|
+
<span class="w-4 text-center" x-show="deep.type !== 'dir'"></span>
|
|
417
|
+
<span x-text="deep.type === 'dir' ? '📁' : fileIcon(deep.name)" class="text-sm"></span>
|
|
418
|
+
<span class="truncate text-gray-400" x-text="deep.name"></span>
|
|
419
|
+
</div>
|
|
420
|
+
</template>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</template>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</template>
|
|
427
|
+
</div>
|
|
428
|
+
</template>
|
|
429
|
+
<div x-show="loading" class="p-4 text-center text-gray-500 text-sm">Loading tree...</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<!-- System Diagram Button -->
|
|
433
|
+
<div class="p-3 border-t border-cg-border space-y-2">
|
|
434
|
+
<button @click="loadSystemDiagram()" class="w-full bg-cg-card hover:bg-gray-700 border border-cg-border text-sm text-gray-300 rounded py-2 transition">
|
|
435
|
+
🗺️ System Architecture
|
|
436
|
+
</button>
|
|
437
|
+
<button @click="showExportModal = true; loadExportStatus()" class="w-full bg-blue-900/30 hover:bg-blue-900/50 border border-blue-800/40 text-sm text-cg-accent rounded py-2 transition">
|
|
438
|
+
📄 Export to DOCX
|
|
439
|
+
</button>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<!-- Main Panel -->
|
|
444
|
+
<div class="flex-1 flex flex-col overflow-hidden">
|
|
445
|
+
|
|
446
|
+
<!-- Top Bar -->
|
|
447
|
+
<div class="flex items-center justify-between px-5 py-3 border-b border-cg-border bg-cg-sidebar">
|
|
448
|
+
<div class="flex items-center gap-3">
|
|
449
|
+
<span class="text-sm text-gray-400" x-text="selectedFile || 'Select a file to explore'"></span>
|
|
450
|
+
<span x-show="fileLoading" class="text-xs text-gray-500">⏳ Loading...</span>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="flex items-center gap-2" x-show="fileData">
|
|
453
|
+
<button @click="triggerExplain()" class="px-3 py-1 text-xs bg-purple-900/40 text-cg-purple border border-purple-800/50 rounded hover:bg-purple-900/60 transition" title="AI Explain">
|
|
454
|
+
🤖 Explain
|
|
455
|
+
</button>
|
|
456
|
+
<button @click="exportMermaid()" x-show="fileData && fileData.mermaid" class="px-3 py-1 text-xs bg-cg-card text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition">
|
|
457
|
+
📋 Copy Mermaid
|
|
458
|
+
</button>
|
|
459
|
+
<button @click="openMermaidLive(fileData?.mermaid)" x-show="fileData && fileData.mermaid" class="px-3 py-1 text-xs bg-cg-card text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Open in Mermaid Live Editor">
|
|
460
|
+
🔗 Mermaid Live
|
|
461
|
+
</button>
|
|
462
|
+
<button @click="exportDiagramSVG('fileMermaid', fileData?.name || 'diagram')" x-show="fileData && fileData.mermaid" class="px-3 py-1 text-xs bg-cg-card text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Download as SVG">
|
|
463
|
+
🖼️ SVG
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Content -->
|
|
469
|
+
<div class="flex-1 overflow-y-auto p-6 space-y-6">
|
|
470
|
+
|
|
471
|
+
<!-- Welcome -->
|
|
472
|
+
<div x-show="!selectedFile && !systemDiagram" class="flex flex-col items-center justify-center h-full text-center fade-in">
|
|
473
|
+
<div class="text-6xl mb-4">🧠</div>
|
|
474
|
+
<h2 class="text-2xl font-bold text-white mb-2">CodeGraph Explorer</h2>
|
|
475
|
+
<p class="text-gray-500 max-w-md">Select a file from the sidebar to explore its structure, dependencies, and AI-powered explanations.</p>
|
|
476
|
+
<div class="mt-6 flex gap-3">
|
|
477
|
+
<div class="px-4 py-2 bg-cg-card rounded border border-cg-border text-sm text-gray-400">📁 Browse files in sidebar</div>
|
|
478
|
+
<div class="px-4 py-2 bg-cg-card rounded border border-cg-border text-sm text-gray-400">🗺️ View system architecture</div>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="mt-8 text-xs text-gray-600">
|
|
481
|
+
<span class="text-gray-500">Shortcuts:</span> Esc to deselect
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<!-- System Diagram View -->
|
|
486
|
+
<div x-show="systemDiagram && !selectedFile" class="fade-in">
|
|
487
|
+
<div class="bg-cg-card rounded-lg border border-cg-border p-6">
|
|
488
|
+
<div class="flex items-center justify-between mb-4">
|
|
489
|
+
<h3 class="text-lg font-bold text-white">🗺️ System Architecture</h3>
|
|
490
|
+
<div class="flex items-center gap-2">
|
|
491
|
+
<button @click="copyToClipboard(systemDiagram); showToast('Mermaid copied!')" class="px-3 py-1 text-xs bg-cg-bg text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Copy Mermaid code">📋 Mermaid</button>
|
|
492
|
+
<button @click="exportDiagramSVG('systemMermaid', 'system-architecture')" class="px-3 py-1 text-xs bg-cg-bg text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Download as SVG">🖼️ SVG</button>
|
|
493
|
+
<button @click="openMermaidLive(systemDiagram)" class="px-3 py-1 text-xs bg-cg-bg text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Open in Mermaid Live Editor">🔗 Mermaid Live</button>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
<div class="bg-cg-bg rounded p-4 overflow-x-auto">
|
|
497
|
+
<div class="mermaid" x-ref="systemMermaid"></div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<!-- File View -->
|
|
503
|
+
<div x-show="selectedFile && fileData" class="space-y-6 fade-in">
|
|
504
|
+
|
|
505
|
+
<!-- File Header -->
|
|
506
|
+
<div class="flex items-center gap-3">
|
|
507
|
+
<span class="text-2xl" x-text="fileIcon(fileData?.name || '')"></span>
|
|
508
|
+
<div>
|
|
509
|
+
<h2 class="text-xl font-bold text-white" x-text="fileData?.name"></h2>
|
|
510
|
+
<span class="text-xs text-gray-500" x-text="fileData?.path"></span>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<!-- AI Explanation -->
|
|
515
|
+
<div x-show="fileData?.explanation" class="bg-purple-900/20 border border-purple-800/40 rounded-lg p-4 fade-in">
|
|
516
|
+
<div class="flex items-center gap-2 mb-2">
|
|
517
|
+
<span class="text-sm">🤖</span>
|
|
518
|
+
<span class="text-sm font-medium text-cg-purple">AI Explanation</span>
|
|
519
|
+
</div>
|
|
520
|
+
<p class="text-sm text-gray-300 leading-relaxed" x-text="fileData?.explanation"></p>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<!-- Symbols -->
|
|
524
|
+
<div x-show="(fileData?.functions?.length || 0) + (fileData?.classes?.length || 0) > 0" class="bg-cg-card rounded-lg border border-cg-border p-4">
|
|
525
|
+
<h3 class="text-sm font-semibold text-white mb-3">📌 Symbols</h3>
|
|
526
|
+
<div class="flex flex-wrap gap-2">
|
|
527
|
+
<template x-for="fn in (fileData?.functions || [])" :key="fn.name">
|
|
528
|
+
<span class="inline-flex items-center gap-1 px-2 py-1 bg-blue-900/30 text-blue-400 text-xs rounded border border-blue-800/40">
|
|
529
|
+
<span>ƒ</span> <span x-text="fn.name"></span>
|
|
530
|
+
<span class="text-blue-600" x-text="'L' + fn.line"></span>
|
|
531
|
+
</span>
|
|
532
|
+
</template>
|
|
533
|
+
<template x-for="cls in (fileData?.classes || [])" :key="cls.name">
|
|
534
|
+
<span class="inline-flex items-center gap-1 px-2 py-1 bg-orange-900/30 text-cg-orange text-xs rounded border border-orange-800/40">
|
|
535
|
+
<span>◆</span> <span x-text="cls.name"></span>
|
|
536
|
+
<span class="text-orange-600" x-text="'L' + cls.line"></span>
|
|
537
|
+
</span>
|
|
538
|
+
</template>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
<!-- Dependencies Grid -->
|
|
543
|
+
<div class="grid grid-cols-2 gap-4" x-show="(fileData?.deps?.length || 0) + (fileData?.dependents?.length || 0) > 0">
|
|
544
|
+
<div class="bg-cg-card rounded-lg border border-cg-border p-4">
|
|
545
|
+
<h3 class="text-sm font-semibold text-cg-green mb-3">⬆️ Dependencies <span class="text-gray-600 font-normal" x-text="'(' + (fileData?.deps?.length || 0) + ')'"></span></h3>
|
|
546
|
+
<div class="space-y-1 max-h-40 overflow-y-auto">
|
|
547
|
+
<template x-for="dep in (fileData?.deps || [])" :key="dep">
|
|
548
|
+
<div class="text-xs text-gray-400 font-mono truncate" x-text="dep"></div>
|
|
549
|
+
</template>
|
|
550
|
+
<div x-show="!(fileData?.deps?.length)" class="text-xs text-gray-600 italic">None</div>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="bg-cg-card rounded-lg border border-cg-border p-4">
|
|
554
|
+
<h3 class="text-sm font-semibold text-cg-accent mb-3">⬇️ Dependents <span class="text-gray-600 font-normal" x-text="'(' + (fileData?.dependents?.length || 0) + ')'"></span></h3>
|
|
555
|
+
<div class="space-y-1 max-h-40 overflow-y-auto">
|
|
556
|
+
<template x-for="d in (fileData?.dependents || [])" :key="d">
|
|
557
|
+
<div class="text-xs text-gray-400 font-mono truncate" x-text="d"></div>
|
|
558
|
+
</template>
|
|
559
|
+
<div x-show="!(fileData?.dependents?.length)" class="text-xs text-gray-600 italic">None</div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<!-- Source Code (collapsible) -->
|
|
565
|
+
<div class="bg-cg-card rounded-lg border border-cg-border overflow-hidden">
|
|
566
|
+
<div class="flex items-center justify-between px-4 py-2 border-b border-cg-border cursor-pointer select-none" @click="codeOpen = !codeOpen">
|
|
567
|
+
<div class="flex items-center gap-2">
|
|
568
|
+
<span class="text-xs text-gray-500 transition-transform duration-200" :class="codeOpen && 'rotate-90'" style="display:inline-block">▶</span>
|
|
569
|
+
<h3 class="text-sm font-semibold text-white">📝 Source Code</h3>
|
|
570
|
+
</div>
|
|
571
|
+
<div class="flex items-center gap-1" @click.stop>
|
|
572
|
+
<button @click="copyToClipboard(fileData?.content || ''); showToast('Code copied!')" class="px-2 py-1 text-xs text-gray-400 hover:text-white transition" title="Copy source code">📋 Copy</button>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
<div x-show="codeOpen" x-transition:enter="transition ease-out duration-200" x-transition:leave="transition ease-in duration-150" class="overflow-x-auto">
|
|
576
|
+
<pre class="p-4 text-sm leading-relaxed" style="max-height:600px; overflow-y:auto"><code x-ref="codeBlock" class="hljs"></code></pre>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<!-- Mermaid Diagram -->
|
|
581
|
+
<div x-show="fileData?.mermaid" class="bg-cg-card rounded-lg border border-cg-border p-4">
|
|
582
|
+
<div class="flex items-center justify-between mb-3">
|
|
583
|
+
<h3 class="text-sm font-semibold text-white">📊 File Diagram</h3>
|
|
584
|
+
<div class="flex items-center gap-2">
|
|
585
|
+
<button @click="exportMermaid()" class="px-2 py-1 text-xs bg-cg-bg text-gray-400 border border-cg-border rounded hover:text-white hover:bg-gray-700 transition" title="Copy Mermaid code">📋 Mermaid</button>
|
|
586
|
+
<button @click="exportDiagramSVG('fileMermaid', fileData?.name || 'diagram')" class="px-2 py-1 text-xs bg-cg-bg text-gray-400 border border-cg-border rounded hover:text-white hover:bg-gray-700 transition" title="Download as SVG">🖼️ SVG</button>
|
|
587
|
+
<button @click="openMermaidLive(fileData?.mermaid)" class="px-2 py-1 text-xs bg-cg-bg text-gray-400 border border-cg-border rounded hover:text-white hover:bg-gray-700 transition" title="Open in Mermaid Live Editor">🔗 Mermaid Live</button>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="bg-cg-bg rounded p-4 overflow-x-auto">
|
|
591
|
+
<div class="mermaid" x-ref="fileMermaid"></div>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<script>
|
|
601
|
+
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' });
|
|
602
|
+
|
|
603
|
+
function explorer() {
|
|
604
|
+
return {
|
|
605
|
+
tree: null,
|
|
606
|
+
selectedFile: null,
|
|
607
|
+
fileData: null,
|
|
608
|
+
fileLoading: false,
|
|
609
|
+
loading: true,
|
|
610
|
+
toast: null,
|
|
611
|
+
treeFilter: '',
|
|
612
|
+
systemDiagram: null,
|
|
613
|
+
projectName: '',
|
|
614
|
+
projectStats: '',
|
|
615
|
+
codeOpen: true,
|
|
616
|
+
showExportModal: false,
|
|
617
|
+
exporting: false,
|
|
618
|
+
exportStatus: { llm_available: false, provider: null, model: null },
|
|
619
|
+
exportOpts: { includeCode: false, includeDiagram: true, enhanced: false, depth: 'modules' },
|
|
620
|
+
|
|
621
|
+
async init() {
|
|
622
|
+
try {
|
|
623
|
+
const res = await fetch('/api/tree');
|
|
624
|
+
const data = await res.json();
|
|
625
|
+
this.tree = data.tree;
|
|
626
|
+
this.projectName = data.project || '';
|
|
627
|
+
const stats = data.stats || {};
|
|
628
|
+
this.projectStats = `${stats.files || 0} files · ${stats.functions || 0} functions · ${stats.classes || 0} classes`;
|
|
629
|
+
} catch(e) {
|
|
630
|
+
console.error('Failed to load tree:', e);
|
|
631
|
+
}
|
|
632
|
+
this.loading = false;
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
filteredTree(children) {
|
|
636
|
+
if (!this.treeFilter) return children || [];
|
|
637
|
+
const q = this.treeFilter.toLowerCase();
|
|
638
|
+
return (children || []).filter(c => {
|
|
639
|
+
if (c.name.toLowerCase().includes(q)) return true;
|
|
640
|
+
if (c.type === 'dir' && c.children) return this.filteredTree(c.children).length > 0;
|
|
641
|
+
return false;
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
async loadFile(path) {
|
|
646
|
+
this.selectedFile = path;
|
|
647
|
+
this.systemDiagram = null;
|
|
648
|
+
this.fileLoading = true;
|
|
649
|
+
this.fileData = null;
|
|
650
|
+
this.codeOpen = true;
|
|
651
|
+
try {
|
|
652
|
+
const res = await fetch('/api/file/' + encodeURIComponent(path));
|
|
653
|
+
this.fileData = await res.json();
|
|
654
|
+
this.$nextTick(() => {
|
|
655
|
+
this.highlightCode();
|
|
656
|
+
this.renderFileMermaid();
|
|
657
|
+
});
|
|
658
|
+
} catch(e) {
|
|
659
|
+
console.error('Failed to load file:', e);
|
|
660
|
+
this.fileData = { name: path.split('/').pop(), path: path, content: 'Error loading file', deps: [], dependents: [], functions: [], classes: [] };
|
|
661
|
+
}
|
|
662
|
+
this.fileLoading = false;
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
async loadSystemDiagram() {
|
|
666
|
+
this.selectedFile = null;
|
|
667
|
+
this.fileData = null;
|
|
668
|
+
try {
|
|
669
|
+
const res = await fetch('/api/diagram');
|
|
670
|
+
const data = await res.json();
|
|
671
|
+
this.systemDiagram = data.mermaid;
|
|
672
|
+
this.$nextTick(() => this.renderSystemMermaid());
|
|
673
|
+
} catch(e) {
|
|
674
|
+
console.error('Failed to load diagram:', e);
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
async triggerExplain() {
|
|
679
|
+
if (!this.selectedFile) return;
|
|
680
|
+
try {
|
|
681
|
+
const res = await fetch('/api/explain/' + encodeURIComponent(this.selectedFile));
|
|
682
|
+
const data = await res.json();
|
|
683
|
+
if (data.explanation && this.fileData) {
|
|
684
|
+
this.fileData.explanation = data.explanation;
|
|
685
|
+
} else if (data.error) {
|
|
686
|
+
this.showToast('LLM not available');
|
|
687
|
+
}
|
|
688
|
+
} catch(e) { this.showToast('Explain failed'); }
|
|
689
|
+
},
|
|
690
|
+
|
|
691
|
+
highlightCode() {
|
|
692
|
+
if (!this.fileData?.content || !this.$refs.codeBlock) return;
|
|
693
|
+
const ext = (this.fileData.name || '').split('.').pop();
|
|
694
|
+
const langMap = { py: 'python', js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript' };
|
|
695
|
+
const lang = langMap[ext] || ext || 'plaintext';
|
|
696
|
+
this.$refs.codeBlock.className = 'hljs language-' + lang;
|
|
697
|
+
this.$refs.codeBlock.textContent = this.fileData.content;
|
|
698
|
+
hljs.highlightElement(this.$refs.codeBlock);
|
|
699
|
+
},
|
|
700
|
+
|
|
701
|
+
async renderFileMermaid() {
|
|
702
|
+
if (!this.fileData?.mermaid || !this.$refs.fileMermaid) return;
|
|
703
|
+
try {
|
|
704
|
+
const id = 'fm-' + Date.now();
|
|
705
|
+
const { svg } = await mermaid.render(id, this.fileData.mermaid);
|
|
706
|
+
this.$refs.fileMermaid.innerHTML = svg;
|
|
707
|
+
} catch(e) { this.$refs.fileMermaid.innerHTML = '<span class="text-red-400 text-xs">Diagram render error</span>'; }
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
async renderSystemMermaid() {
|
|
711
|
+
if (!this.systemDiagram || !this.$refs.systemMermaid) return;
|
|
712
|
+
try {
|
|
713
|
+
const id = 'sm-' + Date.now();
|
|
714
|
+
const { svg } = await mermaid.render(id, this.systemDiagram);
|
|
715
|
+
this.$refs.systemMermaid.innerHTML = svg;
|
|
716
|
+
} catch(e) { this.$refs.systemMermaid.innerHTML = '<span class="text-red-400 text-xs">Diagram render error</span>'; }
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
fileIcon(name) {
|
|
720
|
+
const ext = (name || '').split('.').pop();
|
|
721
|
+
const icons = { py: '🐍', js: '📜', ts: '🔷', jsx: '⚛️', tsx: '⚛️', json: '📋', md: '📝', yml: '⚙️', yaml: '⚙️', toml: '⚙️', html: '🌐', css: '🎨', go: '🔹', rs: '🦀', java: '☕', rb: '💎' };
|
|
722
|
+
return icons[ext] || '📄';
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
exportMermaid() {
|
|
726
|
+
if (this.fileData?.mermaid) {
|
|
727
|
+
this.copyToClipboard(this.fileData.mermaid);
|
|
728
|
+
this.showToast('Mermaid copied!');
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
|
|
732
|
+
exportDiagramSVG(refName, filename) {
|
|
733
|
+
const el = this.$refs[refName];
|
|
734
|
+
if (!el) return;
|
|
735
|
+
const svg = el.querySelector('svg');
|
|
736
|
+
if (!svg) { this.showToast('No diagram rendered'); return; }
|
|
737
|
+
const svgData = new XMLSerializer().serializeToString(svg);
|
|
738
|
+
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
|
739
|
+
const a = document.createElement('a');
|
|
740
|
+
a.href = URL.createObjectURL(blob);
|
|
741
|
+
a.download = (filename || 'diagram') + '.svg';
|
|
742
|
+
a.click();
|
|
743
|
+
URL.revokeObjectURL(a.href);
|
|
744
|
+
this.showToast('SVG downloaded!');
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
openMermaidLive(mermaidCode) {
|
|
748
|
+
if (!mermaidCode) return;
|
|
749
|
+
try {
|
|
750
|
+
const state = JSON.stringify({
|
|
751
|
+
code: mermaidCode,
|
|
752
|
+
mermaid: JSON.stringify({ theme: 'dark' }),
|
|
753
|
+
autoSync: true,
|
|
754
|
+
updateDiagram: true
|
|
755
|
+
});
|
|
756
|
+
const data = new TextEncoder().encode(state);
|
|
757
|
+
const compressed = pako.deflate(data, { level: 9 });
|
|
758
|
+
let b64 = '';
|
|
759
|
+
for (let i = 0; i < compressed.length; i++) {
|
|
760
|
+
b64 += String.fromCharCode(compressed[i]);
|
|
761
|
+
}
|
|
762
|
+
const encoded = btoa(b64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
763
|
+
window.open('https://mermaid.live/edit#pako:' + encoded, '_blank');
|
|
764
|
+
} catch(e) {
|
|
765
|
+
console.error('Mermaid Live encoding failed:', e);
|
|
766
|
+
this.copyToClipboard(mermaidCode);
|
|
767
|
+
this.showToast('Copied — paste at mermaid.live/edit');
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
async loadExportStatus() {
|
|
772
|
+
try {
|
|
773
|
+
const res = await fetch('/api/export/status');
|
|
774
|
+
this.exportStatus = await res.json();
|
|
775
|
+
} catch(e) { console.error('Export status failed:', e); }
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
async startExport() {
|
|
779
|
+
this.exporting = true;
|
|
780
|
+
try {
|
|
781
|
+
const params = new URLSearchParams({
|
|
782
|
+
code: this.exportOpts.includeCode,
|
|
783
|
+
enhanced: this.exportOpts.enhanced,
|
|
784
|
+
depth: this.exportOpts.depth,
|
|
785
|
+
no_diagram: !this.exportOpts.includeDiagram,
|
|
786
|
+
});
|
|
787
|
+
const res = await fetch('/api/export?' + params.toString());
|
|
788
|
+
if (!res.ok) {
|
|
789
|
+
const err = await res.json();
|
|
790
|
+
this.showToast('Export failed: ' + (err.error || 'unknown'));
|
|
791
|
+
this.exporting = false;
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const blob = await res.blob();
|
|
795
|
+
const url = URL.createObjectURL(blob);
|
|
796
|
+
const a = document.createElement('a');
|
|
797
|
+
a.href = url;
|
|
798
|
+
const cd = res.headers.get('Content-Disposition') || '';
|
|
799
|
+
const filenameMatch = cd.match(/filename="(.+?)"/);
|
|
800
|
+
a.download = filenameMatch ? filenameMatch[1] : 'project-docs.docx';
|
|
801
|
+
a.click();
|
|
802
|
+
URL.revokeObjectURL(url);
|
|
803
|
+
this.showToast('DOCX downloaded!');
|
|
804
|
+
this.showExportModal = false;
|
|
805
|
+
} catch(e) {
|
|
806
|
+
console.error('Export failed:', e);
|
|
807
|
+
this.showToast('Export failed');
|
|
808
|
+
}
|
|
809
|
+
this.exporting = false;
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
copyToClipboard(text) { navigator.clipboard.writeText(text).catch(() => {}); },
|
|
813
|
+
|
|
814
|
+
showToast(msg) {
|
|
815
|
+
this.toast = msg;
|
|
816
|
+
setTimeout(() => this.toast = null, 2000);
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
</script>
|
|
821
|
+
</body>
|
|
822
|
+
</html>"""
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
# ===================================================================
|
|
826
|
+
# Starlette app + Uvicorn server
|
|
827
|
+
# ===================================================================
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _create_server(store: Any, llm_provider: str, llm_model: str, llm_api_key: str):
|
|
831
|
+
"""Create the Starlette ASGI application."""
|
|
832
|
+
try:
|
|
833
|
+
from starlette.applications import Starlette
|
|
834
|
+
from starlette.responses import HTMLResponse, JSONResponse
|
|
835
|
+
from starlette.routing import Route
|
|
836
|
+
except ImportError:
|
|
837
|
+
raise ImportError(
|
|
838
|
+
"The 'explore' feature requires starlette and uvicorn.\n"
|
|
839
|
+
"Install with: pip install codegraph-cli[explore]"
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
async def homepage(request):
|
|
843
|
+
return HTMLResponse(HTML_TEMPLATE)
|
|
844
|
+
|
|
845
|
+
async def api_tree(request):
|
|
846
|
+
try:
|
|
847
|
+
tree = _build_api_tree(store)
|
|
848
|
+
nodes = store.get_nodes()
|
|
849
|
+
files = set(n["file_path"] for n in nodes)
|
|
850
|
+
functions = sum(1 for n in nodes if n["node_type"] == "function")
|
|
851
|
+
classes = sum(1 for n in nodes if n["node_type"] == "class")
|
|
852
|
+
meta = store.get_metadata()
|
|
853
|
+
return JSONResponse({
|
|
854
|
+
"tree": tree,
|
|
855
|
+
"project": meta.get("project_name", ""),
|
|
856
|
+
"stats": {"files": len(files), "functions": functions, "classes": classes, "nodes": len(nodes)},
|
|
857
|
+
})
|
|
858
|
+
except Exception as e:
|
|
859
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
860
|
+
|
|
861
|
+
async def api_file(request):
|
|
862
|
+
file_path = urllib.parse.unquote(request.path_params["path"])
|
|
863
|
+
try:
|
|
864
|
+
data = _analyze_file(store, file_path)
|
|
865
|
+
return JSONResponse(data)
|
|
866
|
+
except Exception as e:
|
|
867
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
868
|
+
|
|
869
|
+
async def api_diagram(request):
|
|
870
|
+
try:
|
|
871
|
+
mermaid = _generate_system_mermaid(store)
|
|
872
|
+
return JSONResponse({"mermaid": mermaid})
|
|
873
|
+
except Exception as e:
|
|
874
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
875
|
+
|
|
876
|
+
async def api_explain(request):
|
|
877
|
+
file_path = urllib.parse.unquote(request.path_params["path"])
|
|
878
|
+
try:
|
|
879
|
+
explanation = _explain_file(store, file_path, llm_provider, llm_model, llm_api_key)
|
|
880
|
+
if explanation:
|
|
881
|
+
return JSONResponse({"explanation": explanation})
|
|
882
|
+
return JSONResponse({"error": "LLM not configured or unavailable"}, status_code=200)
|
|
883
|
+
except Exception as e:
|
|
884
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
885
|
+
|
|
886
|
+
async def api_export(request):
|
|
887
|
+
"""Generate and return DOCX as a file download."""
|
|
888
|
+
try:
|
|
889
|
+
import tempfile
|
|
890
|
+
from .cli_export import generate_basic_docx, generate_enhanced_docx
|
|
891
|
+
from starlette.responses import Response
|
|
892
|
+
|
|
893
|
+
params = request.query_params
|
|
894
|
+
include_code = params.get("code", "false").lower() == "true"
|
|
895
|
+
enhanced = params.get("enhanced", "false").lower() == "true"
|
|
896
|
+
depth = params.get("depth", "modules")
|
|
897
|
+
no_diagram = params.get("no_diagram", "false").lower() == "true"
|
|
898
|
+
|
|
899
|
+
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
|
|
900
|
+
tmp_path = Path(tmp.name)
|
|
901
|
+
|
|
902
|
+
if enhanced and llm_provider:
|
|
903
|
+
generate_enhanced_docx(
|
|
904
|
+
store=store,
|
|
905
|
+
output_path=tmp_path,
|
|
906
|
+
include_code=include_code,
|
|
907
|
+
include_diagram=not no_diagram,
|
|
908
|
+
explanation_depth=depth,
|
|
909
|
+
llm_provider=llm_provider,
|
|
910
|
+
llm_model=llm_model,
|
|
911
|
+
llm_api_key=llm_api_key,
|
|
912
|
+
)
|
|
913
|
+
else:
|
|
914
|
+
generate_basic_docx(
|
|
915
|
+
store=store,
|
|
916
|
+
output_path=tmp_path,
|
|
917
|
+
include_code=include_code,
|
|
918
|
+
include_diagram=not no_diagram,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
data = tmp_path.read_bytes()
|
|
922
|
+
tmp_path.unlink(missing_ok=True)
|
|
923
|
+
|
|
924
|
+
meta = store.get_metadata()
|
|
925
|
+
filename = f"{meta.get('project_name', 'project')}-docs.docx"
|
|
926
|
+
|
|
927
|
+
return Response(
|
|
928
|
+
content=data,
|
|
929
|
+
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
930
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
931
|
+
)
|
|
932
|
+
except Exception as e:
|
|
933
|
+
logger.exception("Export failed")
|
|
934
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
935
|
+
|
|
936
|
+
async def api_export_status(request):
|
|
937
|
+
"""Return export capabilities (whether LLM is available etc)."""
|
|
938
|
+
return JSONResponse({
|
|
939
|
+
"llm_available": bool(llm_provider and llm_model),
|
|
940
|
+
"provider": llm_provider or None,
|
|
941
|
+
"model": llm_model or None,
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
app = Starlette(routes=[
|
|
945
|
+
Route("/", homepage),
|
|
946
|
+
Route("/api/tree", api_tree),
|
|
947
|
+
Route("/api/file/{path:path}", api_file),
|
|
948
|
+
Route("/api/diagram", api_diagram),
|
|
949
|
+
Route("/api/explain/{path:path}", api_explain),
|
|
950
|
+
Route("/api/export", api_export),
|
|
951
|
+
Route("/api/export/status", api_export_status),
|
|
952
|
+
])
|
|
953
|
+
return app
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
# ===================================================================
|
|
957
|
+
# Typer command
|
|
958
|
+
# ===================================================================
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
@explore_app.command("open")
|
|
962
|
+
def explore_open(
|
|
963
|
+
port: int = typer.Option(8421, "--port", "-p", help="Port for the local web server."),
|
|
964
|
+
):
|
|
965
|
+
"""🌐 Open the visual code explorer in your browser.
|
|
966
|
+
|
|
967
|
+
Launches a local web server and opens a modern UI for navigating
|
|
968
|
+
your indexed codebase with syntax highlighting, dependency graphs,
|
|
969
|
+
AI explanations, and Mermaid diagrams.
|
|
970
|
+
|
|
971
|
+
Example:
|
|
972
|
+
cg explore open
|
|
973
|
+
cg explore open --port 9000
|
|
974
|
+
"""
|
|
975
|
+
import socket
|
|
976
|
+
import threading
|
|
977
|
+
import time
|
|
978
|
+
import webbrowser
|
|
979
|
+
|
|
980
|
+
from rich.console import Console
|
|
981
|
+
|
|
982
|
+
from .storage import GraphStore, ProjectManager
|
|
983
|
+
from . import config as cfg
|
|
984
|
+
|
|
985
|
+
console = Console()
|
|
986
|
+
|
|
987
|
+
pm = ProjectManager()
|
|
988
|
+
project = pm.get_current_project()
|
|
989
|
+
if not project:
|
|
990
|
+
console.print("[red]No project loaded.[/red] Run [cyan]cg project index <path>[/cyan] first.")
|
|
991
|
+
raise typer.Exit(code=1)
|
|
992
|
+
|
|
993
|
+
project_dir = pm.project_dir(project)
|
|
994
|
+
if not project_dir.exists():
|
|
995
|
+
console.print(f"[red]Project '{project}' not found.[/red]")
|
|
996
|
+
raise typer.Exit(code=1)
|
|
997
|
+
|
|
998
|
+
store = GraphStore(project_dir)
|
|
999
|
+
|
|
1000
|
+
# Verify the project has nodes
|
|
1001
|
+
nodes = store.get_nodes()
|
|
1002
|
+
if not nodes:
|
|
1003
|
+
console.print("[red]Project is empty — no indexed nodes found.[/red] Re-run [cyan]cg project index[/cyan].")
|
|
1004
|
+
store.close()
|
|
1005
|
+
raise typer.Exit(code=1)
|
|
1006
|
+
|
|
1007
|
+
# Check if port is available
|
|
1008
|
+
actual_port = port
|
|
1009
|
+
for attempt in range(5):
|
|
1010
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
1011
|
+
if s.connect_ex(("127.0.0.1", actual_port)) != 0:
|
|
1012
|
+
break
|
|
1013
|
+
actual_port += 1
|
|
1014
|
+
else:
|
|
1015
|
+
console.print(f"[red]Ports {port}-{port+4} are all in use.[/red]")
|
|
1016
|
+
store.close()
|
|
1017
|
+
raise typer.Exit(code=1)
|
|
1018
|
+
|
|
1019
|
+
server_app = _create_server(
|
|
1020
|
+
store,
|
|
1021
|
+
llm_provider=cfg.LLM_PROVIDER,
|
|
1022
|
+
llm_model=cfg.LLM_MODEL,
|
|
1023
|
+
llm_api_key=cfg.LLM_API_KEY,
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
url = f"http://127.0.0.1:{actual_port}"
|
|
1027
|
+
console.print(f"\n[bold green]🌐 CodeGraph Explorer[/bold green]")
|
|
1028
|
+
console.print(f" Project: [cyan]{project}[/cyan]")
|
|
1029
|
+
console.print(f" URL: [link={url}]{url}[/link]")
|
|
1030
|
+
console.print(f" Nodes: {len(nodes)}")
|
|
1031
|
+
console.print(f"\n [dim]Press Ctrl+C to stop the server[/dim]\n")
|
|
1032
|
+
|
|
1033
|
+
# Open browser after a short delay
|
|
1034
|
+
def _open_browser():
|
|
1035
|
+
time.sleep(1.0)
|
|
1036
|
+
webbrowser.open(url)
|
|
1037
|
+
|
|
1038
|
+
threading.Thread(target=_open_browser, daemon=True).start()
|
|
1039
|
+
|
|
1040
|
+
try:
|
|
1041
|
+
import uvicorn
|
|
1042
|
+
except ImportError:
|
|
1043
|
+
raise ImportError(
|
|
1044
|
+
"The 'explore' feature requires uvicorn.\n"
|
|
1045
|
+
"Install with: pip install codegraph-cli[explore]"
|
|
1046
|
+
)
|
|
1047
|
+
try:
|
|
1048
|
+
uvicorn.run(server_app, host="127.0.0.1", port=actual_port, log_level="warning")
|
|
1049
|
+
except KeyboardInterrupt:
|
|
1050
|
+
pass
|
|
1051
|
+
finally:
|
|
1052
|
+
store.close()
|
|
1053
|
+
console.print("\n[dim]Server stopped.[/dim]")
|