codexa 0.4.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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- semantic_code_intelligence/workspace/__init__.py +427 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""Lightweight server-rendered web UI for CodexA.
|
|
2
|
+
|
|
3
|
+
Serves a minimal HTML interface using only the Python standard library.
|
|
4
|
+
The UI uses inline CSS and vanilla JavaScript — no build step, no npm,
|
|
5
|
+
no framework dependencies.
|
|
6
|
+
|
|
7
|
+
Pages:
|
|
8
|
+
/ → dashboard / search page
|
|
9
|
+
/symbols → symbol browser
|
|
10
|
+
/workspace → workspace repo viewer
|
|
11
|
+
/viz → visualization viewer (call graph, deps)
|
|
12
|
+
|
|
13
|
+
The UI pages call the JSON API endpoints (``/api/...``) via ``fetch()``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import html
|
|
19
|
+
import json
|
|
20
|
+
import urllib.parse
|
|
21
|
+
from http.server import BaseHTTPRequestHandler
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from semantic_code_intelligence.utils.logging import get_logger
|
|
26
|
+
from semantic_code_intelligence.web.visualize import (
|
|
27
|
+
render_call_graph,
|
|
28
|
+
render_dependency_graph,
|
|
29
|
+
render_symbol_map,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = get_logger("web.ui")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
# HTML templates (inline — no external files needed)
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
_CSS = """\
|
|
40
|
+
:root { --bg: #0d1117; --surface: #161b22; --border: #30363d;
|
|
41
|
+
--text: #e6edf3; --dim: #8b949e; --accent: #58a6ff;
|
|
42
|
+
--green: #56d364; --red: #f85149; --yellow: #e3b341; }
|
|
43
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
44
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
45
|
+
background: var(--bg); color: var(--text); line-height: 1.6; }
|
|
46
|
+
a { color: var(--accent); text-decoration: none; }
|
|
47
|
+
a:hover { text-decoration: underline; }
|
|
48
|
+
nav { background: var(--surface); border-bottom: 1px solid var(--border);
|
|
49
|
+
padding: 0.75rem 1.5rem; display: flex; align-items: center; gap: 2rem; }
|
|
50
|
+
nav .logo { font-weight: 700; font-size: 1.1rem; }
|
|
51
|
+
nav .links { display: flex; gap: 1rem; }
|
|
52
|
+
.container { max-width: 960px; margin: 2rem auto; padding: 0 1rem; }
|
|
53
|
+
h1 { font-size: 1.5rem; margin-bottom: 1rem; }
|
|
54
|
+
.search-box { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
|
|
55
|
+
.search-box input { flex: 1; padding: 0.6rem 1rem; background: var(--surface);
|
|
56
|
+
border: 1px solid var(--border); border-radius: 6px; color: var(--text);
|
|
57
|
+
font-size: 0.95rem; }
|
|
58
|
+
.search-box input:focus { outline: none; border-color: var(--accent); }
|
|
59
|
+
.search-box button { padding: 0.6rem 1.2rem; background: var(--accent); color: #fff;
|
|
60
|
+
border: none; border-radius: 6px; cursor: pointer; font-weight: 600; }
|
|
61
|
+
.search-box button:hover { opacity: 0.9; }
|
|
62
|
+
.card { background: var(--surface); border: 1px solid var(--border);
|
|
63
|
+
border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
|
|
64
|
+
.card h3 { font-size: 0.95rem; margin-bottom: 0.5rem; }
|
|
65
|
+
.card .meta { color: var(--dim); font-size: 0.8rem; margin-bottom: 0.5rem; }
|
|
66
|
+
pre { background: #0d1117; border: 1px solid var(--border); border-radius: 6px;
|
|
67
|
+
padding: 0.75rem; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; }
|
|
68
|
+
code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; }
|
|
69
|
+
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px;
|
|
70
|
+
font-size: 0.75rem; font-weight: 600; }
|
|
71
|
+
.badge-fn { background: #1f3a5f; color: var(--accent); }
|
|
72
|
+
.badge-cls { background: #2d1f3f; color: #d2a8ff; }
|
|
73
|
+
.badge-method { background: #1f3f2f; color: var(--green); }
|
|
74
|
+
.badge-import { background: #3f2f1f; color: var(--yellow); }
|
|
75
|
+
.score { color: var(--green); font-weight: 600; }
|
|
76
|
+
.mermaid { background: var(--surface); border: 1px solid var(--border);
|
|
77
|
+
border-radius: 8px; padding: 1rem; text-align: center; }
|
|
78
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; }
|
|
79
|
+
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); }
|
|
80
|
+
th { color: var(--dim); font-weight: 600; font-size: 0.85rem; }
|
|
81
|
+
.status-ok { color: var(--green); }
|
|
82
|
+
.status-err { color: var(--red); }
|
|
83
|
+
#loading { color: var(--dim); display: none; }
|
|
84
|
+
.empty { color: var(--dim); text-align: center; padding: 2rem; }
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
_NAV = """\
|
|
88
|
+
<nav>
|
|
89
|
+
<span class="logo">⚛ CodexA</span>
|
|
90
|
+
<div class="links">
|
|
91
|
+
<a href="/">Search</a>
|
|
92
|
+
<a href="/symbols">Symbols</a>
|
|
93
|
+
<a href="/tools">Tools</a>
|
|
94
|
+
<a href="/quality">Quality</a>
|
|
95
|
+
<a href="/ask">Ask</a>
|
|
96
|
+
<a href="/workspace">Workspace</a>
|
|
97
|
+
<a href="/viz">Visualize</a>
|
|
98
|
+
</div>
|
|
99
|
+
</nav>
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _page(title: str, body: str, script: str = "", *, mermaid: bool = False) -> str:
|
|
104
|
+
"""Wrap content in a full HTML page."""
|
|
105
|
+
mermaid_tag = (
|
|
106
|
+
'<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>\n'
|
|
107
|
+
'<script>mermaid.initialize({startOnLoad:false, theme:"dark"});</script>'
|
|
108
|
+
if mermaid else ""
|
|
109
|
+
)
|
|
110
|
+
return f"""\
|
|
111
|
+
<!DOCTYPE html>
|
|
112
|
+
<html lang="en">
|
|
113
|
+
<head>
|
|
114
|
+
<meta charset="utf-8">
|
|
115
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
116
|
+
<title>{html.escape(title)} — CodexA</title>
|
|
117
|
+
<style>{_CSS}</style>
|
|
118
|
+
{mermaid_tag}
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
{_NAV}
|
|
122
|
+
<div class="container">
|
|
123
|
+
{body}
|
|
124
|
+
</div>
|
|
125
|
+
<script>{script}</script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>"""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Page builders
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def page_search() -> str:
|
|
135
|
+
"""Search page with live results."""
|
|
136
|
+
body = """\
|
|
137
|
+
<h1>Semantic Code Search</h1>
|
|
138
|
+
<div class="search-box">
|
|
139
|
+
<input type="text" id="q" placeholder="Search your codebase..." autofocus>
|
|
140
|
+
<button onclick="doSearch()">Search</button>
|
|
141
|
+
</div>
|
|
142
|
+
<div id="loading">Searching...</div>
|
|
143
|
+
<div id="results"></div>
|
|
144
|
+
"""
|
|
145
|
+
script = """\
|
|
146
|
+
document.getElementById('q').addEventListener('keydown', e => {
|
|
147
|
+
if (e.key === 'Enter') doSearch();
|
|
148
|
+
});
|
|
149
|
+
async function doSearch() {
|
|
150
|
+
const q = document.getElementById('q').value.trim();
|
|
151
|
+
if (!q) return;
|
|
152
|
+
document.getElementById('loading').style.display = 'block';
|
|
153
|
+
document.getElementById('results').innerHTML = '';
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch('/api/search?q=' + encodeURIComponent(q) + '&top_k=10');
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
document.getElementById('loading').style.display = 'none';
|
|
158
|
+
const snippets = data.snippets || [];
|
|
159
|
+
if (!snippets.length) {
|
|
160
|
+
document.getElementById('results').innerHTML = '<div class="empty">No results found.</div>';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let html = '';
|
|
164
|
+
snippets.forEach((s, i) => {
|
|
165
|
+
const esc = s => (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
166
|
+
html += '<div class="card">' +
|
|
167
|
+
'<h3>#' + (i+1) + ' ' + esc(s.file_path) + '</h3>' +
|
|
168
|
+
'<div class="meta">Lines ' + s.start_line + '-' + s.end_line +
|
|
169
|
+
' · <span class="score">score: ' + (s.score||0).toFixed(4) + '</span></div>' +
|
|
170
|
+
'<pre><code>' + esc(s.content) + '</code></pre></div>';
|
|
171
|
+
});
|
|
172
|
+
document.getElementById('results').innerHTML = html;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
document.getElementById('loading').style.display = 'none';
|
|
175
|
+
document.getElementById('results').innerHTML = '<div class="card"><p class="status-err">' + err + '</p></div>';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
"""
|
|
179
|
+
return _page("Search", body, script)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def page_symbols() -> str:
|
|
183
|
+
"""Symbol browser page."""
|
|
184
|
+
body = """\
|
|
185
|
+
<h1>Symbol Browser</h1>
|
|
186
|
+
<div class="search-box">
|
|
187
|
+
<input type="text" id="filter" placeholder="Filter by file or symbol name...">
|
|
188
|
+
<select id="kind">
|
|
189
|
+
<option value="">All kinds</option>
|
|
190
|
+
<option value="function">Functions</option>
|
|
191
|
+
<option value="class">Classes</option>
|
|
192
|
+
<option value="method">Methods</option>
|
|
193
|
+
<option value="import">Imports</option>
|
|
194
|
+
</select>
|
|
195
|
+
<button onclick="loadSymbols()">Load</button>
|
|
196
|
+
</div>
|
|
197
|
+
<div id="loading">Loading...</div>
|
|
198
|
+
<div id="results"></div>
|
|
199
|
+
"""
|
|
200
|
+
script = """\
|
|
201
|
+
async function loadSymbols() {
|
|
202
|
+
const file = document.getElementById('filter').value.trim();
|
|
203
|
+
const kind = document.getElementById('kind').value;
|
|
204
|
+
document.getElementById('loading').style.display = 'block';
|
|
205
|
+
let url = '/api/symbols?';
|
|
206
|
+
if (file) url += 'file=' + encodeURIComponent(file) + '&';
|
|
207
|
+
if (kind) url += 'kind=' + encodeURIComponent(kind);
|
|
208
|
+
try {
|
|
209
|
+
const res = await fetch(url);
|
|
210
|
+
const data = await res.json();
|
|
211
|
+
document.getElementById('loading').style.display = 'none';
|
|
212
|
+
const syms = data.symbols || [];
|
|
213
|
+
if (!syms.length) {
|
|
214
|
+
document.getElementById('results').innerHTML = '<div class="empty">No symbols found. Make sure the project is indexed.</div>';
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let html = '<table><tr><th>Name</th><th>Kind</th><th>File</th><th>Lines</th></tr>';
|
|
218
|
+
syms.forEach(s => {
|
|
219
|
+
const esc = t => (t||'').replace(/&/g,'&').replace(/</g,'<');
|
|
220
|
+
const badge = {'function':'badge-fn','class':'badge-cls','method':'badge-method','import':'badge-import'}[s.kind] || '';
|
|
221
|
+
html += '<tr><td><code>' + esc(s.name) + '</code></td>' +
|
|
222
|
+
'<td><span class="badge ' + badge + '">' + esc(s.kind) + '</span></td>' +
|
|
223
|
+
'<td>' + esc(s.file_path) + '</td>' +
|
|
224
|
+
'<td>' + s.start_line + '-' + s.end_line + '</td></tr>';
|
|
225
|
+
});
|
|
226
|
+
html += '</table>';
|
|
227
|
+
if (data.count > syms.length) html += '<div class="empty">Showing ' + syms.length + ' of ' + data.count + ' symbols.</div>';
|
|
228
|
+
document.getElementById('results').innerHTML = html;
|
|
229
|
+
} catch(e) {
|
|
230
|
+
document.getElementById('loading').style.display = 'none';
|
|
231
|
+
document.getElementById('results').innerHTML = '<div class="card status-err">' + e + '</div>';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
"""
|
|
235
|
+
return _page("Symbols", body, script)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def page_workspace() -> str:
|
|
239
|
+
"""Workspace repo browser page."""
|
|
240
|
+
body = """\
|
|
241
|
+
<h1>Workspace</h1>
|
|
242
|
+
<div id="health"></div>
|
|
243
|
+
<div id="summary" style="margin-top:1rem;"></div>
|
|
244
|
+
"""
|
|
245
|
+
script = """\
|
|
246
|
+
async function load() {
|
|
247
|
+
try {
|
|
248
|
+
const hRes = await fetch('/health');
|
|
249
|
+
const health = await hRes.json();
|
|
250
|
+
document.getElementById('health').innerHTML =
|
|
251
|
+
'<div class="card"><h3>Project Status</h3>' +
|
|
252
|
+
'<table><tr><td>Root</td><td><code>' + (health.project_root||'') + '</code></td></tr>' +
|
|
253
|
+
'<tr><td>Indexed</td><td class="' + (health.indexed?'status-ok':'status-err') + '">' + health.indexed + '</td></tr>' +
|
|
254
|
+
'<tr><td>Config</td><td class="' + (health.config_found?'status-ok':'status-err') + '">' + health.config_found + '</td></tr></table></div>';
|
|
255
|
+
} catch(e) {
|
|
256
|
+
document.getElementById('health').innerHTML = '<div class="card status-err">' + e + '</div>';
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const sRes = await fetch('/api/summary');
|
|
260
|
+
const summary = await sRes.json();
|
|
261
|
+
const langs = summary.languages || summary.language_breakdown || {};
|
|
262
|
+
let langHtml = '';
|
|
263
|
+
for (const [lang, count] of Object.entries(langs)) {
|
|
264
|
+
langHtml += '<tr><td>' + lang + '</td><td>' + count + '</td></tr>';
|
|
265
|
+
}
|
|
266
|
+
document.getElementById('summary').innerHTML =
|
|
267
|
+
'<div class="card"><h3>Repository Summary</h3>' +
|
|
268
|
+
(langHtml ? '<table><tr><th>Language</th><th>Files</th></tr>' + langHtml + '</table>' : '<p class="empty">No summary available. Index the project first.</p>') +
|
|
269
|
+
'</div>';
|
|
270
|
+
} catch(e) {}
|
|
271
|
+
}
|
|
272
|
+
load();
|
|
273
|
+
"""
|
|
274
|
+
return _page("Workspace", body, script)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def page_viz() -> str:
|
|
278
|
+
"""Visualization page with Mermaid rendering."""
|
|
279
|
+
body = """\
|
|
280
|
+
<h1>Visualizations</h1>
|
|
281
|
+
<p style="color:var(--dim);margin-bottom:1rem;">
|
|
282
|
+
Generate Mermaid-compatible diagrams from your codebase analysis.
|
|
283
|
+
</p>
|
|
284
|
+
<div style="display:flex;gap:0.5rem;margin-bottom:1rem;">
|
|
285
|
+
<button onclick="loadViz('callgraph')">Call Graph</button>
|
|
286
|
+
<button onclick="loadViz('deps')">Dependencies</button>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="search-box">
|
|
289
|
+
<input type="text" id="target" placeholder="Symbol or file name (optional)">
|
|
290
|
+
</div>
|
|
291
|
+
<div id="diagram"></div>
|
|
292
|
+
<div id="mermaid-source" style="margin-top:1rem;"></div>
|
|
293
|
+
"""
|
|
294
|
+
script = """\
|
|
295
|
+
async function loadViz(kind) {
|
|
296
|
+
const target = document.getElementById('target').value.trim();
|
|
297
|
+
const diagram = document.getElementById('diagram');
|
|
298
|
+
const source = document.getElementById('mermaid-source');
|
|
299
|
+
diagram.innerHTML = '<div class="empty">Loading…</div>';
|
|
300
|
+
source.innerHTML = '';
|
|
301
|
+
try {
|
|
302
|
+
const vizRes = await fetch('/api/viz/' + kind + '?target=' + encodeURIComponent(target));
|
|
303
|
+
const vizData = await vizRes.json();
|
|
304
|
+
if (vizData.error) { diagram.innerHTML = '<div class="card status-err">' + vizData.error + '</div>'; return; }
|
|
305
|
+
const src = vizData.mermaid || '';
|
|
306
|
+
const title = kind === 'callgraph' ? 'Call Graph' : 'Dependencies';
|
|
307
|
+
// Render with Mermaid
|
|
308
|
+
const container = document.createElement('div');
|
|
309
|
+
container.className = 'card';
|
|
310
|
+
const heading = document.createElement('h3');
|
|
311
|
+
heading.textContent = title;
|
|
312
|
+
container.appendChild(heading);
|
|
313
|
+
const mermaidDiv = document.createElement('div');
|
|
314
|
+
mermaidDiv.className = 'mermaid';
|
|
315
|
+
const id = 'mermaid-' + Date.now();
|
|
316
|
+
try {
|
|
317
|
+
const { svg } = await mermaid.render(id, src);
|
|
318
|
+
mermaidDiv.innerHTML = svg;
|
|
319
|
+
} catch(_) {
|
|
320
|
+
mermaidDiv.innerHTML = '<pre>' + src.replace(/&/g,'&').replace(/</g,'<') + '</pre>';
|
|
321
|
+
}
|
|
322
|
+
container.appendChild(mermaidDiv);
|
|
323
|
+
diagram.innerHTML = '';
|
|
324
|
+
diagram.appendChild(container);
|
|
325
|
+
source.innerHTML =
|
|
326
|
+
'<details><summary style="color:var(--dim);cursor:pointer;">Raw Mermaid source</summary>' +
|
|
327
|
+
'<pre><code>' + src.replace(/&/g,'&').replace(/</g,'<') + '</code></pre></details>';
|
|
328
|
+
} catch(e) {
|
|
329
|
+
diagram.innerHTML = '<div class="card status-err">' + e + '</div>';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
"""
|
|
333
|
+
return _page("Visualize", body, script, mermaid=True)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def page_tools() -> str:
|
|
337
|
+
"""Tool runner page — focused quick actions for code exploration."""
|
|
338
|
+
body = """\
|
|
339
|
+
<h1>Tools</h1>
|
|
340
|
+
<p style="color:var(--dim);margin-bottom:1.5rem;">Quick code exploration — explain symbols, trace call graphs, and inspect dependencies.</p>
|
|
341
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;margin-bottom:1.5rem;">
|
|
342
|
+
<div class="card" style="cursor:pointer;" onclick="showTool('explain_symbol')">
|
|
343
|
+
<h3>🔍 Explain Symbol</h3>
|
|
344
|
+
<div class="meta">Structural explanation of a class, function, or method</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="card" style="cursor:pointer;" onclick="showTool('explain_file')">
|
|
347
|
+
<h3>📄 Explain File</h3>
|
|
348
|
+
<div class="meta">List and explain all symbols in a source file</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="card" style="cursor:pointer;" onclick="showTool('get_call_graph')">
|
|
351
|
+
<h3>🔬 Call Graph</h3>
|
|
352
|
+
<div class="meta">Show callers and callees for a symbol</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="card" style="cursor:pointer;" onclick="showTool('get_dependencies')">
|
|
355
|
+
<h3>🔗 Dependencies</h3>
|
|
356
|
+
<div class="meta">Import and dependency map for a file</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
<div id="tool-form" style="display:none;">
|
|
360
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
|
|
361
|
+
<span id="tool-title" style="font-weight:600;font-size:1.1rem;"></span>
|
|
362
|
+
<span style="flex:1;"></span>
|
|
363
|
+
<button onclick="hideTool()" style="padding:0.3rem 0.75rem;background:var(--surface);
|
|
364
|
+
border:1px solid var(--border);border-radius:6px;color:var(--dim);cursor:pointer;">Back</button>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="search-box">
|
|
367
|
+
<input type="text" id="tool-input" placeholder="" autofocus>
|
|
368
|
+
<button id="tool-run" onclick="runTool()">Run</button>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
<div id="loading">Running...</div>
|
|
372
|
+
<div id="results"></div>
|
|
373
|
+
"""
|
|
374
|
+
script = """\
|
|
375
|
+
const TOOLS = {
|
|
376
|
+
explain_symbol: {title:'Explain Symbol', placeholder:'Symbol name (e.g. MyClass)', arg:'symbol_name'},
|
|
377
|
+
explain_file: {title:'Explain File', placeholder:'File path (e.g. src/auth.py)', arg:'file_path'},
|
|
378
|
+
get_call_graph: {title:'Call Graph', placeholder:'Symbol name (e.g. handle_request)', arg:'symbol_name'},
|
|
379
|
+
get_dependencies:{title:'Dependencies', placeholder:'File path (e.g. src/main.py)', arg:'file_path'},
|
|
380
|
+
};
|
|
381
|
+
let currentTool = null;
|
|
382
|
+
function showTool(name) {
|
|
383
|
+
currentTool = name;
|
|
384
|
+
const t = TOOLS[name];
|
|
385
|
+
document.getElementById('tool-title').textContent = t.title;
|
|
386
|
+
document.getElementById('tool-input').placeholder = t.placeholder;
|
|
387
|
+
document.getElementById('tool-input').value = '';
|
|
388
|
+
document.getElementById('tool-form').style.display = 'block';
|
|
389
|
+
document.getElementById('results').innerHTML = '';
|
|
390
|
+
document.getElementById('tool-input').focus();
|
|
391
|
+
}
|
|
392
|
+
function hideTool() {
|
|
393
|
+
document.getElementById('tool-form').style.display = 'none';
|
|
394
|
+
document.getElementById('results').innerHTML = '';
|
|
395
|
+
currentTool = null;
|
|
396
|
+
}
|
|
397
|
+
document.addEventListener('keydown', e => {
|
|
398
|
+
if (e.key === 'Enter' && currentTool) runTool();
|
|
399
|
+
});
|
|
400
|
+
async function runTool() {
|
|
401
|
+
if (!currentTool) return;
|
|
402
|
+
const t = TOOLS[currentTool];
|
|
403
|
+
const val = document.getElementById('tool-input').value.trim();
|
|
404
|
+
if (!val) return;
|
|
405
|
+
document.getElementById('loading').style.display = 'block';
|
|
406
|
+
document.getElementById('results').innerHTML = '';
|
|
407
|
+
const args = {}; args[t.arg] = val;
|
|
408
|
+
try {
|
|
409
|
+
const res = await fetch('/api/tools/run', {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: {'Content-Type': 'application/json'},
|
|
412
|
+
body: JSON.stringify({tool_name: currentTool, arguments: args}),
|
|
413
|
+
});
|
|
414
|
+
const data = await res.json();
|
|
415
|
+
document.getElementById('loading').style.display = 'none';
|
|
416
|
+
const esc = s => (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
417
|
+
const payload = data.result_payload || data;
|
|
418
|
+
const ok = data.success !== false;
|
|
419
|
+
let html = '';
|
|
420
|
+
if (!ok) {
|
|
421
|
+
const err = data.error || {};
|
|
422
|
+
html = '<div class="card" style="border-color:var(--red);"><p class="status-err">'
|
|
423
|
+
+ esc(err.error_message || JSON.stringify(data)) + '</p></div>';
|
|
424
|
+
} else if (currentTool === 'explain_symbol' || currentTool === 'explain_file') {
|
|
425
|
+
const d = payload.data || payload;
|
|
426
|
+
const syms = d.symbols || d.explanations || [];
|
|
427
|
+
if (typeof syms === 'object' && !Array.isArray(syms)) {
|
|
428
|
+
html = '<div class="card"><pre><code>' + esc(JSON.stringify(d, null, 2)) + '</code></pre></div>';
|
|
429
|
+
} else if (Array.isArray(syms)) {
|
|
430
|
+
syms.forEach(s => {
|
|
431
|
+
html += '<div class="card"><h3><span class="badge badge-' + (s.kind||'fn') + '">' + esc(s.kind||'symbol') + '</span> ' + esc(s.name||s.symbol_name||'') + '</h3>'
|
|
432
|
+
+ '<div class="meta">' + esc(s.file_path||'') + ' · ' + esc(s.lines||s.summary||'') + '</div></div>';
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (!html) html = '<div class="card"><pre><code>' + esc(JSON.stringify(d, null, 2)) + '</code></pre></div>';
|
|
436
|
+
} else {
|
|
437
|
+
html = '<div class="card"><pre><code>' + esc(JSON.stringify(payload.data || payload, null, 2)) + '</code></pre></div>';
|
|
438
|
+
}
|
|
439
|
+
document.getElementById('results').innerHTML = html;
|
|
440
|
+
} catch(e) {
|
|
441
|
+
document.getElementById('loading').style.display = 'none';
|
|
442
|
+
document.getElementById('results').innerHTML = '<div class="card"><p class="status-err">' + e + '</p></div>';
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
"""
|
|
446
|
+
return _page("Tools", body, script)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def page_quality() -> str:
|
|
450
|
+
"""Quality analysis page — metrics, hotspots, complexity."""
|
|
451
|
+
body = """\
|
|
452
|
+
<h1>Code Quality</h1>
|
|
453
|
+
<div style="display:flex;gap:0.5rem;margin-bottom:1.5rem;">
|
|
454
|
+
<button onclick="load('quality')" style="padding:0.6rem 1.2rem;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;">Quality</button>
|
|
455
|
+
<button onclick="load('metrics')" style="padding:0.6rem 1.2rem;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;">Metrics</button>
|
|
456
|
+
<button onclick="load('hotspots')" style="padding:0.6rem 1.2rem;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;">Hotspots</button>
|
|
457
|
+
</div>
|
|
458
|
+
<div id="loading">Analyzing...</div>
|
|
459
|
+
<div id="results"></div>
|
|
460
|
+
"""
|
|
461
|
+
script = """\
|
|
462
|
+
async function load(kind) {
|
|
463
|
+
document.getElementById('loading').style.display = 'block';
|
|
464
|
+
document.getElementById('results').innerHTML = '';
|
|
465
|
+
try {
|
|
466
|
+
const res = await fetch('/api/' + kind);
|
|
467
|
+
const data = await res.json();
|
|
468
|
+
document.getElementById('loading').style.display = 'none';
|
|
469
|
+
const esc = s => (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
470
|
+
if (kind === 'quality') {
|
|
471
|
+
const issues = data.complexity_issues || [];
|
|
472
|
+
let html = '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.5rem;margin-bottom:1rem;">';
|
|
473
|
+
html += stat(data.files_analyzed || 0, 'Files');
|
|
474
|
+
html += stat(data.symbol_count || 0, 'Symbols');
|
|
475
|
+
html += stat(data.issue_count || 0, 'Issues');
|
|
476
|
+
html += stat(issues.length, 'Complex');
|
|
477
|
+
html += '</div>';
|
|
478
|
+
if (issues.length) {
|
|
479
|
+
html += '<h3>High Complexity</h3>';
|
|
480
|
+
issues.slice(0, 20).forEach(i => {
|
|
481
|
+
const cls = i.complexity > 20 ? 'status-err' : i.complexity > 10 ? '' : 'status-ok';
|
|
482
|
+
html += '<div class="card"><h3>' + esc(i.symbol_name) + ' <span class="' + cls + '">' + i.complexity + '</span></h3>'
|
|
483
|
+
+ '<div class="meta">' + esc(i.file_path) + ':' + i.start_line + '</div></div>';
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
document.getElementById('results').innerHTML = html;
|
|
487
|
+
} else if (kind === 'metrics') {
|
|
488
|
+
let html = '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.5rem;">';
|
|
489
|
+
html += stat(data.total_loc || 0, 'Lines of Code');
|
|
490
|
+
html += stat(data.total_symbols || 0, 'Symbols');
|
|
491
|
+
html += stat((data.avg_complexity || 0).toFixed(1), 'Avg Complexity');
|
|
492
|
+
html += stat(data.max_complexity || 0, 'Max Complexity');
|
|
493
|
+
html += stat(data.files_analyzed || 0, 'Files');
|
|
494
|
+
html += stat((data.maintainability_index || 0).toFixed(1), 'Maintainability');
|
|
495
|
+
html += stat((data.comment_ratio || 0).toFixed(3), 'Comment Ratio');
|
|
496
|
+
html += stat(data.total_comment_lines || 0, 'Comment Lines');
|
|
497
|
+
html += '</div>';
|
|
498
|
+
document.getElementById('results').innerHTML = html;
|
|
499
|
+
} else if (kind === 'hotspots') {
|
|
500
|
+
const spots = data.hotspots || [];
|
|
501
|
+
let html = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;margin-bottom:1rem;">';
|
|
502
|
+
html += stat(data.files_analyzed || 0, 'Files');
|
|
503
|
+
html += stat(spots.length, 'Hotspots');
|
|
504
|
+
html += '</div>';
|
|
505
|
+
spots.forEach(h => {
|
|
506
|
+
const cls = h.risk_score > 20 ? 'status-err' : h.risk_score > 10 ? '' : 'status-ok';
|
|
507
|
+
html += '<div class="card"><h3>' + esc(h.name) + ' <span class="badge ' + cls + '">risk ' + (h.risk_score||0).toFixed(1) + '</span></h3>'
|
|
508
|
+
+ '<div class="meta">' + esc(h.file_path||'') + '</div></div>';
|
|
509
|
+
});
|
|
510
|
+
document.getElementById('results').innerHTML = html || '<div class="empty">No hotspots found.</div>';
|
|
511
|
+
}
|
|
512
|
+
} catch(e) {
|
|
513
|
+
document.getElementById('loading').style.display = 'none';
|
|
514
|
+
document.getElementById('results').innerHTML = '<div class="card"><p class="status-err">' + e + '</p></div>';
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function stat(val, label) {
|
|
518
|
+
return '<div class="card" style="text-align:center;padding:1rem;"><div style="font-size:1.5rem;font-weight:700;">' + val + '</div><div style="font-size:0.75rem;color:var(--dim);text-transform:uppercase;">' + label + '</div></div>';
|
|
519
|
+
}
|
|
520
|
+
"""
|
|
521
|
+
return _page("Quality", body, script)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def page_ask() -> str:
|
|
525
|
+
"""AI Q&A page — ask questions about the codebase."""
|
|
526
|
+
body = """\
|
|
527
|
+
<h1>Ask CodexA</h1>
|
|
528
|
+
<p style="color:var(--dim);margin-bottom:1rem;">Ask a natural-language question and get an AI-generated answer grounded in your codebase.</p>
|
|
529
|
+
<div class="search-box">
|
|
530
|
+
<input type="text" id="question" placeholder="How does authentication work?" autofocus>
|
|
531
|
+
<button onclick="doAsk()">Ask</button>
|
|
532
|
+
</div>
|
|
533
|
+
<div id="loading">Thinking...</div>
|
|
534
|
+
<div id="answer"></div>
|
|
535
|
+
<div id="context"></div>
|
|
536
|
+
"""
|
|
537
|
+
script = """\
|
|
538
|
+
document.getElementById('question').addEventListener('keydown', e => {
|
|
539
|
+
if (e.key === 'Enter') doAsk();
|
|
540
|
+
});
|
|
541
|
+
async function doAsk() {
|
|
542
|
+
const q = document.getElementById('question').value.trim();
|
|
543
|
+
if (!q) return;
|
|
544
|
+
document.getElementById('loading').style.display = 'block';
|
|
545
|
+
document.getElementById('answer').innerHTML = '';
|
|
546
|
+
document.getElementById('context').innerHTML = '';
|
|
547
|
+
try {
|
|
548
|
+
const res = await fetch('/api/ask', {
|
|
549
|
+
method: 'POST',
|
|
550
|
+
headers: {'Content-Type': 'application/json'},
|
|
551
|
+
body: JSON.stringify({question: q, top_k: 5}),
|
|
552
|
+
});
|
|
553
|
+
const data = await res.json();
|
|
554
|
+
document.getElementById('loading').style.display = 'none';
|
|
555
|
+
const esc = s => (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
556
|
+
// Render the answer
|
|
557
|
+
const answer = data.answer || 'No answer available.';
|
|
558
|
+
const elapsed = (data.elapsed_ms || 0).toFixed(0);
|
|
559
|
+
document.getElementById('answer').innerHTML = '<div class="card" style="border-color:var(--accent);">'
|
|
560
|
+
+ '<h3 style="color:var(--accent);">Answer</h3>'
|
|
561
|
+
+ '<div style="white-space:pre-wrap;line-height:1.7;">' + esc(answer) + '</div>'
|
|
562
|
+
+ '<div class="meta" style="margin-top:0.75rem;">' + elapsed + 'ms</div></div>';
|
|
563
|
+
// Render context snippets (collapsed)
|
|
564
|
+
const snippets = data.snippets || data.context_snippets || [];
|
|
565
|
+
if (snippets.length) {
|
|
566
|
+
let ctx = '<details style="margin-top:0.5rem;"><summary style="cursor:pointer;color:var(--dim);font-size:0.9rem;">'
|
|
567
|
+
+ snippets.length + ' supporting code snippets</summary>';
|
|
568
|
+
snippets.forEach((s, i) => {
|
|
569
|
+
ctx += '<div class="card" style="margin-top:0.5rem;"><h3>#' + (i+1) + ' ' + esc(s.file_path||'') + '</h3>'
|
|
570
|
+
+ '<div class="meta">Lines ' + (s.start_line||'?') + '-' + (s.end_line||'?')
|
|
571
|
+
+ '</div><pre><code>' + esc(s.content||s.chunk||'') + '</code></pre></div>';
|
|
572
|
+
});
|
|
573
|
+
ctx += '</details>';
|
|
574
|
+
document.getElementById('context').innerHTML = ctx;
|
|
575
|
+
}
|
|
576
|
+
} catch(e) {
|
|
577
|
+
document.getElementById('loading').style.display = 'none';
|
|
578
|
+
document.getElementById('answer').innerHTML = '<div class="card"><p class="status-err">' + e + '</p></div>';
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
"""
|
|
582
|
+
return _page("Ask", body, script)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ------------------------------------------------------------------
|
|
586
|
+
# HTTP handler mixin
|
|
587
|
+
# ------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
class UIHandler(BaseHTTPRequestHandler):
|
|
590
|
+
"""Serves HTML pages for the CodexA web interface.
|
|
591
|
+
|
|
592
|
+
Class-level attributes must be injected before use:
|
|
593
|
+
provider — a ContextProvider instance
|
|
594
|
+
"""
|
|
595
|
+
|
|
596
|
+
provider: Any # ContextProvider — avoids circular import at module level
|
|
597
|
+
|
|
598
|
+
def log_message(self, fmt: str, *args: Any) -> None:
|
|
599
|
+
logger.debug(fmt, *args)
|
|
600
|
+
|
|
601
|
+
def do_GET(self) -> None: # noqa: N802
|
|
602
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
603
|
+
path = parsed.path.rstrip("/") or "/"
|
|
604
|
+
qs = urllib.parse.parse_qs(parsed.query)
|
|
605
|
+
|
|
606
|
+
ui_routes: dict[str, str] = {
|
|
607
|
+
"/": page_search(),
|
|
608
|
+
"/symbols": page_symbols(),
|
|
609
|
+
"/tools": page_tools(),
|
|
610
|
+
"/quality": page_quality(),
|
|
611
|
+
"/ask": page_ask(),
|
|
612
|
+
"/workspace": page_workspace(),
|
|
613
|
+
"/viz": page_viz(),
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if path in ui_routes:
|
|
617
|
+
self._html(200, ui_routes[path])
|
|
618
|
+
elif path.startswith("/api/viz/"):
|
|
619
|
+
self._handle_viz_api(path, qs)
|
|
620
|
+
else:
|
|
621
|
+
self._html(404, _page("Not Found", '<div class="empty">Page not found.</div>'))
|
|
622
|
+
|
|
623
|
+
def _handle_viz_api(self, path: str, qs: dict[str, list[str]]) -> None:
|
|
624
|
+
"""Handle /api/viz/{kind}?target=... — return Mermaid source as JSON."""
|
|
625
|
+
kind = path.replace("/api/viz/", "").strip("/")
|
|
626
|
+
target = (qs.get("target", [""])[0])
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
if kind == "callgraph":
|
|
630
|
+
data = self.provider.get_call_graph(symbol_name=target)
|
|
631
|
+
edges = data.get("edges", [])
|
|
632
|
+
mermaid = render_call_graph(edges)
|
|
633
|
+
elif kind == "deps":
|
|
634
|
+
data = self.provider.get_dependencies(file_path=target)
|
|
635
|
+
mermaid = render_dependency_graph(data)
|
|
636
|
+
else:
|
|
637
|
+
self._json(400, {"error": f"Unknown viz kind: {kind}"})
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
self._json(200, {"kind": kind, "mermaid": mermaid})
|
|
641
|
+
except Exception as exc:
|
|
642
|
+
self._json(500, {"error": str(exc)})
|
|
643
|
+
|
|
644
|
+
def _html(self, status: int, content: str) -> None:
|
|
645
|
+
payload = content.encode("utf-8")
|
|
646
|
+
self.send_response(status)
|
|
647
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
648
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
649
|
+
self.end_headers()
|
|
650
|
+
self.wfile.write(payload)
|
|
651
|
+
|
|
652
|
+
def _json(self, status: int, body: dict[str, Any]) -> None:
|
|
653
|
+
payload = json.dumps(body, indent=2, default=str).encode("utf-8")
|
|
654
|
+
self.send_response(status)
|
|
655
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
656
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
657
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
658
|
+
self.end_headers()
|
|
659
|
+
self.wfile.write(payload)
|