@rangerchaz/aimem 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +380 -0
- package/dist/cli/commands/git.d.ts +6 -0
- package/dist/cli/commands/git.d.ts.map +1 -0
- package/dist/cli/commands/git.js +298 -0
- package/dist/cli/commands/git.js.map +1 -0
- package/dist/cli/commands/hook-session-end.d.ts +7 -0
- package/dist/cli/commands/hook-session-end.d.ts.map +1 -0
- package/dist/cli/commands/hook-session-end.js +109 -0
- package/dist/cli/commands/hook-session-end.js.map +1 -0
- package/dist/cli/commands/hook-session-start.d.ts +7 -0
- package/dist/cli/commands/hook-session-start.d.ts.map +1 -0
- package/dist/cli/commands/hook-session-start.js +116 -0
- package/dist/cli/commands/hook-session-start.js.map +1 -0
- package/dist/cli/commands/import.d.ts +14 -0
- package/dist/cli/commands/import.d.ts.map +1 -0
- package/dist/cli/commands/import.js +527 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +32 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/mcp-serve.d.ts +2 -0
- package/dist/cli/commands/mcp-serve.d.ts.map +1 -0
- package/dist/cli/commands/mcp-serve.js +5 -0
- package/dist/cli/commands/mcp-serve.js.map +1 -0
- package/dist/cli/commands/query.d.ts +8 -0
- package/dist/cli/commands/query.d.ts.map +1 -0
- package/dist/cli/commands/query.js +83 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +10 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +504 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/start.d.ts +8 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +90 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +85 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +7 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +46 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/visualize.d.ts +8 -0
- package/dist/cli/commands/visualize.d.ts.map +1 -0
- package/dist/cli/commands/visualize.js +96 -0
- package/dist/cli/commands/visualize.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +114 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/db/index.d.ts +55 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +464 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +4 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +200 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/extractor/index.d.ts +27 -0
- package/dist/extractor/index.d.ts.map +1 -0
- package/dist/extractor/index.js +227 -0
- package/dist/extractor/index.js.map +1 -0
- package/dist/git/extractor.d.ts +30 -0
- package/dist/git/extractor.d.ts.map +1 -0
- package/dist/git/extractor.js +126 -0
- package/dist/git/extractor.js.map +1 -0
- package/dist/git/hooks.d.ts +36 -0
- package/dist/git/hooks.d.ts.map +1 -0
- package/dist/git/hooks.js +142 -0
- package/dist/git/hooks.js.map +1 -0
- package/dist/git/index.d.ts +69 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +250 -0
- package/dist/git/index.js.map +1 -0
- package/dist/indexer/index.d.ts +20 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +173 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/parsers/base.d.ts +19 -0
- package/dist/indexer/parsers/base.d.ts.map +1 -0
- package/dist/indexer/parsers/base.js +46 -0
- package/dist/indexer/parsers/base.js.map +1 -0
- package/dist/indexer/parsers/cpp.d.ts +3 -0
- package/dist/indexer/parsers/cpp.d.ts.map +1 -0
- package/dist/indexer/parsers/cpp.js +180 -0
- package/dist/indexer/parsers/cpp.js.map +1 -0
- package/dist/indexer/parsers/go.d.ts +3 -0
- package/dist/indexer/parsers/go.d.ts.map +1 -0
- package/dist/indexer/parsers/go.js +98 -0
- package/dist/indexer/parsers/go.js.map +1 -0
- package/dist/indexer/parsers/java.d.ts +3 -0
- package/dist/indexer/parsers/java.d.ts.map +1 -0
- package/dist/indexer/parsers/java.js +204 -0
- package/dist/indexer/parsers/java.js.map +1 -0
- package/dist/indexer/parsers/javascript.d.ts +3 -0
- package/dist/indexer/parsers/javascript.d.ts.map +1 -0
- package/dist/indexer/parsers/javascript.js +157 -0
- package/dist/indexer/parsers/javascript.js.map +1 -0
- package/dist/indexer/parsers/kotlin.d.ts +3 -0
- package/dist/indexer/parsers/kotlin.d.ts.map +1 -0
- package/dist/indexer/parsers/kotlin.js +182 -0
- package/dist/indexer/parsers/kotlin.js.map +1 -0
- package/dist/indexer/parsers/php.d.ts +3 -0
- package/dist/indexer/parsers/php.d.ts.map +1 -0
- package/dist/indexer/parsers/php.js +190 -0
- package/dist/indexer/parsers/php.js.map +1 -0
- package/dist/indexer/parsers/python.d.ts +3 -0
- package/dist/indexer/parsers/python.d.ts.map +1 -0
- package/dist/indexer/parsers/python.js +101 -0
- package/dist/indexer/parsers/python.js.map +1 -0
- package/dist/indexer/parsers/ruby.d.ts +3 -0
- package/dist/indexer/parsers/ruby.d.ts.map +1 -0
- package/dist/indexer/parsers/ruby.js +92 -0
- package/dist/indexer/parsers/ruby.js.map +1 -0
- package/dist/indexer/parsers/rust.d.ts +3 -0
- package/dist/indexer/parsers/rust.d.ts.map +1 -0
- package/dist/indexer/parsers/rust.js +190 -0
- package/dist/indexer/parsers/rust.js.map +1 -0
- package/dist/indexer/watcher-daemon.d.ts +2 -0
- package/dist/indexer/watcher-daemon.d.ts.map +1 -0
- package/dist/indexer/watcher-daemon.js +27 -0
- package/dist/indexer/watcher-daemon.js.map +1 -0
- package/dist/indexer/watcher.d.ts +7 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +77 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +241 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/proxy/interceptor-mockttp.d.ts +27 -0
- package/dist/proxy/interceptor-mockttp.d.ts.map +1 -0
- package/dist/proxy/interceptor-mockttp.js +274 -0
- package/dist/proxy/interceptor-mockttp.js.map +1 -0
- package/dist/proxy/proxy-daemon.d.ts +5 -0
- package/dist/proxy/proxy-daemon.d.ts.map +1 -0
- package/dist/proxy/proxy-daemon.js +26 -0
- package/dist/proxy/proxy-daemon.js.map +1 -0
- package/dist/query/index.d.ts +32 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +135 -0
- package/dist/query/index.js.map +1 -0
- package/dist/types/index.d.ts +89 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/visualize/index.d.ts +144 -0
- package/dist/visualize/index.d.ts.map +1 -0
- package/dist/visualize/index.js +707 -0
- package/dist/visualize/index.js.map +1 -0
- package/dist/visualize/server.d.ts +7 -0
- package/dist/visualize/server.d.ts.map +1 -0
- package/dist/visualize/server.js +77 -0
- package/dist/visualize/server.js.map +1 -0
- package/dist/visualize/template.d.ts +3 -0
- package/dist/visualize/template.d.ts.map +1 -0
- package/dist/visualize/template.js +3465 -0
- package/dist/visualize/template.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,3465 @@
|
|
|
1
|
+
export function generateDashboardHTML(data) {
|
|
2
|
+
// Escape JSON for safe embedding in HTML script tag
|
|
3
|
+
// We use a script tag with type="application/json" to avoid parsing issues
|
|
4
|
+
const jsonData = JSON.stringify(data)
|
|
5
|
+
.replace(/</g, '\\u003c') // Escape < to prevent </script> issues
|
|
6
|
+
.replace(/>/g, '\\u003e') // Escape > for safety
|
|
7
|
+
.replace(/&/g, '\\u0026'); // Escape & for safety
|
|
8
|
+
return `<!DOCTYPE html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
+
<title>aimem Dashboard - ${escapeHtml(data.project.name)}</title>
|
|
14
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
|
|
15
|
+
<style>
|
|
16
|
+
* {
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
24
|
+
background: #1a1a2e;
|
|
25
|
+
color: #eee;
|
|
26
|
+
height: 100vh;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Header */
|
|
31
|
+
.header {
|
|
32
|
+
background: #16213e;
|
|
33
|
+
padding: 12px 20px;
|
|
34
|
+
display: flex;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
align-items: center;
|
|
37
|
+
border-bottom: 1px solid #0f3460;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.header h1 {
|
|
41
|
+
font-size: 18px;
|
|
42
|
+
font-weight: 500;
|
|
43
|
+
color: #e94560;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.header h1 span {
|
|
47
|
+
color: #eee;
|
|
48
|
+
font-weight: 400;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.search-box {
|
|
52
|
+
display: flex;
|
|
53
|
+
gap: 8px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.search-box input {
|
|
57
|
+
background: #0f3460;
|
|
58
|
+
border: 1px solid #1a1a2e;
|
|
59
|
+
color: #eee;
|
|
60
|
+
padding: 6px 12px;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
width: 200px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.search-box input::placeholder {
|
|
66
|
+
color: #666;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* View mode toggle */
|
|
70
|
+
.view-toggle {
|
|
71
|
+
display: flex;
|
|
72
|
+
background: #0f3460;
|
|
73
|
+
border-radius: 4px;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.view-toggle-btn {
|
|
78
|
+
background: transparent;
|
|
79
|
+
border: none;
|
|
80
|
+
color: #888;
|
|
81
|
+
padding: 6px 12px;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
font-size: 13px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.view-toggle-btn:hover {
|
|
87
|
+
color: #ccc;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.view-toggle-btn.active {
|
|
91
|
+
background: #e94560;
|
|
92
|
+
color: white;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.back-btn {
|
|
96
|
+
background: #0f3460;
|
|
97
|
+
border: 1px solid #1a1a2e;
|
|
98
|
+
color: #aaa;
|
|
99
|
+
padding: 6px 12px;
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-size: 13px;
|
|
103
|
+
display: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.back-btn:hover {
|
|
107
|
+
background: #1a1a2e;
|
|
108
|
+
color: #fff;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.back-btn.visible {
|
|
112
|
+
display: inline-block;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.visualize-search-btn {
|
|
116
|
+
background: #e94560;
|
|
117
|
+
border: 1px solid #e94560;
|
|
118
|
+
color: white;
|
|
119
|
+
padding: 6px 12px;
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
font-size: 13px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.visualize-search-btn:hover {
|
|
126
|
+
background: #ff6b8a;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.fullscreen-btn {
|
|
130
|
+
background: #0f3460;
|
|
131
|
+
border: 1px solid #1a1a2e;
|
|
132
|
+
color: #aaa;
|
|
133
|
+
padding: 6px 12px;
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
font-size: 13px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.fullscreen-btn:hover {
|
|
140
|
+
background: #1a1a2e;
|
|
141
|
+
color: #fff;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Fullscreen mode */
|
|
145
|
+
body.fullscreen .sidebar,
|
|
146
|
+
body.fullscreen .details-panel,
|
|
147
|
+
body.fullscreen .header,
|
|
148
|
+
body.fullscreen .tabs {
|
|
149
|
+
display: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
body.fullscreen .main {
|
|
153
|
+
height: 100vh;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
body.fullscreen .graph-container {
|
|
157
|
+
position: fixed;
|
|
158
|
+
top: 0;
|
|
159
|
+
left: 0;
|
|
160
|
+
right: 0;
|
|
161
|
+
bottom: 0;
|
|
162
|
+
z-index: 1000;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
body.fullscreen .view-description {
|
|
166
|
+
position: absolute;
|
|
167
|
+
top: 0;
|
|
168
|
+
left: 0;
|
|
169
|
+
right: 0;
|
|
170
|
+
z-index: 1001;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
body.fullscreen .stats-bar {
|
|
174
|
+
position: absolute;
|
|
175
|
+
bottom: 0;
|
|
176
|
+
left: 0;
|
|
177
|
+
right: 0;
|
|
178
|
+
z-index: 1001;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
body.fullscreen .exit-fullscreen {
|
|
182
|
+
position: fixed;
|
|
183
|
+
top: 16px;
|
|
184
|
+
right: 16px;
|
|
185
|
+
z-index: 1002;
|
|
186
|
+
background: rgba(22, 33, 62, 0.9);
|
|
187
|
+
color: #ccc;
|
|
188
|
+
border: 1px solid #0f3460;
|
|
189
|
+
padding: 10px 20px;
|
|
190
|
+
border-radius: 6px;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
font-size: 13px;
|
|
193
|
+
backdrop-filter: blur(4px);
|
|
194
|
+
transition: all 0.2s ease;
|
|
195
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
body.fullscreen .exit-fullscreen:hover {
|
|
199
|
+
background: rgba(233, 69, 96, 0.9);
|
|
200
|
+
color: white;
|
|
201
|
+
border-color: #e94560;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Fullscreen details panel - slides in from right */
|
|
205
|
+
body.fullscreen .details-panel {
|
|
206
|
+
display: none;
|
|
207
|
+
position: fixed;
|
|
208
|
+
top: 60px;
|
|
209
|
+
right: 16px;
|
|
210
|
+
bottom: 16px;
|
|
211
|
+
width: 380px;
|
|
212
|
+
z-index: 1001;
|
|
213
|
+
border-radius: 8px;
|
|
214
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
215
|
+
background: rgba(22, 33, 62, 0.95);
|
|
216
|
+
backdrop-filter: blur(8px);
|
|
217
|
+
border: 1px solid #0f3460;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
body.fullscreen .details-panel.visible {
|
|
221
|
+
display: flex;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
body.fullscreen .details-panel .details-header {
|
|
225
|
+
position: relative;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
body.fullscreen .details-panel .close-details {
|
|
229
|
+
display: block;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.close-details {
|
|
233
|
+
display: none;
|
|
234
|
+
background: none;
|
|
235
|
+
border: none;
|
|
236
|
+
color: #888;
|
|
237
|
+
font-size: 20px;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
padding: 4px 8px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.close-details:hover {
|
|
243
|
+
color: #e94560;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Visualize button in details panel */
|
|
247
|
+
.visualize-btn {
|
|
248
|
+
background: #e94560;
|
|
249
|
+
color: white;
|
|
250
|
+
border: none;
|
|
251
|
+
padding: 10px 16px;
|
|
252
|
+
border-radius: 6px;
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
font-size: 13px;
|
|
255
|
+
width: 100%;
|
|
256
|
+
margin-top: 12px;
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
gap: 8px;
|
|
261
|
+
transition: background 0.2s;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.visualize-btn:hover {
|
|
265
|
+
background: #d63d56;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.visualize-btn:disabled {
|
|
269
|
+
background: #475569;
|
|
270
|
+
cursor: not-allowed;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.visualize-btn-group {
|
|
274
|
+
display: flex;
|
|
275
|
+
gap: 8px;
|
|
276
|
+
margin-top: 12px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.visualize-btn-group .visualize-btn {
|
|
280
|
+
flex: 1;
|
|
281
|
+
margin-top: 0;
|
|
282
|
+
padding: 8px 12px;
|
|
283
|
+
font-size: 12px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.visualize-btn-group .visualize-btn.secondary {
|
|
287
|
+
background: #0f3460;
|
|
288
|
+
color: #ccc;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.visualize-btn-group .visualize-btn.secondary:hover {
|
|
292
|
+
background: #1a4a7a;
|
|
293
|
+
color: #fff;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* Smooth transitions for fullscreen */
|
|
297
|
+
.main, .graph-container, #cy {
|
|
298
|
+
transition: all 0.2s ease;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* Tabs */
|
|
302
|
+
.tabs {
|
|
303
|
+
background: #16213e;
|
|
304
|
+
display: flex;
|
|
305
|
+
gap: 0;
|
|
306
|
+
border-bottom: 1px solid #0f3460;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.tab {
|
|
310
|
+
padding: 10px 20px;
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
color: #888;
|
|
313
|
+
border-bottom: 2px solid transparent;
|
|
314
|
+
transition: all 0.2s;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.tab:hover {
|
|
318
|
+
color: #ccc;
|
|
319
|
+
background: rgba(233, 69, 96, 0.1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.tab.active {
|
|
323
|
+
color: #e94560;
|
|
324
|
+
border-bottom-color: #e94560;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Main layout */
|
|
328
|
+
.main {
|
|
329
|
+
display: flex;
|
|
330
|
+
height: calc(100vh - 90px);
|
|
331
|
+
min-height: 0;
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Sidebar */
|
|
336
|
+
.sidebar {
|
|
337
|
+
width: 220px;
|
|
338
|
+
background: #16213e;
|
|
339
|
+
padding: 16px;
|
|
340
|
+
border-right: 1px solid #0f3460;
|
|
341
|
+
overflow-y: auto;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.sidebar h3 {
|
|
345
|
+
font-size: 12px;
|
|
346
|
+
text-transform: uppercase;
|
|
347
|
+
color: #666;
|
|
348
|
+
margin-bottom: 12px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.filter-group {
|
|
352
|
+
margin-bottom: 20px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.filter-item {
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
gap: 8px;
|
|
359
|
+
padding: 6px 0;
|
|
360
|
+
cursor: pointer;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.filter-item input {
|
|
364
|
+
accent-color: #e94560;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.filter-item label {
|
|
368
|
+
font-size: 13px;
|
|
369
|
+
cursor: pointer;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.filter-item .count {
|
|
373
|
+
margin-left: auto;
|
|
374
|
+
font-size: 11px;
|
|
375
|
+
color: #666;
|
|
376
|
+
background: #0f3460;
|
|
377
|
+
padding: 2px 6px;
|
|
378
|
+
border-radius: 10px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.layout-select {
|
|
382
|
+
width: 100%;
|
|
383
|
+
background: #0f3460;
|
|
384
|
+
border: 1px solid #1a1a2e;
|
|
385
|
+
color: #eee;
|
|
386
|
+
padding: 8px;
|
|
387
|
+
border-radius: 4px;
|
|
388
|
+
margin-top: 8px;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* Graph container */
|
|
392
|
+
.graph-container {
|
|
393
|
+
flex: 1;
|
|
394
|
+
display: flex;
|
|
395
|
+
flex-direction: column;
|
|
396
|
+
min-height: 0; /* Important for flex overflow */
|
|
397
|
+
min-width: 0;
|
|
398
|
+
position: relative;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#cy {
|
|
402
|
+
flex: 1;
|
|
403
|
+
background: #1a1a2e;
|
|
404
|
+
min-height: 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Details panel */
|
|
408
|
+
.details-panel {
|
|
409
|
+
width: 350px;
|
|
410
|
+
background: #16213e;
|
|
411
|
+
border-left: 1px solid #0f3460;
|
|
412
|
+
display: flex;
|
|
413
|
+
flex-direction: column;
|
|
414
|
+
overflow: hidden;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.details-header {
|
|
418
|
+
padding: 12px 16px;
|
|
419
|
+
border-bottom: 1px solid #0f3460;
|
|
420
|
+
display: flex;
|
|
421
|
+
justify-content: space-between;
|
|
422
|
+
align-items: center;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.details-header h3 {
|
|
426
|
+
font-size: 14px;
|
|
427
|
+
font-weight: 500;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.details-content {
|
|
431
|
+
flex: 1;
|
|
432
|
+
overflow-y: auto;
|
|
433
|
+
padding: 16px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.detail-row {
|
|
437
|
+
margin-bottom: 12px;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.detail-label {
|
|
441
|
+
font-size: 11px;
|
|
442
|
+
text-transform: uppercase;
|
|
443
|
+
color: #666;
|
|
444
|
+
margin-bottom: 4px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.detail-value {
|
|
448
|
+
font-size: 13px;
|
|
449
|
+
color: #eee;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.detail-value a {
|
|
453
|
+
color: #e94560;
|
|
454
|
+
text-decoration: none;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.detail-value a:hover {
|
|
458
|
+
text-decoration: underline;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* Code block */
|
|
462
|
+
.code-block {
|
|
463
|
+
background: #0f3460;
|
|
464
|
+
border-radius: 4px;
|
|
465
|
+
padding: 12px;
|
|
466
|
+
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
|
|
467
|
+
font-size: 12px;
|
|
468
|
+
line-height: 1.5;
|
|
469
|
+
overflow-x: auto;
|
|
470
|
+
white-space: pre;
|
|
471
|
+
max-height: 300px;
|
|
472
|
+
overflow-y: auto;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* Tags */
|
|
476
|
+
.tag {
|
|
477
|
+
display: inline-block;
|
|
478
|
+
padding: 2px 8px;
|
|
479
|
+
border-radius: 3px;
|
|
480
|
+
font-size: 11px;
|
|
481
|
+
font-weight: 500;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.tag.function { background: #2563eb; color: white; }
|
|
485
|
+
.tag.class { background: #7c3aed; color: white; }
|
|
486
|
+
.tag.method { background: #0891b2; color: white; }
|
|
487
|
+
.tag.interface { background: #059669; color: white; }
|
|
488
|
+
.tag.type { background: #d97706; color: white; }
|
|
489
|
+
.tag.variable { background: #dc2626; color: white; }
|
|
490
|
+
.tag.module { background: #4f46e5; color: white; }
|
|
491
|
+
.tag.file { background: #475569; color: white; }
|
|
492
|
+
.tag.decision { background: #16a34a; color: white; }
|
|
493
|
+
.tag.pattern { background: #0d9488; color: white; }
|
|
494
|
+
.tag.rejection { background: #dc2626; color: white; }
|
|
495
|
+
|
|
496
|
+
/* List view */
|
|
497
|
+
#list-view {
|
|
498
|
+
display: none;
|
|
499
|
+
flex: 1;
|
|
500
|
+
overflow-y: auto;
|
|
501
|
+
overflow-x: hidden;
|
|
502
|
+
padding: 16px;
|
|
503
|
+
background: #1a1a2e;
|
|
504
|
+
min-height: 0; /* Important for flex overflow */
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#list-view.active {
|
|
508
|
+
display: flex;
|
|
509
|
+
flex-direction: column;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#list-view .list-content {
|
|
513
|
+
flex: 1;
|
|
514
|
+
overflow-y: auto;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#cy.hidden {
|
|
518
|
+
display: none !important;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.list-section {
|
|
522
|
+
margin-bottom: 24px;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.list-section h4 {
|
|
526
|
+
font-size: 12px;
|
|
527
|
+
text-transform: uppercase;
|
|
528
|
+
color: #666;
|
|
529
|
+
margin-bottom: 12px;
|
|
530
|
+
padding-bottom: 8px;
|
|
531
|
+
border-bottom: 1px solid #0f3460;
|
|
532
|
+
position: sticky;
|
|
533
|
+
top: 0;
|
|
534
|
+
background: #1a1a2e;
|
|
535
|
+
z-index: 10;
|
|
536
|
+
padding-top: 8px;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.list-item {
|
|
540
|
+
display: flex;
|
|
541
|
+
align-items: flex-start;
|
|
542
|
+
gap: 12px;
|
|
543
|
+
padding: 10px 12px;
|
|
544
|
+
background: #16213e;
|
|
545
|
+
border-radius: 4px;
|
|
546
|
+
margin-bottom: 8px;
|
|
547
|
+
cursor: pointer;
|
|
548
|
+
transition: background 0.2s;
|
|
549
|
+
max-width: 100%;
|
|
550
|
+
overflow: hidden;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.list-item:hover {
|
|
554
|
+
background: #1e2a4a;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.list-item .tag {
|
|
558
|
+
flex-shrink: 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.list-item-content {
|
|
562
|
+
flex: 1;
|
|
563
|
+
min-width: 0;
|
|
564
|
+
overflow: hidden;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.list-item-name {
|
|
568
|
+
font-weight: 500;
|
|
569
|
+
color: #eee;
|
|
570
|
+
margin-bottom: 4px;
|
|
571
|
+
white-space: nowrap;
|
|
572
|
+
overflow: hidden;
|
|
573
|
+
text-overflow: ellipsis;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.list-item-location {
|
|
577
|
+
font-size: 12px;
|
|
578
|
+
color: #666;
|
|
579
|
+
white-space: nowrap;
|
|
580
|
+
overflow: hidden;
|
|
581
|
+
text-overflow: ellipsis;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.list-item-signature {
|
|
585
|
+
font-size: 11px;
|
|
586
|
+
color: #888;
|
|
587
|
+
font-family: 'Fira Code', monospace;
|
|
588
|
+
margin-top: 4px;
|
|
589
|
+
white-space: nowrap;
|
|
590
|
+
overflow: hidden;
|
|
591
|
+
text-overflow: ellipsis;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* Stats bar */
|
|
595
|
+
.stats-bar {
|
|
596
|
+
padding: 8px 16px;
|
|
597
|
+
background: #0f3460;
|
|
598
|
+
display: flex;
|
|
599
|
+
gap: 20px;
|
|
600
|
+
font-size: 12px;
|
|
601
|
+
color: #888;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.stat {
|
|
605
|
+
display: flex;
|
|
606
|
+
gap: 4px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.stat-value {
|
|
610
|
+
color: #e94560;
|
|
611
|
+
font-weight: 500;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* Legend */
|
|
615
|
+
.legend {
|
|
616
|
+
position: absolute;
|
|
617
|
+
bottom: 60px;
|
|
618
|
+
left: 16px;
|
|
619
|
+
background: rgba(22, 33, 62, 0.95);
|
|
620
|
+
border: 1px solid #0f3460;
|
|
621
|
+
border-radius: 8px;
|
|
622
|
+
padding: 12px 16px;
|
|
623
|
+
font-size: 11px;
|
|
624
|
+
z-index: 100;
|
|
625
|
+
backdrop-filter: blur(4px);
|
|
626
|
+
transition: opacity 0.2s;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/* Hide legend in list view */
|
|
630
|
+
.legend.hidden {
|
|
631
|
+
display: none;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.legend-title {
|
|
635
|
+
font-weight: 600;
|
|
636
|
+
color: #888;
|
|
637
|
+
margin-bottom: 8px;
|
|
638
|
+
text-transform: uppercase;
|
|
639
|
+
font-size: 10px;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.legend-item {
|
|
643
|
+
display: flex;
|
|
644
|
+
align-items: center;
|
|
645
|
+
gap: 8px;
|
|
646
|
+
margin: 4px 0;
|
|
647
|
+
color: #ccc;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.legend-dot {
|
|
651
|
+
width: 12px;
|
|
652
|
+
height: 12px;
|
|
653
|
+
border-radius: 50%;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.legend-shape {
|
|
657
|
+
width: 16px;
|
|
658
|
+
height: 10px;
|
|
659
|
+
border-radius: 3px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/* Breadcrumb */
|
|
663
|
+
.breadcrumb {
|
|
664
|
+
display: flex;
|
|
665
|
+
align-items: center;
|
|
666
|
+
gap: 8px;
|
|
667
|
+
padding: 8px 16px;
|
|
668
|
+
background: #0f3460;
|
|
669
|
+
font-size: 12px;
|
|
670
|
+
color: #888;
|
|
671
|
+
border-bottom: 1px solid #1a1a2e;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.breadcrumb-item {
|
|
675
|
+
color: #888;
|
|
676
|
+
cursor: pointer;
|
|
677
|
+
transition: color 0.2s;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.breadcrumb-item:hover {
|
|
681
|
+
color: #e94560;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.breadcrumb-item.current {
|
|
685
|
+
color: #eee;
|
|
686
|
+
cursor: default;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.breadcrumb-sep {
|
|
690
|
+
color: #444;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* Tooltip */
|
|
694
|
+
#tooltip {
|
|
695
|
+
position: fixed;
|
|
696
|
+
background: rgba(22, 33, 62, 0.98);
|
|
697
|
+
border: 1px solid #0f3460;
|
|
698
|
+
border-radius: 6px;
|
|
699
|
+
padding: 10px 14px;
|
|
700
|
+
font-size: 12px;
|
|
701
|
+
color: #eee;
|
|
702
|
+
pointer-events: none;
|
|
703
|
+
z-index: 2000;
|
|
704
|
+
max-width: 350px;
|
|
705
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
|
|
706
|
+
display: none;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
#tooltip.visible {
|
|
710
|
+
display: block;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#tooltip .tip-type {
|
|
714
|
+
font-size: 10px;
|
|
715
|
+
text-transform: uppercase;
|
|
716
|
+
color: #888;
|
|
717
|
+
margin-bottom: 4px;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#tooltip .tip-name {
|
|
721
|
+
font-weight: 600;
|
|
722
|
+
font-size: 14px;
|
|
723
|
+
margin-bottom: 6px;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
#tooltip .tip-location {
|
|
727
|
+
font-size: 11px;
|
|
728
|
+
color: #666;
|
|
729
|
+
margin-bottom: 6px;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#tooltip .tip-hint {
|
|
733
|
+
font-size: 10px;
|
|
734
|
+
color: #e94560;
|
|
735
|
+
margin-top: 8px;
|
|
736
|
+
padding-top: 6px;
|
|
737
|
+
border-top: 1px solid #0f3460;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/* Welcome overlay */
|
|
741
|
+
.welcome-overlay {
|
|
742
|
+
position: absolute;
|
|
743
|
+
top: 50%;
|
|
744
|
+
left: 50%;
|
|
745
|
+
transform: translate(-50%, -50%);
|
|
746
|
+
background: rgba(22, 33, 62, 0.98);
|
|
747
|
+
border: 1px solid #0f3460;
|
|
748
|
+
border-radius: 12px;
|
|
749
|
+
padding: 32px 40px;
|
|
750
|
+
text-align: center;
|
|
751
|
+
z-index: 500;
|
|
752
|
+
max-width: 400px;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.welcome-overlay h2 {
|
|
756
|
+
color: #e94560;
|
|
757
|
+
margin-bottom: 16px;
|
|
758
|
+
font-size: 20px;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.welcome-overlay p {
|
|
762
|
+
color: #aaa;
|
|
763
|
+
margin-bottom: 12px;
|
|
764
|
+
line-height: 1.5;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.welcome-overlay .hint {
|
|
768
|
+
display: flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
gap: 8px;
|
|
771
|
+
padding: 8px 12px;
|
|
772
|
+
background: #0f3460;
|
|
773
|
+
border-radius: 6px;
|
|
774
|
+
margin: 8px 0;
|
|
775
|
+
text-align: left;
|
|
776
|
+
font-size: 13px;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.welcome-overlay .hint-icon {
|
|
780
|
+
font-size: 18px;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.welcome-overlay button {
|
|
784
|
+
background: #e94560;
|
|
785
|
+
color: white;
|
|
786
|
+
border: none;
|
|
787
|
+
padding: 12px 32px;
|
|
788
|
+
border-radius: 6px;
|
|
789
|
+
cursor: pointer;
|
|
790
|
+
font-size: 14px;
|
|
791
|
+
margin-top: 16px;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.welcome-overlay button:hover {
|
|
795
|
+
background: #d63d56;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/* View description */
|
|
799
|
+
.view-description {
|
|
800
|
+
padding: 10px 16px;
|
|
801
|
+
background: #0f3460;
|
|
802
|
+
border-bottom: 1px solid #1a1a2e;
|
|
803
|
+
font-size: 13px;
|
|
804
|
+
color: #aaa;
|
|
805
|
+
line-height: 1.4;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.view-description strong {
|
|
809
|
+
color: #e94560;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/* Empty state */
|
|
813
|
+
.empty-state {
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
align-items: center;
|
|
817
|
+
justify-content: center;
|
|
818
|
+
height: 100%;
|
|
819
|
+
color: #666;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.empty-state h3 {
|
|
823
|
+
margin-bottom: 8px;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/* Tooltip */
|
|
827
|
+
.cy-tooltip {
|
|
828
|
+
position: absolute;
|
|
829
|
+
background: #16213e;
|
|
830
|
+
border: 1px solid #0f3460;
|
|
831
|
+
padding: 8px 12px;
|
|
832
|
+
border-radius: 4px;
|
|
833
|
+
font-size: 12px;
|
|
834
|
+
pointer-events: none;
|
|
835
|
+
z-index: 1000;
|
|
836
|
+
max-width: 300px;
|
|
837
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/* ============================================
|
|
841
|
+
New View Styles
|
|
842
|
+
============================================ */
|
|
843
|
+
|
|
844
|
+
/* Custom view containers */
|
|
845
|
+
.custom-view {
|
|
846
|
+
display: none;
|
|
847
|
+
flex: 1;
|
|
848
|
+
overflow-y: auto;
|
|
849
|
+
padding: 20px;
|
|
850
|
+
background: #1a1a2e;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.custom-view.active {
|
|
854
|
+
display: block;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* Code Smells View */
|
|
858
|
+
.smells-summary {
|
|
859
|
+
display: flex;
|
|
860
|
+
gap: 16px;
|
|
861
|
+
margin-bottom: 24px;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.smell-stat {
|
|
865
|
+
background: #16213e;
|
|
866
|
+
border-radius: 8px;
|
|
867
|
+
padding: 16px 24px;
|
|
868
|
+
text-align: center;
|
|
869
|
+
min-width: 120px;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.smell-stat .count {
|
|
873
|
+
font-size: 32px;
|
|
874
|
+
font-weight: bold;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.smell-stat .label {
|
|
878
|
+
font-size: 12px;
|
|
879
|
+
color: #888;
|
|
880
|
+
text-transform: uppercase;
|
|
881
|
+
margin-top: 4px;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.smell-stat.high .count { color: #ef4444; }
|
|
885
|
+
.smell-stat.medium .count { color: #f59e0b; }
|
|
886
|
+
.smell-stat.low .count { color: #22c55e; }
|
|
887
|
+
|
|
888
|
+
.smell-filters {
|
|
889
|
+
display: flex;
|
|
890
|
+
gap: 8px;
|
|
891
|
+
margin-bottom: 16px;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.smell-filter-btn {
|
|
895
|
+
background: #0f3460;
|
|
896
|
+
border: 1px solid #1a1a2e;
|
|
897
|
+
color: #888;
|
|
898
|
+
padding: 6px 14px;
|
|
899
|
+
border-radius: 4px;
|
|
900
|
+
cursor: pointer;
|
|
901
|
+
font-size: 13px;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.smell-filter-btn:hover { color: #ccc; }
|
|
905
|
+
.smell-filter-btn.active { background: #e94560; color: white; border-color: #e94560; }
|
|
906
|
+
|
|
907
|
+
.smell-list {
|
|
908
|
+
display: flex;
|
|
909
|
+
flex-direction: column;
|
|
910
|
+
gap: 8px;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.smell-item {
|
|
914
|
+
display: flex;
|
|
915
|
+
align-items: center;
|
|
916
|
+
gap: 12px;
|
|
917
|
+
padding: 12px 16px;
|
|
918
|
+
background: #16213e;
|
|
919
|
+
border-radius: 6px;
|
|
920
|
+
cursor: pointer;
|
|
921
|
+
border-left: 4px solid transparent;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.smell-item:hover { background: #1e2a4a; }
|
|
925
|
+
.smell-item.high { border-left-color: #ef4444; }
|
|
926
|
+
.smell-item.medium { border-left-color: #f59e0b; }
|
|
927
|
+
.smell-item.low { border-left-color: #22c55e; }
|
|
928
|
+
|
|
929
|
+
.smell-badge {
|
|
930
|
+
padding: 4px 8px;
|
|
931
|
+
border-radius: 4px;
|
|
932
|
+
font-size: 11px;
|
|
933
|
+
font-weight: 600;
|
|
934
|
+
text-transform: uppercase;
|
|
935
|
+
min-width: 100px;
|
|
936
|
+
text-align: center;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.smell-badge.large-file { background: #7c3aed; color: white; }
|
|
940
|
+
.smell-badge.long-function { background: #2563eb; color: white; }
|
|
941
|
+
.smell-badge.too-many-callers { background: #0891b2; color: white; }
|
|
942
|
+
.smell-badge.too-many-callees { background: #059669; color: white; }
|
|
943
|
+
.smell-badge.orphan { background: #6b7280; color: white; }
|
|
944
|
+
|
|
945
|
+
.smell-info { flex: 1; }
|
|
946
|
+
.smell-name { font-weight: 500; margin-bottom: 2px; }
|
|
947
|
+
.smell-desc { font-size: 12px; color: #888; }
|
|
948
|
+
.smell-metric { font-size: 14px; color: #e94560; font-weight: 500; }
|
|
949
|
+
|
|
950
|
+
/* Hotspots View */
|
|
951
|
+
.hotspots-grid {
|
|
952
|
+
display: grid;
|
|
953
|
+
grid-template-columns: repeat(2, 1fr);
|
|
954
|
+
gap: 20px;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.hotspot-section {
|
|
958
|
+
background: #16213e;
|
|
959
|
+
border-radius: 8px;
|
|
960
|
+
padding: 16px;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.hotspot-section h3 {
|
|
964
|
+
font-size: 14px;
|
|
965
|
+
color: #e94560;
|
|
966
|
+
margin-bottom: 12px;
|
|
967
|
+
padding-bottom: 8px;
|
|
968
|
+
border-bottom: 1px solid #0f3460;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.hotspot-list {
|
|
972
|
+
display: flex;
|
|
973
|
+
flex-direction: column;
|
|
974
|
+
gap: 8px;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.hotspot-item {
|
|
978
|
+
display: flex;
|
|
979
|
+
align-items: center;
|
|
980
|
+
gap: 12px;
|
|
981
|
+
padding: 8px 12px;
|
|
982
|
+
background: #0f3460;
|
|
983
|
+
border-radius: 4px;
|
|
984
|
+
cursor: pointer;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.hotspot-item:hover { background: #1a4a7a; }
|
|
988
|
+
|
|
989
|
+
.hotspot-rank {
|
|
990
|
+
width: 24px;
|
|
991
|
+
height: 24px;
|
|
992
|
+
background: #e94560;
|
|
993
|
+
color: white;
|
|
994
|
+
border-radius: 50%;
|
|
995
|
+
display: flex;
|
|
996
|
+
align-items: center;
|
|
997
|
+
justify-content: center;
|
|
998
|
+
font-size: 12px;
|
|
999
|
+
font-weight: bold;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.hotspot-info { flex: 1; min-width: 0; }
|
|
1003
|
+
.hotspot-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1004
|
+
.hotspot-file { font-size: 11px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1005
|
+
.hotspot-value { font-size: 14px; color: #e94560; font-weight: 500; }
|
|
1006
|
+
|
|
1007
|
+
/* Gallery View */
|
|
1008
|
+
.gallery-filters {
|
|
1009
|
+
display: flex;
|
|
1010
|
+
gap: 8px;
|
|
1011
|
+
margin-bottom: 20px;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.gallery-grid {
|
|
1015
|
+
display: grid;
|
|
1016
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
1017
|
+
gap: 16px;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.gallery-card {
|
|
1021
|
+
background: #16213e;
|
|
1022
|
+
border-radius: 8px;
|
|
1023
|
+
padding: 16px;
|
|
1024
|
+
cursor: pointer;
|
|
1025
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.gallery-card:hover {
|
|
1029
|
+
transform: translateY(-2px);
|
|
1030
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.gallery-card-header {
|
|
1034
|
+
display: flex;
|
|
1035
|
+
align-items: center;
|
|
1036
|
+
gap: 8px;
|
|
1037
|
+
margin-bottom: 12px;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.gallery-type-badge {
|
|
1041
|
+
padding: 4px 8px;
|
|
1042
|
+
border-radius: 4px;
|
|
1043
|
+
font-size: 11px;
|
|
1044
|
+
font-weight: 600;
|
|
1045
|
+
text-transform: uppercase;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
.gallery-type-badge.decision { background: #16a34a; color: white; }
|
|
1049
|
+
.gallery-type-badge.pattern { background: #0d9488; color: white; }
|
|
1050
|
+
.gallery-type-badge.rejection { background: #dc2626; color: white; }
|
|
1051
|
+
|
|
1052
|
+
.gallery-timestamp {
|
|
1053
|
+
font-size: 11px;
|
|
1054
|
+
color: #666;
|
|
1055
|
+
margin-left: auto;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.gallery-content {
|
|
1059
|
+
font-size: 13px;
|
|
1060
|
+
line-height: 1.5;
|
|
1061
|
+
color: #ccc;
|
|
1062
|
+
margin-bottom: 12px;
|
|
1063
|
+
max-height: 80px;
|
|
1064
|
+
overflow: hidden;
|
|
1065
|
+
text-overflow: ellipsis;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.gallery-card.expanded .gallery-content {
|
|
1069
|
+
max-height: none;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.gallery-affected {
|
|
1073
|
+
display: flex;
|
|
1074
|
+
flex-wrap: wrap;
|
|
1075
|
+
gap: 6px;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.gallery-code-chip {
|
|
1079
|
+
padding: 4px 8px;
|
|
1080
|
+
background: #0f3460;
|
|
1081
|
+
border-radius: 4px;
|
|
1082
|
+
font-size: 11px;
|
|
1083
|
+
color: #aaa;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
.gallery-code-chip:hover { background: #1a4a7a; color: #fff; }
|
|
1087
|
+
|
|
1088
|
+
/* Timeline View */
|
|
1089
|
+
.timeline-container {
|
|
1090
|
+
display: flex;
|
|
1091
|
+
flex-direction: column;
|
|
1092
|
+
height: 100%;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.timeline-header {
|
|
1096
|
+
display: flex;
|
|
1097
|
+
justify-content: space-between;
|
|
1098
|
+
align-items: center;
|
|
1099
|
+
margin-bottom: 16px;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.timeline-scroll {
|
|
1103
|
+
flex: 1;
|
|
1104
|
+
overflow-x: auto;
|
|
1105
|
+
overflow-y: hidden;
|
|
1106
|
+
padding-bottom: 16px;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.timeline-track {
|
|
1110
|
+
display: flex;
|
|
1111
|
+
align-items: flex-end;
|
|
1112
|
+
min-width: max-content;
|
|
1113
|
+
padding: 0 20px;
|
|
1114
|
+
height: 300px;
|
|
1115
|
+
border-bottom: 2px solid #0f3460;
|
|
1116
|
+
position: relative;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.timeline-entry {
|
|
1120
|
+
display: flex;
|
|
1121
|
+
flex-direction: column;
|
|
1122
|
+
align-items: center;
|
|
1123
|
+
width: 60px;
|
|
1124
|
+
cursor: pointer;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.timeline-bar {
|
|
1128
|
+
width: 40px;
|
|
1129
|
+
background: #e94560;
|
|
1130
|
+
border-radius: 4px 4px 0 0;
|
|
1131
|
+
min-height: 20px;
|
|
1132
|
+
transition: background 0.2s;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.timeline-entry:hover .timeline-bar { background: #ff6b8a; }
|
|
1136
|
+
|
|
1137
|
+
.timeline-dot {
|
|
1138
|
+
width: 12px;
|
|
1139
|
+
height: 12px;
|
|
1140
|
+
background: #e94560;
|
|
1141
|
+
border-radius: 50%;
|
|
1142
|
+
margin-top: 8px;
|
|
1143
|
+
border: 2px solid #1a1a2e;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
.timeline-date {
|
|
1147
|
+
font-size: 10px;
|
|
1148
|
+
color: #666;
|
|
1149
|
+
margin-top: 8px;
|
|
1150
|
+
writing-mode: vertical-rl;
|
|
1151
|
+
text-orientation: mixed;
|
|
1152
|
+
transform: rotate(180deg);
|
|
1153
|
+
max-height: 60px;
|
|
1154
|
+
overflow: hidden;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.timeline-details {
|
|
1158
|
+
margin-top: 20px;
|
|
1159
|
+
background: #16213e;
|
|
1160
|
+
border-radius: 8px;
|
|
1161
|
+
padding: 16px;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/* Treemap View */
|
|
1165
|
+
#treemap-container {
|
|
1166
|
+
width: 100%;
|
|
1167
|
+
height: calc(100% - 50px);
|
|
1168
|
+
min-height: 400px;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.treemap-breadcrumb {
|
|
1172
|
+
display: flex;
|
|
1173
|
+
align-items: center;
|
|
1174
|
+
gap: 8px;
|
|
1175
|
+
margin-bottom: 12px;
|
|
1176
|
+
font-size: 13px;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.treemap-breadcrumb span {
|
|
1180
|
+
color: #888;
|
|
1181
|
+
cursor: pointer;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.treemap-breadcrumb span:hover { color: #e94560; }
|
|
1185
|
+
.treemap-breadcrumb span.current { color: #eee; cursor: default; }
|
|
1186
|
+
|
|
1187
|
+
.treemap-node {
|
|
1188
|
+
stroke: #1a1a2e;
|
|
1189
|
+
stroke-width: 1px;
|
|
1190
|
+
cursor: pointer;
|
|
1191
|
+
transition: opacity 0.2s;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.treemap-node:hover { opacity: 0.8; }
|
|
1195
|
+
|
|
1196
|
+
.treemap-label {
|
|
1197
|
+
font-size: 11px;
|
|
1198
|
+
fill: white;
|
|
1199
|
+
pointer-events: none;
|
|
1200
|
+
text-anchor: middle;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.treemap-tooltip {
|
|
1204
|
+
position: absolute;
|
|
1205
|
+
background: rgba(22, 33, 62, 0.95);
|
|
1206
|
+
border: 1px solid #0f3460;
|
|
1207
|
+
border-radius: 6px;
|
|
1208
|
+
padding: 8px 12px;
|
|
1209
|
+
font-size: 12px;
|
|
1210
|
+
pointer-events: none;
|
|
1211
|
+
z-index: 1000;
|
|
1212
|
+
}
|
|
1213
|
+
</style>
|
|
1214
|
+
</head>
|
|
1215
|
+
<body>
|
|
1216
|
+
<div class="header">
|
|
1217
|
+
<h1>aimem <span>Dashboard: ${escapeHtml(data.project.name)}</span></h1>
|
|
1218
|
+
<div class="search-box">
|
|
1219
|
+
<input type="text" id="search" placeholder="Search nodes...">
|
|
1220
|
+
<button class="visualize-search-btn" id="visualize-search-btn" title="Visualize search results as graph" style="display: none;">Visualize</button>
|
|
1221
|
+
<button class="back-btn" id="back-btn" title="Go back">← Back</button>
|
|
1222
|
+
<div class="view-toggle">
|
|
1223
|
+
<button class="view-toggle-btn active" data-mode="visual">Visual</button>
|
|
1224
|
+
<button class="view-toggle-btn" data-mode="list">List</button>
|
|
1225
|
+
</div>
|
|
1226
|
+
<button class="fullscreen-btn" id="fullscreen-btn" title="Toggle fullscreen">Fullscreen</button>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
|
|
1230
|
+
<div class="tabs">
|
|
1231
|
+
<div class="tab active" data-view="overview">Overview</div>
|
|
1232
|
+
<div class="tab" data-view="callGraph">Call Graph</div>
|
|
1233
|
+
<div class="tab" data-view="dependencies">Dependencies</div>
|
|
1234
|
+
<div class="tab" data-view="classes">Classes</div>
|
|
1235
|
+
<div class="tab" data-view="decisions">Decisions</div>
|
|
1236
|
+
<div class="tab" data-view="smells">Code Smells</div>
|
|
1237
|
+
<div class="tab" data-view="hotspots">Hotspots</div>
|
|
1238
|
+
<div class="tab" data-view="gallery">Gallery</div>
|
|
1239
|
+
<div class="tab" data-view="timeline">Timeline</div>
|
|
1240
|
+
<div class="tab" data-view="treemap">Treemap</div>
|
|
1241
|
+
</div>
|
|
1242
|
+
|
|
1243
|
+
<button class="exit-fullscreen" id="exit-fullscreen" style="display: none;">Exit Fullscreen (ESC)</button>
|
|
1244
|
+
|
|
1245
|
+
<div class="main">
|
|
1246
|
+
<div class="sidebar">
|
|
1247
|
+
<div class="filter-group">
|
|
1248
|
+
<h3>Filter by Type</h3>
|
|
1249
|
+
<div class="filter-item">
|
|
1250
|
+
<input type="checkbox" id="filter-function" checked>
|
|
1251
|
+
<label for="filter-function">Functions</label>
|
|
1252
|
+
<span class="count">${data.stats.byType['function'] || 0}</span>
|
|
1253
|
+
</div>
|
|
1254
|
+
<div class="filter-item">
|
|
1255
|
+
<input type="checkbox" id="filter-class" checked>
|
|
1256
|
+
<label for="filter-class">Classes</label>
|
|
1257
|
+
<span class="count">${data.stats.byType['class'] || 0}</span>
|
|
1258
|
+
</div>
|
|
1259
|
+
<div class="filter-item">
|
|
1260
|
+
<input type="checkbox" id="filter-method" checked>
|
|
1261
|
+
<label for="filter-method">Methods</label>
|
|
1262
|
+
<span class="count">${data.stats.byType['method'] || 0}</span>
|
|
1263
|
+
</div>
|
|
1264
|
+
<div class="filter-item">
|
|
1265
|
+
<input type="checkbox" id="filter-interface" checked>
|
|
1266
|
+
<label for="filter-interface">Interfaces</label>
|
|
1267
|
+
<span class="count">${data.stats.byType['interface'] || 0}</span>
|
|
1268
|
+
</div>
|
|
1269
|
+
<div class="filter-item">
|
|
1270
|
+
<input type="checkbox" id="filter-type" checked>
|
|
1271
|
+
<label for="filter-type">Types</label>
|
|
1272
|
+
<span class="count">${data.stats.byType['type'] || 0}</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
<div class="filter-item">
|
|
1275
|
+
<input type="checkbox" id="filter-file" checked>
|
|
1276
|
+
<label for="filter-file">Files</label>
|
|
1277
|
+
<span class="count">${data.stats.totalFiles}</span>
|
|
1278
|
+
</div>
|
|
1279
|
+
</div>
|
|
1280
|
+
|
|
1281
|
+
<div class="filter-group">
|
|
1282
|
+
<h3>Layout</h3>
|
|
1283
|
+
<select class="layout-select" id="layout-select">
|
|
1284
|
+
<option value="grid">Grid</option>
|
|
1285
|
+
<option value="cose">Force Directed</option>
|
|
1286
|
+
<option value="breadthfirst">Hierarchical</option>
|
|
1287
|
+
<option value="circle">Circular</option>
|
|
1288
|
+
<option value="concentric">Concentric</option>
|
|
1289
|
+
</select>
|
|
1290
|
+
</div>
|
|
1291
|
+
|
|
1292
|
+
<div class="filter-group" id="flow-mode-group" style="display: none;">
|
|
1293
|
+
<h3>Flow Mode</h3>
|
|
1294
|
+
<div class="filter-item">
|
|
1295
|
+
<input type="radio" name="flow-mode" id="flow-connections" value="connections" checked>
|
|
1296
|
+
<label for="flow-connections">Connections</label>
|
|
1297
|
+
</div>
|
|
1298
|
+
<div class="filter-item">
|
|
1299
|
+
<input type="radio" name="flow-mode" id="flow-downstream" value="downstream">
|
|
1300
|
+
<label for="flow-downstream">Downstream (calls)</label>
|
|
1301
|
+
</div>
|
|
1302
|
+
<div class="filter-item">
|
|
1303
|
+
<input type="radio" name="flow-mode" id="flow-upstream" value="upstream">
|
|
1304
|
+
<label for="flow-upstream">Upstream (callers)</label>
|
|
1305
|
+
</div>
|
|
1306
|
+
<p style="font-size: 11px; color: #666; margin-top: 8px;">Click a function to trace its flow</p>
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
|
|
1310
|
+
<div class="graph-container">
|
|
1311
|
+
<div class="breadcrumb" id="breadcrumb">
|
|
1312
|
+
<span class="breadcrumb-item current">Project</span>
|
|
1313
|
+
</div>
|
|
1314
|
+
<div class="view-description" id="view-description">
|
|
1315
|
+
<strong>Overview</strong> - All files in your codebase. Each node shows the file name and number of structures (functions, classes, etc.) it contains. Click a file to see details.
|
|
1316
|
+
</div>
|
|
1317
|
+
<div id="cy"></div>
|
|
1318
|
+
<div id="list-view"></div>
|
|
1319
|
+
|
|
1320
|
+
<!-- Custom Views (non-graph) -->
|
|
1321
|
+
<div id="smells-view" class="custom-view"></div>
|
|
1322
|
+
<div id="hotspots-view" class="custom-view"></div>
|
|
1323
|
+
<div id="gallery-view" class="custom-view"></div>
|
|
1324
|
+
<div id="timeline-view" class="custom-view"></div>
|
|
1325
|
+
<div id="treemap-view" class="custom-view"></div>
|
|
1326
|
+
|
|
1327
|
+
<!-- Legend -->
|
|
1328
|
+
<div class="legend" id="legend">
|
|
1329
|
+
<div class="legend-title">Legend</div>
|
|
1330
|
+
<div class="legend-item"><span class="legend-shape" style="background: #475569;"></span> File</div>
|
|
1331
|
+
<div class="legend-item"><span class="legend-dot" style="background: #2563eb;"></span> Function</div>
|
|
1332
|
+
<div class="legend-item"><span class="legend-dot" style="background: #7c3aed;"></span> Class</div>
|
|
1333
|
+
<div class="legend-item"><span class="legend-dot" style="background: #0891b2;"></span> Method</div>
|
|
1334
|
+
<div class="legend-item"><span class="legend-dot" style="background: #059669;"></span> Interface</div>
|
|
1335
|
+
</div>
|
|
1336
|
+
|
|
1337
|
+
<!-- Tooltip -->
|
|
1338
|
+
<div id="tooltip">
|
|
1339
|
+
<div class="tip-type"></div>
|
|
1340
|
+
<div class="tip-name"></div>
|
|
1341
|
+
<div class="tip-location"></div>
|
|
1342
|
+
<div class="tip-hint">Double-click to explore</div>
|
|
1343
|
+
</div>
|
|
1344
|
+
|
|
1345
|
+
<!-- Welcome overlay (shown on first visit) -->
|
|
1346
|
+
<div class="welcome-overlay" id="welcome" style="display: none;">
|
|
1347
|
+
<h2>Explore Your Code</h2>
|
|
1348
|
+
<p>Navigate your codebase like a map:</p>
|
|
1349
|
+
<div class="hint"><strong>Click</strong> - See details and source code</div>
|
|
1350
|
+
<div class="hint"><strong>Double-click</strong> - Dive deeper into that item</div>
|
|
1351
|
+
<div class="hint"><strong>Hover</strong> - See what is connected</div>
|
|
1352
|
+
<div class="hint"><strong>Search</strong> - Find anything by name</div>
|
|
1353
|
+
<button onclick="dismissWelcome()">Start Exploring</button>
|
|
1354
|
+
</div>
|
|
1355
|
+
|
|
1356
|
+
<div class="stats-bar">
|
|
1357
|
+
<div class="stat"><span class="stat-value">${data.stats.totalStructures}</span> structures</div>
|
|
1358
|
+
<div class="stat"><span class="stat-value">${data.stats.totalFiles}</span> files</div>
|
|
1359
|
+
<div class="stat"><span class="stat-value">${data.stats.totalLinks}</span> links</div>
|
|
1360
|
+
<div class="stat"><span class="stat-value">${data.stats.totalDecisions}</span> decisions</div>
|
|
1361
|
+
<div class="stat"><span class="stat-value">${data.stats.totalConversations}</span> conversations</div>
|
|
1362
|
+
</div>
|
|
1363
|
+
</div>
|
|
1364
|
+
|
|
1365
|
+
<div class="details-panel" id="details-panel">
|
|
1366
|
+
<div class="details-header">
|
|
1367
|
+
<h3>Details</h3>
|
|
1368
|
+
<button class="close-details" id="close-details" title="Close">×</button>
|
|
1369
|
+
</div>
|
|
1370
|
+
<div class="details-content" id="details-content">
|
|
1371
|
+
<div class="empty-state">
|
|
1372
|
+
<h3>No selection</h3>
|
|
1373
|
+
<p>Click a node to view details</p>
|
|
1374
|
+
</div>
|
|
1375
|
+
</div>
|
|
1376
|
+
</div>
|
|
1377
|
+
</div>
|
|
1378
|
+
|
|
1379
|
+
<script id="viz-data" type="application/json">${jsonData}</script>
|
|
1380
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
1381
|
+
<script>
|
|
1382
|
+
// Visualization data - parse from JSON script tag to avoid escaping issues
|
|
1383
|
+
const vizData = JSON.parse(document.getElementById('viz-data').textContent);
|
|
1384
|
+
|
|
1385
|
+
// Initialize Cytoscape
|
|
1386
|
+
let cy = null;
|
|
1387
|
+
let currentView = 'overview';
|
|
1388
|
+
let currentViewMode = 'visual'; // 'visual' or 'list'
|
|
1389
|
+
|
|
1390
|
+
// View descriptions - clear, action-oriented
|
|
1391
|
+
const viewDescriptions = {
|
|
1392
|
+
overview: '<strong>Files</strong> - Your codebase at a glance. Hover to see connections. <em>Double-click any file</em> to explore inside.',
|
|
1393
|
+
callGraph: '<strong>Call Graph</strong> - See how functions connect. Bigger nodes = more connections. <em>Double-click</em> to follow the flow.',
|
|
1394
|
+
dependencies: '<strong>Dependencies</strong> - File relationships. <em>Double-click</em> to see inside each file.',
|
|
1395
|
+
classes: '<strong>Classes</strong> - Your types and structures. <em>Double-click a class</em> to see its methods.',
|
|
1396
|
+
decisions: '<strong>Decisions</strong> - Why your code is the way it is. Connects decisions to the code they affect.',
|
|
1397
|
+
smells: '<strong>Code Smells</strong> - Potential issues detected in your codebase. Filter by type or severity.',
|
|
1398
|
+
hotspots: '<strong>Hotspots</strong> - The most complex and connected parts of your code. Focus refactoring efforts here.',
|
|
1399
|
+
gallery: '<strong>Gallery</strong> - Browse all decisions, patterns, and rejections from AI conversations.',
|
|
1400
|
+
timeline: '<strong>Timeline</strong> - See conversation activity over time. Click entries to see details.',
|
|
1401
|
+
treemap: '<strong>Treemap</strong> - Hierarchical view of your codebase by size. Click to zoom into directories.',
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
// Store full graph data for focus mode filtering
|
|
1405
|
+
let fullCallGraphData = null;
|
|
1406
|
+
|
|
1407
|
+
// Navigation history for back button
|
|
1408
|
+
let drillHistory = [];
|
|
1409
|
+
|
|
1410
|
+
// Flow mode: 'connections' (default) or 'downstream' or 'upstream'
|
|
1411
|
+
let flowMode = 'connections';
|
|
1412
|
+
|
|
1413
|
+
// Node colors
|
|
1414
|
+
const nodeColors = {
|
|
1415
|
+
function: '#2563eb',
|
|
1416
|
+
class: '#7c3aed',
|
|
1417
|
+
method: '#0891b2',
|
|
1418
|
+
interface: '#059669',
|
|
1419
|
+
type: '#d97706',
|
|
1420
|
+
variable: '#dc2626',
|
|
1421
|
+
module: '#4f46e5',
|
|
1422
|
+
file: '#475569',
|
|
1423
|
+
decision: '#16a34a',
|
|
1424
|
+
pattern: '#0d9488',
|
|
1425
|
+
rejection: '#dc2626',
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
// Cytoscape styles - node size based on weight (connections)
|
|
1429
|
+
const cyStyle = [
|
|
1430
|
+
{
|
|
1431
|
+
selector: 'node',
|
|
1432
|
+
style: {
|
|
1433
|
+
'label': 'data(label)',
|
|
1434
|
+
'text-valign': 'bottom',
|
|
1435
|
+
'text-halign': 'center',
|
|
1436
|
+
'font-size': '10px',
|
|
1437
|
+
'color': '#ccc',
|
|
1438
|
+
'text-margin-y': 4,
|
|
1439
|
+
'background-color': '#475569',
|
|
1440
|
+
'width': 'mapData(weight, 0, 10, 25, 60)', // Size based on connections
|
|
1441
|
+
'height': 'mapData(weight, 0, 10, 25, 60)',
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
selector: 'node[type="function"]',
|
|
1446
|
+
style: { 'background-color': nodeColors.function }
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
selector: 'node[type="class"]',
|
|
1450
|
+
style: { 'background-color': nodeColors.class, 'width': 40, 'height': 40 }
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
selector: 'node[type="method"]',
|
|
1454
|
+
style: { 'background-color': nodeColors.method }
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
selector: 'node[type="interface"]',
|
|
1458
|
+
style: { 'background-color': nodeColors.interface }
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
selector: 'node[type="type"]',
|
|
1462
|
+
style: { 'background-color': nodeColors.type }
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
selector: 'node[type="file"]',
|
|
1466
|
+
style: {
|
|
1467
|
+
'background-color': nodeColors.file,
|
|
1468
|
+
'width': 50,
|
|
1469
|
+
'height': 30,
|
|
1470
|
+
'shape': 'round-rectangle',
|
|
1471
|
+
'font-size': '11px',
|
|
1472
|
+
'text-wrap': 'ellipsis',
|
|
1473
|
+
'text-max-width': '80px',
|
|
1474
|
+
}
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
selector: 'node[type="decision"]',
|
|
1478
|
+
style: { 'background-color': nodeColors.decision, 'shape': 'diamond' }
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
selector: 'node[type="pattern"]',
|
|
1482
|
+
style: { 'background-color': nodeColors.pattern, 'shape': 'diamond' }
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
selector: 'node[type="rejection"]',
|
|
1486
|
+
style: { 'background-color': nodeColors.rejection, 'shape': 'diamond' }
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
selector: 'edge',
|
|
1490
|
+
style: {
|
|
1491
|
+
'width': 1.5,
|
|
1492
|
+
'line-color': '#334155',
|
|
1493
|
+
'target-arrow-color': '#334155',
|
|
1494
|
+
'target-arrow-shape': 'triangle',
|
|
1495
|
+
'curve-style': 'bezier',
|
|
1496
|
+
'arrow-scale': 0.8,
|
|
1497
|
+
}
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
selector: 'edge[type="calls"]',
|
|
1501
|
+
style: { 'line-color': '#2563eb', 'target-arrow-color': '#2563eb' }
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
selector: 'edge[type="contains"]',
|
|
1505
|
+
style: { 'line-style': 'dashed', 'line-color': '#475569', 'target-arrow-shape': 'none' }
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
selector: 'edge[type="decision"]',
|
|
1509
|
+
style: { 'line-color': '#16a34a', 'target-arrow-color': '#16a34a' }
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
selector: ':selected',
|
|
1513
|
+
style: {
|
|
1514
|
+
'border-width': 3,
|
|
1515
|
+
'border-color': '#e94560',
|
|
1516
|
+
}
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
selector: 'node.hover',
|
|
1520
|
+
style: {
|
|
1521
|
+
'border-width': 2,
|
|
1522
|
+
'border-color': '#e94560',
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
selector: 'node.faded',
|
|
1527
|
+
style: {
|
|
1528
|
+
'opacity': 0.2,
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
selector: 'edge.highlighted',
|
|
1533
|
+
style: {
|
|
1534
|
+
'width': 3,
|
|
1535
|
+
'line-color': '#e94560',
|
|
1536
|
+
'target-arrow-color': '#e94560',
|
|
1537
|
+
'z-index': 999,
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
selector: 'edge.faded',
|
|
1542
|
+
style: {
|
|
1543
|
+
'opacity': 0.1,
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
];
|
|
1547
|
+
|
|
1548
|
+
function destroyCy() {
|
|
1549
|
+
if (cy) {
|
|
1550
|
+
cy.removeAllListeners();
|
|
1551
|
+
cy.destroy();
|
|
1552
|
+
cy = null;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function initCytoscape(graphData) {
|
|
1557
|
+
destroyCy();
|
|
1558
|
+
|
|
1559
|
+
const container = document.getElementById('cy');
|
|
1560
|
+
if (!container) return;
|
|
1561
|
+
|
|
1562
|
+
// Clear any previous content
|
|
1563
|
+
container.innerHTML = '';
|
|
1564
|
+
|
|
1565
|
+
// Ensure all nodes have a weight for sizing
|
|
1566
|
+
const nodesWithWeight = graphData.nodes.map(n => ({
|
|
1567
|
+
...n,
|
|
1568
|
+
data: {
|
|
1569
|
+
...n.data,
|
|
1570
|
+
weight: n.data.weight || 1
|
|
1571
|
+
}
|
|
1572
|
+
}));
|
|
1573
|
+
|
|
1574
|
+
cy = cytoscape({
|
|
1575
|
+
container: container,
|
|
1576
|
+
elements: [...nodesWithWeight, ...graphData.edges],
|
|
1577
|
+
style: cyStyle,
|
|
1578
|
+
layout: { name: 'cose', animate: false, randomize: true },
|
|
1579
|
+
minZoom: 0.1,
|
|
1580
|
+
maxZoom: 3,
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
// Click handler
|
|
1584
|
+
cy.on('tap', 'node', function(evt) {
|
|
1585
|
+
const node = evt.target;
|
|
1586
|
+
const nodeData = node.data();
|
|
1587
|
+
showDetails(nodeData);
|
|
1588
|
+
|
|
1589
|
+
// In call graph, handle flow mode
|
|
1590
|
+
if (currentView === 'callGraph' && fullCallGraphData && nodeData.id) {
|
|
1591
|
+
setTimeout(() => {
|
|
1592
|
+
if (node.selected()) {
|
|
1593
|
+
if (flowMode === 'downstream') {
|
|
1594
|
+
// Save history before tracing
|
|
1595
|
+
drillHistory.push({
|
|
1596
|
+
view: currentView,
|
|
1597
|
+
label: 'Call Graph',
|
|
1598
|
+
description: document.getElementById('view-description').innerHTML
|
|
1599
|
+
});
|
|
1600
|
+
updateBackButton();
|
|
1601
|
+
updateBreadcrumb([{ label: 'Call Graph' }, { label: nodeData.label + ' (downstream)' }]);
|
|
1602
|
+
traceFlow(nodeData.id, 'downstream');
|
|
1603
|
+
} else if (flowMode === 'upstream') {
|
|
1604
|
+
// Save history before tracing
|
|
1605
|
+
drillHistory.push({
|
|
1606
|
+
view: currentView,
|
|
1607
|
+
label: 'Call Graph',
|
|
1608
|
+
description: document.getElementById('view-description').innerHTML
|
|
1609
|
+
});
|
|
1610
|
+
updateBackButton();
|
|
1611
|
+
updateBreadcrumb([{ label: 'Call Graph' }, { label: nodeData.label + ' (upstream)' }]);
|
|
1612
|
+
traceFlow(nodeData.id, 'upstream');
|
|
1613
|
+
} else {
|
|
1614
|
+
// Default: focus on connections
|
|
1615
|
+
focusOnNode(nodeData.id);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}, 300);
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// Double-click to drill down
|
|
1623
|
+
cy.on('dbltap', 'node', function(evt) {
|
|
1624
|
+
const nodeData = evt.target.data();
|
|
1625
|
+
drillDown(nodeData);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
// Clear selection on background click
|
|
1629
|
+
cy.on('tap', function(evt) {
|
|
1630
|
+
if (evt.target === cy) {
|
|
1631
|
+
clearDetails();
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
// Hover effects - show tooltip and highlight connections
|
|
1636
|
+
cy.on('mouseover', 'node', function(evt) {
|
|
1637
|
+
const node = evt.target;
|
|
1638
|
+
const nodeData = node.data();
|
|
1639
|
+
|
|
1640
|
+
// Show tooltip
|
|
1641
|
+
showTooltip(evt.originalEvent, nodeData);
|
|
1642
|
+
|
|
1643
|
+
// Highlight this node and its connections
|
|
1644
|
+
node.addClass('hover');
|
|
1645
|
+
|
|
1646
|
+
// Get connected edges and nodes
|
|
1647
|
+
const connectedEdges = node.connectedEdges();
|
|
1648
|
+
const connectedNodes = connectedEdges.connectedNodes();
|
|
1649
|
+
|
|
1650
|
+
// Fade everything else
|
|
1651
|
+
cy.elements().addClass('faded');
|
|
1652
|
+
node.removeClass('faded');
|
|
1653
|
+
connectedNodes.removeClass('faded');
|
|
1654
|
+
connectedEdges.removeClass('faded').addClass('highlighted');
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
cy.on('mouseout', 'node', function(evt) {
|
|
1658
|
+
hideTooltip();
|
|
1659
|
+
|
|
1660
|
+
// Remove all highlight classes
|
|
1661
|
+
cy.elements().removeClass('faded hover highlighted');
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
// Update tooltip position on mouse move
|
|
1665
|
+
cy.on('mousemove', 'node', function(evt) {
|
|
1666
|
+
moveTooltip(evt.originalEvent);
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
runLayout();
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function runLayout() {
|
|
1673
|
+
if (!cy) return;
|
|
1674
|
+
const layoutName = document.getElementById('layout-select').value;
|
|
1675
|
+
const nodeCount = cy.nodes(':visible').length;
|
|
1676
|
+
|
|
1677
|
+
const layoutOptions = {
|
|
1678
|
+
name: layoutName,
|
|
1679
|
+
animate: nodeCount < 100,
|
|
1680
|
+
animationDuration: 300,
|
|
1681
|
+
nodeDimensionsIncludeLabels: true,
|
|
1682
|
+
fit: true,
|
|
1683
|
+
padding: 50,
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
// Layout-specific options for better spacing
|
|
1687
|
+
if (layoutName === 'cose') {
|
|
1688
|
+
Object.assign(layoutOptions, {
|
|
1689
|
+
nodeRepulsion: 8000,
|
|
1690
|
+
idealEdgeLength: 100,
|
|
1691
|
+
edgeElasticity: 100,
|
|
1692
|
+
nestingFactor: 1.2,
|
|
1693
|
+
gravity: 0.25,
|
|
1694
|
+
numIter: 1000,
|
|
1695
|
+
randomize: true,
|
|
1696
|
+
});
|
|
1697
|
+
} else if (layoutName === 'breadthfirst') {
|
|
1698
|
+
Object.assign(layoutOptions, {
|
|
1699
|
+
spacingFactor: 1.5,
|
|
1700
|
+
directed: true,
|
|
1701
|
+
});
|
|
1702
|
+
} else if (layoutName === 'circle') {
|
|
1703
|
+
Object.assign(layoutOptions, {
|
|
1704
|
+
spacingFactor: 1.2,
|
|
1705
|
+
});
|
|
1706
|
+
} else if (layoutName === 'grid') {
|
|
1707
|
+
Object.assign(layoutOptions, {
|
|
1708
|
+
spacingFactor: 1.5,
|
|
1709
|
+
condense: false,
|
|
1710
|
+
});
|
|
1711
|
+
} else if (layoutName === 'concentric') {
|
|
1712
|
+
Object.assign(layoutOptions, {
|
|
1713
|
+
spacingFactor: 2,
|
|
1714
|
+
minNodeSpacing: 50,
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
cy.layout(layoutOptions).run();
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Store current selected node data for visualization buttons
|
|
1722
|
+
let currentSelectedData = null;
|
|
1723
|
+
|
|
1724
|
+
function showDetails(data) {
|
|
1725
|
+
currentSelectedData = data;
|
|
1726
|
+
const content = document.getElementById('details-content');
|
|
1727
|
+
let html = '';
|
|
1728
|
+
|
|
1729
|
+
// Type tag
|
|
1730
|
+
html += '<div class="detail-row">';
|
|
1731
|
+
html += '<span class="tag ' + data.type + '">' + data.type + '</span>';
|
|
1732
|
+
html += '</div>';
|
|
1733
|
+
|
|
1734
|
+
// Name
|
|
1735
|
+
html += '<div class="detail-row">';
|
|
1736
|
+
html += '<div class="detail-label">Name</div>';
|
|
1737
|
+
html += '<div class="detail-value">' + escapeHtml(data.label) + '</div>';
|
|
1738
|
+
html += '</div>';
|
|
1739
|
+
|
|
1740
|
+
// File location
|
|
1741
|
+
if (data.file) {
|
|
1742
|
+
html += '<div class="detail-row">';
|
|
1743
|
+
html += '<div class="detail-label">Location</div>';
|
|
1744
|
+
html += '<div class="detail-value">' + escapeHtml(data.file);
|
|
1745
|
+
if (data.line) {
|
|
1746
|
+
html += ':' + data.line;
|
|
1747
|
+
}
|
|
1748
|
+
html += '</div>';
|
|
1749
|
+
html += '</div>';
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Signature
|
|
1753
|
+
if (data.signature) {
|
|
1754
|
+
html += '<div class="detail-row">';
|
|
1755
|
+
html += '<div class="detail-label">Signature</div>';
|
|
1756
|
+
html += '<div class="detail-value"><code>' + escapeHtml(data.signature) + '</code></div>';
|
|
1757
|
+
html += '</div>';
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Visualize buttons for functions and methods
|
|
1761
|
+
if (data.type === 'function' || data.type === 'method') {
|
|
1762
|
+
html += '<div class="detail-row">';
|
|
1763
|
+
html += '<div class="detail-label">Visualize</div>';
|
|
1764
|
+
html += '<button class="visualize-btn" onclick="visualizeNode(\\'connections\\')">Show Connections</button>';
|
|
1765
|
+
html += '<div class="visualize-btn-group">';
|
|
1766
|
+
html += '<button class="visualize-btn secondary" onclick="visualizeNode(\\'downstream\\')">Trace Calls →</button>';
|
|
1767
|
+
html += '<button class="visualize-btn secondary" onclick="visualizeNode(\\'upstream\\')">← Trace Callers</button>';
|
|
1768
|
+
html += '</div>';
|
|
1769
|
+
html += '</div>';
|
|
1770
|
+
}
|
|
1771
|
+
// For files: show contents button
|
|
1772
|
+
else if (data.type === 'file') {
|
|
1773
|
+
html += '<div class="detail-row">';
|
|
1774
|
+
html += '<button class="visualize-btn" onclick="visualizeFile()">Show File Contents</button>';
|
|
1775
|
+
html += '</div>';
|
|
1776
|
+
}
|
|
1777
|
+
// For classes/interfaces: show methods button
|
|
1778
|
+
else if (data.type === 'class' || data.type === 'interface') {
|
|
1779
|
+
html += '<div class="detail-row">';
|
|
1780
|
+
html += '<button class="visualize-btn" onclick="visualizeClass()">Show Methods</button>';
|
|
1781
|
+
html += '</div>';
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Source code
|
|
1785
|
+
if (data.content) {
|
|
1786
|
+
html += '<div class="detail-row">';
|
|
1787
|
+
html += '<div class="detail-label">Source Code</div>';
|
|
1788
|
+
html += '<div class="code-block">' + escapeHtml(data.content) + '</div>';
|
|
1789
|
+
html += '</div>';
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
content.innerHTML = html;
|
|
1793
|
+
|
|
1794
|
+
// Show panel in fullscreen mode
|
|
1795
|
+
if (document.body.classList.contains('fullscreen')) {
|
|
1796
|
+
document.getElementById('details-panel').classList.add('visible');
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// Visualize a node in the call graph
|
|
1801
|
+
function visualizeNode(mode) {
|
|
1802
|
+
if (!currentSelectedData) return;
|
|
1803
|
+
|
|
1804
|
+
// Switch to visual mode if in list mode
|
|
1805
|
+
if (currentViewMode === 'list') {
|
|
1806
|
+
setViewMode('visual');
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Make sure we have call graph data
|
|
1810
|
+
fullCallGraphData = vizData.graphs.callGraph;
|
|
1811
|
+
|
|
1812
|
+
// Save current state to history
|
|
1813
|
+
drillHistory.push({
|
|
1814
|
+
view: currentView,
|
|
1815
|
+
label: getBreadcrumbLabel(currentView),
|
|
1816
|
+
description: document.getElementById('view-description').innerHTML
|
|
1817
|
+
});
|
|
1818
|
+
updateBackButton();
|
|
1819
|
+
|
|
1820
|
+
// Set flow mode and update UI
|
|
1821
|
+
flowMode = mode;
|
|
1822
|
+
document.querySelectorAll('input[name="flow-mode"]').forEach(radio => {
|
|
1823
|
+
radio.checked = radio.value === mode;
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
// Switch to call graph tab
|
|
1827
|
+
currentView = 'callGraph';
|
|
1828
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
1829
|
+
tab.classList.toggle('active', tab.dataset.view === 'callGraph');
|
|
1830
|
+
});
|
|
1831
|
+
document.getElementById('flow-mode-group').style.display = 'block';
|
|
1832
|
+
|
|
1833
|
+
// Perform the visualization
|
|
1834
|
+
if (mode === 'downstream') {
|
|
1835
|
+
updateBreadcrumb([{ label: 'Call Graph' }, { label: currentSelectedData.label + ' (downstream)' }]);
|
|
1836
|
+
traceFlow(currentSelectedData.id, 'downstream');
|
|
1837
|
+
} else if (mode === 'upstream') {
|
|
1838
|
+
updateBreadcrumb([{ label: 'Call Graph' }, { label: currentSelectedData.label + ' (upstream)' }]);
|
|
1839
|
+
traceFlow(currentSelectedData.id, 'upstream');
|
|
1840
|
+
} else {
|
|
1841
|
+
updateBreadcrumb([{ label: 'Call Graph' }, { label: currentSelectedData.label }]);
|
|
1842
|
+
focusOnNode(currentSelectedData.id);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Visualize file contents
|
|
1847
|
+
function visualizeFile() {
|
|
1848
|
+
if (!currentSelectedData || !currentSelectedData.file) return;
|
|
1849
|
+
|
|
1850
|
+
// Switch to visual mode if in list mode
|
|
1851
|
+
if (currentViewMode === 'list') {
|
|
1852
|
+
setViewMode('visual');
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
drillDown(currentSelectedData);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Visualize class methods
|
|
1859
|
+
function visualizeClass() {
|
|
1860
|
+
if (!currentSelectedData) return;
|
|
1861
|
+
|
|
1862
|
+
// Switch to visual mode if in list mode
|
|
1863
|
+
if (currentViewMode === 'list') {
|
|
1864
|
+
setViewMode('visual');
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
drillDown(currentSelectedData);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function clearDetails() {
|
|
1871
|
+
document.getElementById('details-content').innerHTML = \`
|
|
1872
|
+
<div class="empty-state">
|
|
1873
|
+
<h3>No selection</h3>
|
|
1874
|
+
<p>Click a node to view details</p>
|
|
1875
|
+
</div>
|
|
1876
|
+
\`;
|
|
1877
|
+
// Hide panel in fullscreen mode
|
|
1878
|
+
document.getElementById('details-panel').classList.remove('visible');
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function escapeHtml(text) {
|
|
1882
|
+
if (!text) return '';
|
|
1883
|
+
const div = document.createElement('div');
|
|
1884
|
+
div.textContent = text;
|
|
1885
|
+
return div.innerHTML;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Tooltip functions
|
|
1889
|
+
function showTooltip(event, nodeData) {
|
|
1890
|
+
const tooltip = document.getElementById('tooltip');
|
|
1891
|
+
tooltip.querySelector('.tip-type').textContent = nodeData.type || '';
|
|
1892
|
+
tooltip.querySelector('.tip-name').textContent = nodeData.label || '';
|
|
1893
|
+
|
|
1894
|
+
let location = '';
|
|
1895
|
+
if (nodeData.file) {
|
|
1896
|
+
location = nodeData.file.split('/').pop();
|
|
1897
|
+
if (nodeData.line) location += ':' + nodeData.line;
|
|
1898
|
+
}
|
|
1899
|
+
tooltip.querySelector('.tip-location').textContent = location;
|
|
1900
|
+
|
|
1901
|
+
// Update hint based on type
|
|
1902
|
+
const hint = tooltip.querySelector('.tip-hint');
|
|
1903
|
+
if (nodeData.type === 'file') {
|
|
1904
|
+
hint.textContent = 'Double-click to see contents';
|
|
1905
|
+
} else if (nodeData.type === 'class' || nodeData.type === 'interface') {
|
|
1906
|
+
hint.textContent = 'Double-click to see methods';
|
|
1907
|
+
} else if (nodeData.type === 'function' || nodeData.type === 'method') {
|
|
1908
|
+
hint.textContent = 'Double-click to see call graph';
|
|
1909
|
+
} else {
|
|
1910
|
+
hint.textContent = 'Double-click to explore';
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
tooltip.classList.add('visible');
|
|
1914
|
+
moveTooltip(event);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function moveTooltip(event) {
|
|
1918
|
+
const tooltip = document.getElementById('tooltip');
|
|
1919
|
+
const x = event.clientX + 15;
|
|
1920
|
+
const y = event.clientY + 15;
|
|
1921
|
+
|
|
1922
|
+
// Keep tooltip on screen
|
|
1923
|
+
const rect = tooltip.getBoundingClientRect();
|
|
1924
|
+
const maxX = window.innerWidth - rect.width - 10;
|
|
1925
|
+
const maxY = window.innerHeight - rect.height - 10;
|
|
1926
|
+
|
|
1927
|
+
tooltip.style.left = Math.min(x, maxX) + 'px';
|
|
1928
|
+
tooltip.style.top = Math.min(y, maxY) + 'px';
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function hideTooltip() {
|
|
1932
|
+
document.getElementById('tooltip').classList.remove('visible');
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// Breadcrumb functions
|
|
1936
|
+
function updateBreadcrumb(items) {
|
|
1937
|
+
const bc = document.getElementById('breadcrumb');
|
|
1938
|
+
let html = '';
|
|
1939
|
+
|
|
1940
|
+
items.forEach((item, i) => {
|
|
1941
|
+
if (i > 0) html += '<span class="breadcrumb-sep">›</span>';
|
|
1942
|
+
|
|
1943
|
+
const isLast = i === items.length - 1;
|
|
1944
|
+
if (isLast) {
|
|
1945
|
+
html += '<span class="breadcrumb-item current">' + escapeHtml(item.label) + '</span>';
|
|
1946
|
+
} else {
|
|
1947
|
+
html += '<span class="breadcrumb-item" onclick="goBackTo(' + i + ')">' + escapeHtml(item.label) + '</span>';
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
bc.innerHTML = html;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
function goBackTo(index) {
|
|
1955
|
+
// Pop history until we reach the target index
|
|
1956
|
+
while (drillHistory.length > index) {
|
|
1957
|
+
drillHistory.pop();
|
|
1958
|
+
}
|
|
1959
|
+
updateBackButton();
|
|
1960
|
+
|
|
1961
|
+
if (drillHistory.length === 0) {
|
|
1962
|
+
switchView(currentView);
|
|
1963
|
+
} else {
|
|
1964
|
+
const target = drillHistory[drillHistory.length - 1];
|
|
1965
|
+
drillHistory.pop();
|
|
1966
|
+
switchView(target.view);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Welcome overlay
|
|
1971
|
+
function dismissWelcome() {
|
|
1972
|
+
document.getElementById('welcome').style.display = 'none';
|
|
1973
|
+
localStorage.setItem('aimem-welcome-dismissed', 'true');
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function maybeShowWelcome() {
|
|
1977
|
+
if (!localStorage.getItem('aimem-welcome-dismissed')) {
|
|
1978
|
+
document.getElementById('welcome').style.display = 'block';
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function switchView(view) {
|
|
1983
|
+
currentView = view;
|
|
1984
|
+
|
|
1985
|
+
// Update tabs
|
|
1986
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
1987
|
+
tab.classList.toggle('active', tab.dataset.view === view);
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Update description
|
|
1991
|
+
document.getElementById('view-description').innerHTML = viewDescriptions[view] || '';
|
|
1992
|
+
|
|
1993
|
+
// Show/hide flow mode controls for Call Graph
|
|
1994
|
+
const flowModeGroup = document.getElementById('flow-mode-group');
|
|
1995
|
+
if (view === 'callGraph') {
|
|
1996
|
+
flowModeGroup.style.display = 'block';
|
|
1997
|
+
} else {
|
|
1998
|
+
flowModeGroup.style.display = 'none';
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// Hide all custom views
|
|
2002
|
+
const customViews = ['smells', 'hotspots', 'gallery', 'timeline', 'treemap'];
|
|
2003
|
+
customViews.forEach(v => {
|
|
2004
|
+
const el = document.getElementById(v + '-view');
|
|
2005
|
+
if (el) el.classList.remove('active');
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
// Show/hide cy and list-view based on view type
|
|
2009
|
+
const isCustomView = customViews.includes(view);
|
|
2010
|
+
document.getElementById('cy').style.display = isCustomView ? 'none' : 'block';
|
|
2011
|
+
document.getElementById('list-view').style.display = isCustomView ? 'none' : (currentViewMode === 'list' ? 'block' : 'none');
|
|
2012
|
+
document.getElementById('legend').style.display = isCustomView ? 'none' : 'block';
|
|
2013
|
+
|
|
2014
|
+
// Handle custom views
|
|
2015
|
+
if (isCustomView) {
|
|
2016
|
+
destroyCy();
|
|
2017
|
+
const customViewEl = document.getElementById(view + '-view');
|
|
2018
|
+
if (customViewEl) customViewEl.classList.add('active');
|
|
2019
|
+
|
|
2020
|
+
if (view === 'smells') renderSmellsView();
|
|
2021
|
+
else if (view === 'hotspots') renderHotspotsView();
|
|
2022
|
+
else if (view === 'gallery') renderGalleryView();
|
|
2023
|
+
else if (view === 'timeline') renderTimelineView();
|
|
2024
|
+
else if (view === 'treemap') renderTreemapView();
|
|
2025
|
+
|
|
2026
|
+
clearDetails();
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Special handling for call graph - focus mode
|
|
2031
|
+
if (view === 'callGraph') {
|
|
2032
|
+
fullCallGraphData = vizData.graphs.callGraph;
|
|
2033
|
+
destroyCy();
|
|
2034
|
+
|
|
2035
|
+
// Find entry points: functions that call others but aren't called themselves
|
|
2036
|
+
const entryPoints = findEntryPoints();
|
|
2037
|
+
|
|
2038
|
+
if (entryPoints.length > 0) {
|
|
2039
|
+
// Show entry points view
|
|
2040
|
+
showEntryPoints(entryPoints);
|
|
2041
|
+
} else {
|
|
2042
|
+
const stats = fullCallGraphData ? fullCallGraphData.nodes.length + ' functions, ' + fullCallGraphData.edges.length + ' call relationships' : 'No data';
|
|
2043
|
+
document.getElementById('cy').innerHTML =
|
|
2044
|
+
'<div class="empty-state">' +
|
|
2045
|
+
'<h3>Search for a function</h3>' +
|
|
2046
|
+
'<p>Type a function name in the search box above to explore its call relationships</p>' +
|
|
2047
|
+
'<p style="margin-top: 16px; font-size: 12px; color: #666;">' + stats + '</p>' +
|
|
2048
|
+
'</div>';
|
|
2049
|
+
}
|
|
2050
|
+
// Re-render list view if in list mode
|
|
2051
|
+
if (currentViewMode === 'list') {
|
|
2052
|
+
renderListView();
|
|
2053
|
+
}
|
|
2054
|
+
clearDetails();
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
fullCallGraphData = null;
|
|
2059
|
+
|
|
2060
|
+
// Load graph data
|
|
2061
|
+
const graphData = vizData.graphs[view];
|
|
2062
|
+
if (graphData && (graphData.nodes.length > 0 || graphData.edges.length > 0)) {
|
|
2063
|
+
initCytoscape(graphData);
|
|
2064
|
+
} else {
|
|
2065
|
+
destroyCy();
|
|
2066
|
+
document.getElementById('cy').innerHTML = \`
|
|
2067
|
+
<div class="empty-state">
|
|
2068
|
+
<h3>No data</h3>
|
|
2069
|
+
<p>No nodes to display for this view</p>
|
|
2070
|
+
</div>
|
|
2071
|
+
\`;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Re-render list view if in list mode
|
|
2075
|
+
if (currentViewMode === 'list') {
|
|
2076
|
+
renderListView();
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
clearDetails();
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// Find entry points: functions that call others but aren't called
|
|
2083
|
+
function findEntryPoints() {
|
|
2084
|
+
if (!fullCallGraphData || !fullCallGraphData.edges.length) return [];
|
|
2085
|
+
|
|
2086
|
+
const callers = new Set(); // nodes that call something
|
|
2087
|
+
const callees = new Set(); // nodes that are called
|
|
2088
|
+
|
|
2089
|
+
for (const edge of fullCallGraphData.edges) {
|
|
2090
|
+
callers.add(edge.data.source);
|
|
2091
|
+
callees.add(edge.data.target);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// Entry points: call others but aren't called
|
|
2095
|
+
const entryPointIds = [];
|
|
2096
|
+
for (const callerId of callers) {
|
|
2097
|
+
if (!callees.has(callerId)) {
|
|
2098
|
+
entryPointIds.push(callerId);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Get the actual nodes, sorted by number of outgoing calls
|
|
2103
|
+
const outgoingCounts = {};
|
|
2104
|
+
for (const edge of fullCallGraphData.edges) {
|
|
2105
|
+
outgoingCounts[edge.data.source] = (outgoingCounts[edge.data.source] || 0) + 1;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
return fullCallGraphData.nodes
|
|
2109
|
+
.filter(n => entryPointIds.includes(n.data.id))
|
|
2110
|
+
.sort((a, b) => (outgoingCounts[b.data.id] || 0) - (outgoingCounts[a.data.id] || 0));
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// Show entry points with their immediate callees
|
|
2114
|
+
function showEntryPoints(entryPoints) {
|
|
2115
|
+
// Take top entry points (most outgoing calls)
|
|
2116
|
+
const topEntries = entryPoints.slice(0, 10);
|
|
2117
|
+
const entryIds = new Set(topEntries.map(n => n.data.id));
|
|
2118
|
+
|
|
2119
|
+
// Get edges from entry points
|
|
2120
|
+
const relevantEdges = fullCallGraphData.edges.filter(e => entryIds.has(e.data.source));
|
|
2121
|
+
|
|
2122
|
+
// Get called nodes
|
|
2123
|
+
const calledIds = new Set();
|
|
2124
|
+
for (const edge of relevantEdges) {
|
|
2125
|
+
calledIds.add(edge.data.target);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Build node set
|
|
2129
|
+
const nodeIds = new Set([...entryIds, ...calledIds]);
|
|
2130
|
+
const nodes = fullCallGraphData.nodes.filter(n => nodeIds.has(n.data.id));
|
|
2131
|
+
|
|
2132
|
+
if (nodes.length > 0) {
|
|
2133
|
+
initCytoscape({ nodes, edges: relevantEdges });
|
|
2134
|
+
|
|
2135
|
+
// Update description
|
|
2136
|
+
document.getElementById('view-description').innerHTML =
|
|
2137
|
+
'<strong>Entry Points</strong> - Functions that call others but are not called. These are typically command handlers or main functions. Click any node to explore further.';
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// Focus mode: show only a node and its direct connections
|
|
2142
|
+
function focusOnNode(nodeId) {
|
|
2143
|
+
if (!fullCallGraphData) return;
|
|
2144
|
+
|
|
2145
|
+
const nodeIdStr = nodeId.startsWith('structure:') ? nodeId : 'structure:' + nodeId;
|
|
2146
|
+
|
|
2147
|
+
// Find the center node
|
|
2148
|
+
const centerNode = fullCallGraphData.nodes.find(n => n.data.id === nodeIdStr);
|
|
2149
|
+
if (!centerNode) return;
|
|
2150
|
+
|
|
2151
|
+
// Find all edges connected to this node
|
|
2152
|
+
const connectedEdges = fullCallGraphData.edges.filter(e =>
|
|
2153
|
+
e.data.source === nodeIdStr || e.data.target === nodeIdStr
|
|
2154
|
+
);
|
|
2155
|
+
|
|
2156
|
+
// Find all connected node IDs
|
|
2157
|
+
const connectedNodeIds = new Set([nodeIdStr]);
|
|
2158
|
+
for (const edge of connectedEdges) {
|
|
2159
|
+
connectedNodeIds.add(edge.data.source);
|
|
2160
|
+
connectedNodeIds.add(edge.data.target);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// Build focused graph
|
|
2164
|
+
const focusedNodes = fullCallGraphData.nodes.filter(n => connectedNodeIds.has(n.data.id));
|
|
2165
|
+
const focusedGraph = {
|
|
2166
|
+
nodes: focusedNodes,
|
|
2167
|
+
edges: connectedEdges,
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
if (focusedNodes.length > 0) {
|
|
2171
|
+
initCytoscape(focusedGraph);
|
|
2172
|
+
|
|
2173
|
+
// Highlight the center node
|
|
2174
|
+
setTimeout(() => {
|
|
2175
|
+
if (cy) {
|
|
2176
|
+
const centerEle = cy.getElementById(nodeIdStr);
|
|
2177
|
+
if (centerEle) {
|
|
2178
|
+
centerEle.select();
|
|
2179
|
+
showDetails(centerEle.data());
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}, 100);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// Trace code flow from a starting node
|
|
2187
|
+
function traceFlow(nodeId, direction) {
|
|
2188
|
+
if (!fullCallGraphData) return;
|
|
2189
|
+
|
|
2190
|
+
const nodeIdStr = nodeId.startsWith('structure:') ? nodeId : 'structure:' + nodeId;
|
|
2191
|
+
const visited = new Set();
|
|
2192
|
+
const nodesToInclude = new Set([nodeIdStr]);
|
|
2193
|
+
const edgesToInclude = [];
|
|
2194
|
+
|
|
2195
|
+
// BFS to trace flow
|
|
2196
|
+
const queue = [nodeIdStr];
|
|
2197
|
+
const maxDepth = 5; // Limit depth to avoid overwhelming
|
|
2198
|
+
const depthMap = { [nodeIdStr]: 0 };
|
|
2199
|
+
|
|
2200
|
+
while (queue.length > 0) {
|
|
2201
|
+
const current = queue.shift();
|
|
2202
|
+
if (visited.has(current)) continue;
|
|
2203
|
+
visited.add(current);
|
|
2204
|
+
|
|
2205
|
+
const currentDepth = depthMap[current] || 0;
|
|
2206
|
+
if (currentDepth >= maxDepth) continue;
|
|
2207
|
+
|
|
2208
|
+
for (const edge of fullCallGraphData.edges) {
|
|
2209
|
+
let nextNode = null;
|
|
2210
|
+
|
|
2211
|
+
if (direction === 'downstream' && edge.data.source === current) {
|
|
2212
|
+
// Following calls: source -> target
|
|
2213
|
+
nextNode = edge.data.target;
|
|
2214
|
+
} else if (direction === 'upstream' && edge.data.target === current) {
|
|
2215
|
+
// Following callers: target <- source
|
|
2216
|
+
nextNode = edge.data.source;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
if (nextNode && !visited.has(nextNode)) {
|
|
2220
|
+
nodesToInclude.add(nextNode);
|
|
2221
|
+
edgesToInclude.push(edge);
|
|
2222
|
+
queue.push(nextNode);
|
|
2223
|
+
depthMap[nextNode] = currentDepth + 1;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// Build flow graph
|
|
2229
|
+
const flowNodes = fullCallGraphData.nodes.filter(n => nodesToInclude.has(n.data.id));
|
|
2230
|
+
const flowGraph = {
|
|
2231
|
+
nodes: flowNodes,
|
|
2232
|
+
edges: edgesToInclude,
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
if (flowNodes.length > 0) {
|
|
2236
|
+
initCytoscape(flowGraph);
|
|
2237
|
+
|
|
2238
|
+
// Use hierarchical layout for flow
|
|
2239
|
+
setTimeout(() => {
|
|
2240
|
+
if (cy) {
|
|
2241
|
+
cy.layout({
|
|
2242
|
+
name: 'breadthfirst',
|
|
2243
|
+
directed: true,
|
|
2244
|
+
roots: direction === 'downstream' ? [nodeIdStr] : undefined,
|
|
2245
|
+
spacingFactor: 1.5,
|
|
2246
|
+
animate: true,
|
|
2247
|
+
animationDuration: 300,
|
|
2248
|
+
}).run();
|
|
2249
|
+
}
|
|
2250
|
+
}, 100);
|
|
2251
|
+
|
|
2252
|
+
// Highlight the starting node
|
|
2253
|
+
setTimeout(() => {
|
|
2254
|
+
if (cy) {
|
|
2255
|
+
const startNode = cy.getElementById(nodeIdStr);
|
|
2256
|
+
if (startNode) {
|
|
2257
|
+
startNode.select();
|
|
2258
|
+
showDetails(startNode.data());
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}, 200);
|
|
2262
|
+
|
|
2263
|
+
const dirLabel = direction === 'downstream' ? 'calls from' : 'callers of';
|
|
2264
|
+
document.getElementById('view-description').innerHTML =
|
|
2265
|
+
'<strong>Flow: ' + dirLabel + '</strong> - Tracing ' + (flowNodes.length - 1) + ' ' + (direction === 'downstream' ? 'called functions' : 'calling functions') + '. Click nodes to see details.';
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// Drill down into a node to see related structures
|
|
2270
|
+
function drillDown(nodeData) {
|
|
2271
|
+
if (!nodeData) return;
|
|
2272
|
+
|
|
2273
|
+
let nodes = [];
|
|
2274
|
+
let edges = [];
|
|
2275
|
+
let title = '';
|
|
2276
|
+
|
|
2277
|
+
// For files: show all structures in that file
|
|
2278
|
+
if (nodeData.type === 'file' && nodeData.file) {
|
|
2279
|
+
const filePath = nodeData.file;
|
|
2280
|
+
title = 'Structures in ' + filePath.split('/').pop();
|
|
2281
|
+
|
|
2282
|
+
// Find all structures in this file from callGraph data
|
|
2283
|
+
const allNodes = vizData.graphs.callGraph.nodes;
|
|
2284
|
+
nodes = allNodes.filter(n => n.data.file === filePath);
|
|
2285
|
+
|
|
2286
|
+
// Find edges between these nodes
|
|
2287
|
+
const nodeIds = new Set(nodes.map(n => n.data.id));
|
|
2288
|
+
edges = vizData.graphs.callGraph.edges.filter(e =>
|
|
2289
|
+
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
// For classes/interfaces: show methods in same file near the class
|
|
2293
|
+
else if ((nodeData.type === 'class' || nodeData.type === 'interface') && nodeData.file) {
|
|
2294
|
+
const filePath = nodeData.file;
|
|
2295
|
+
const classLine = nodeData.line || 0;
|
|
2296
|
+
const classLineEnd = nodeData.lineEnd || classLine + 1000;
|
|
2297
|
+
title = 'Contents of ' + nodeData.label;
|
|
2298
|
+
|
|
2299
|
+
// Find methods in the same file within the class line range
|
|
2300
|
+
const allNodes = vizData.graphs.callGraph.nodes;
|
|
2301
|
+
nodes = allNodes.filter(n => {
|
|
2302
|
+
if (n.data.file !== filePath) return false;
|
|
2303
|
+
if (n.data.id === nodeData.id) return true; // Include the class itself
|
|
2304
|
+
if (n.data.type === 'method') {
|
|
2305
|
+
const line = n.data.line || 0;
|
|
2306
|
+
return line >= classLine && line <= classLineEnd;
|
|
2307
|
+
}
|
|
2308
|
+
return false;
|
|
2309
|
+
});
|
|
2310
|
+
|
|
2311
|
+
// Find edges between these nodes
|
|
2312
|
+
const nodeIds = new Set(nodes.map(n => n.data.id));
|
|
2313
|
+
edges = vizData.graphs.callGraph.edges.filter(e =>
|
|
2314
|
+
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
// For functions/methods: show what they call (focus mode)
|
|
2318
|
+
else if (nodeData.type === 'function' || nodeData.type === 'method') {
|
|
2319
|
+
// Save current state before drilling
|
|
2320
|
+
drillHistory.push({
|
|
2321
|
+
view: currentView,
|
|
2322
|
+
label: getBreadcrumbLabel(currentView),
|
|
2323
|
+
description: document.getElementById('view-description').innerHTML
|
|
2324
|
+
});
|
|
2325
|
+
updateBackButton();
|
|
2326
|
+
|
|
2327
|
+
// Update breadcrumb
|
|
2328
|
+
const breadcrumbItems = drillHistory.map(h => ({ label: h.label }));
|
|
2329
|
+
breadcrumbItems.push({ label: nodeData.label });
|
|
2330
|
+
updateBreadcrumb(breadcrumbItems);
|
|
2331
|
+
|
|
2332
|
+
fullCallGraphData = vizData.graphs.callGraph;
|
|
2333
|
+
focusOnNode(nodeData.id);
|
|
2334
|
+
|
|
2335
|
+
document.getElementById('view-description').innerHTML =
|
|
2336
|
+
'<strong>' + nodeData.label + '</strong> - Shows calls to/from this function. Double-click to explore further.';
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
// Default: show structures in same file
|
|
2340
|
+
else if (nodeData.file) {
|
|
2341
|
+
const filePath = nodeData.file;
|
|
2342
|
+
title = 'Structures in ' + filePath.split('/').pop();
|
|
2343
|
+
|
|
2344
|
+
const allNodes = vizData.graphs.callGraph.nodes;
|
|
2345
|
+
nodes = allNodes.filter(n => n.data.file === filePath);
|
|
2346
|
+
|
|
2347
|
+
const nodeIds = new Set(nodes.map(n => n.data.id));
|
|
2348
|
+
edges = vizData.graphs.callGraph.edges.filter(e =>
|
|
2349
|
+
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
if (nodes.length > 0) {
|
|
2354
|
+
// Save current state before drilling
|
|
2355
|
+
drillHistory.push({
|
|
2356
|
+
view: currentView,
|
|
2357
|
+
label: getBreadcrumbLabel(currentView),
|
|
2358
|
+
description: document.getElementById('view-description').innerHTML
|
|
2359
|
+
});
|
|
2360
|
+
updateBackButton();
|
|
2361
|
+
|
|
2362
|
+
// Update breadcrumb
|
|
2363
|
+
const breadcrumbItems = drillHistory.map(h => ({ label: h.label }));
|
|
2364
|
+
breadcrumbItems.push({ label: title.replace('Structures in ', '').replace('Contents of ', '') });
|
|
2365
|
+
updateBreadcrumb(breadcrumbItems);
|
|
2366
|
+
|
|
2367
|
+
initCytoscape({ nodes, edges });
|
|
2368
|
+
document.getElementById('view-description').innerHTML =
|
|
2369
|
+
'<strong>' + title + '</strong> - Double-click to drill deeper.';
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function getBreadcrumbLabel(view) {
|
|
2374
|
+
const labels = {
|
|
2375
|
+
overview: 'Files',
|
|
2376
|
+
callGraph: 'Call Graph',
|
|
2377
|
+
dependencies: 'Dependencies',
|
|
2378
|
+
classes: 'Classes',
|
|
2379
|
+
decisions: 'Decisions',
|
|
2380
|
+
};
|
|
2381
|
+
return labels[view] || view;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// Go back to previous view
|
|
2385
|
+
function goBack() {
|
|
2386
|
+
if (drillHistory.length === 0) return;
|
|
2387
|
+
|
|
2388
|
+
const prev = drillHistory.pop();
|
|
2389
|
+
updateBackButton();
|
|
2390
|
+
|
|
2391
|
+
// Restore the previous view
|
|
2392
|
+
switchView(prev.view);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// Update back button visibility
|
|
2396
|
+
function updateBackButton() {
|
|
2397
|
+
const btn = document.getElementById('back-btn');
|
|
2398
|
+
if (drillHistory.length > 0) {
|
|
2399
|
+
btn.classList.add('visible');
|
|
2400
|
+
} else {
|
|
2401
|
+
btn.classList.remove('visible');
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
function applyFilters() {
|
|
2406
|
+
if (!cy || cy.destroyed()) return;
|
|
2407
|
+
|
|
2408
|
+
const filters = {
|
|
2409
|
+
function: document.getElementById('filter-function').checked,
|
|
2410
|
+
class: document.getElementById('filter-class').checked,
|
|
2411
|
+
method: document.getElementById('filter-method').checked,
|
|
2412
|
+
interface: document.getElementById('filter-interface').checked,
|
|
2413
|
+
type: document.getElementById('filter-type').checked,
|
|
2414
|
+
file: document.getElementById('filter-file').checked,
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2417
|
+
cy.nodes().forEach(node => {
|
|
2418
|
+
const type = node.data('type');
|
|
2419
|
+
const visible = filters[type] !== false;
|
|
2420
|
+
node.style('display', visible ? 'element' : 'none');
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
// Refit to show visible nodes
|
|
2424
|
+
setTimeout(() => {
|
|
2425
|
+
cy.fit(cy.nodes(':visible'), 50);
|
|
2426
|
+
}, 50);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// View mode toggle (visual vs list)
|
|
2430
|
+
function setViewMode(mode) {
|
|
2431
|
+
currentViewMode = mode;
|
|
2432
|
+
|
|
2433
|
+
// Update toggle buttons
|
|
2434
|
+
document.querySelectorAll('.view-toggle-btn').forEach(btn => {
|
|
2435
|
+
btn.classList.toggle('active', btn.dataset.mode === mode);
|
|
2436
|
+
});
|
|
2437
|
+
|
|
2438
|
+
// Toggle visibility
|
|
2439
|
+
const cyEl = document.getElementById('cy');
|
|
2440
|
+
const listEl = document.getElementById('list-view');
|
|
2441
|
+
const legendEl = document.getElementById('legend');
|
|
2442
|
+
|
|
2443
|
+
if (mode === 'visual') {
|
|
2444
|
+
cyEl.classList.remove('hidden');
|
|
2445
|
+
listEl.classList.remove('active');
|
|
2446
|
+
legendEl.classList.remove('hidden');
|
|
2447
|
+
if (cy) {
|
|
2448
|
+
setTimeout(() => {
|
|
2449
|
+
cy.resize();
|
|
2450
|
+
cy.fit(50);
|
|
2451
|
+
}, 50);
|
|
2452
|
+
}
|
|
2453
|
+
} else {
|
|
2454
|
+
cyEl.classList.add('hidden');
|
|
2455
|
+
listEl.classList.add('active');
|
|
2456
|
+
legendEl.classList.add('hidden');
|
|
2457
|
+
renderListView();
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// Render list view for current data
|
|
2462
|
+
function renderListView() {
|
|
2463
|
+
const listEl = document.getElementById('list-view');
|
|
2464
|
+
const graphData = currentView === 'callGraph' ? fullCallGraphData : vizData.graphs[currentView];
|
|
2465
|
+
|
|
2466
|
+
if (!graphData || graphData.nodes.length === 0) {
|
|
2467
|
+
listEl.innerHTML = '<div class="empty-state"><h3>No data</h3><p>No items to display for this view</p></div>';
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// Group nodes by type
|
|
2472
|
+
const byType = {};
|
|
2473
|
+
for (const node of graphData.nodes) {
|
|
2474
|
+
const type = node.data.type || 'other';
|
|
2475
|
+
if (!byType[type]) byType[type] = [];
|
|
2476
|
+
byType[type].push(node.data);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// Sort types
|
|
2480
|
+
const typeOrder = ['file', 'class', 'interface', 'function', 'method', 'type', 'variable', 'module', 'decision', 'pattern', 'rejection'];
|
|
2481
|
+
const sortedTypes = Object.keys(byType).sort((a, b) => {
|
|
2482
|
+
const aIdx = typeOrder.indexOf(a);
|
|
2483
|
+
const bIdx = typeOrder.indexOf(b);
|
|
2484
|
+
return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
let html = '';
|
|
2488
|
+
for (const type of sortedTypes) {
|
|
2489
|
+
const items = byType[type];
|
|
2490
|
+
html += '<div class="list-section">';
|
|
2491
|
+
html += '<h4>' + type + 's (' + items.length + ')</h4>';
|
|
2492
|
+
|
|
2493
|
+
// Sort items by name
|
|
2494
|
+
items.sort((a, b) => (a.label || '').localeCompare(b.label || ''));
|
|
2495
|
+
|
|
2496
|
+
for (const item of items) {
|
|
2497
|
+
html += '<div class="list-item" data-id="' + escapeHtml(item.id) + '">';
|
|
2498
|
+
html += '<span class="tag ' + type + '">' + type + '</span>';
|
|
2499
|
+
html += '<div class="list-item-content">';
|
|
2500
|
+
html += '<div class="list-item-name">' + escapeHtml(item.label) + '</div>';
|
|
2501
|
+
if (item.file) {
|
|
2502
|
+
html += '<div class="list-item-location">' + escapeHtml(item.file);
|
|
2503
|
+
if (item.line) html += ':' + item.line;
|
|
2504
|
+
html += '</div>';
|
|
2505
|
+
}
|
|
2506
|
+
if (item.signature) {
|
|
2507
|
+
html += '<div class="list-item-signature">' + escapeHtml(item.signature) + '</div>';
|
|
2508
|
+
}
|
|
2509
|
+
html += '</div></div>';
|
|
2510
|
+
}
|
|
2511
|
+
html += '</div>';
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
listEl.innerHTML = html;
|
|
2515
|
+
|
|
2516
|
+
// Add click handlers
|
|
2517
|
+
listEl.querySelectorAll('.list-item').forEach(item => {
|
|
2518
|
+
item.addEventListener('click', () => {
|
|
2519
|
+
const id = item.dataset.id;
|
|
2520
|
+
const nodeData = graphData.nodes.find(n => n.data.id === id);
|
|
2521
|
+
if (nodeData) {
|
|
2522
|
+
showDetails(nodeData.data);
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
// Double-click to drill down
|
|
2527
|
+
item.addEventListener('dblclick', () => {
|
|
2528
|
+
const id = item.dataset.id;
|
|
2529
|
+
const nodeData = graphData.nodes.find(n => n.data.id === id);
|
|
2530
|
+
if (nodeData) {
|
|
2531
|
+
drillDown(nodeData.data);
|
|
2532
|
+
}
|
|
2533
|
+
});
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// Track last search matches for visualization
|
|
2538
|
+
let lastSearchMatches = [];
|
|
2539
|
+
let lastSearchQuery = '';
|
|
2540
|
+
|
|
2541
|
+
function searchNodes(query) {
|
|
2542
|
+
const searchInput = document.getElementById('search');
|
|
2543
|
+
const visualizeBtn = document.getElementById('visualize-search-btn');
|
|
2544
|
+
|
|
2545
|
+
if (!query) {
|
|
2546
|
+
// Reset visual view
|
|
2547
|
+
if (cy) {
|
|
2548
|
+
cy.nodes().style('opacity', 1);
|
|
2549
|
+
cy.nodes().removeClass('search-match');
|
|
2550
|
+
}
|
|
2551
|
+
// Reset list view filter
|
|
2552
|
+
if (currentViewMode === 'list') {
|
|
2553
|
+
document.querySelectorAll('.list-item').forEach(item => {
|
|
2554
|
+
item.style.display = '';
|
|
2555
|
+
});
|
|
2556
|
+
document.querySelectorAll('.list-section').forEach(section => {
|
|
2557
|
+
section.style.display = '';
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
// Hide visualize button
|
|
2561
|
+
lastSearchMatches = [];
|
|
2562
|
+
lastSearchQuery = '';
|
|
2563
|
+
if (visualizeBtn) visualizeBtn.style.display = 'none';
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
query = query.toLowerCase();
|
|
2568
|
+
|
|
2569
|
+
// Get the data source for current view
|
|
2570
|
+
// For custom views (smells, hotspots, etc.), use call graph data
|
|
2571
|
+
const customViews = ['smells', 'hotspots', 'gallery', 'timeline', 'treemap'];
|
|
2572
|
+
const isCustomView = customViews.includes(currentView);
|
|
2573
|
+
let graphData;
|
|
2574
|
+
|
|
2575
|
+
if (currentView === 'callGraph') {
|
|
2576
|
+
graphData = fullCallGraphData;
|
|
2577
|
+
} else if (isCustomView) {
|
|
2578
|
+
// Use call graph for searching from custom views
|
|
2579
|
+
graphData = vizData.graphs.callGraph;
|
|
2580
|
+
} else {
|
|
2581
|
+
graphData = vizData.graphs[currentView];
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
if (!graphData) return;
|
|
2585
|
+
|
|
2586
|
+
// Search across name, file, and signature
|
|
2587
|
+
const matches = graphData.nodes.filter(n => {
|
|
2588
|
+
const label = (n.data.label || '').toLowerCase();
|
|
2589
|
+
const file = (n.data.file || '').toLowerCase();
|
|
2590
|
+
const signature = (n.data.signature || '').toLowerCase();
|
|
2591
|
+
return label.includes(query) || file.includes(query) || signature.includes(query);
|
|
2592
|
+
});
|
|
2593
|
+
|
|
2594
|
+
// Store matches for visualization
|
|
2595
|
+
lastSearchMatches = matches;
|
|
2596
|
+
lastSearchQuery = query;
|
|
2597
|
+
|
|
2598
|
+
// Show/hide visualize button based on matches
|
|
2599
|
+
if (visualizeBtn) {
|
|
2600
|
+
visualizeBtn.style.display = matches.length >= 2 && matches.length <= 100 ? 'inline-block' : 'none';
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// Handle list view
|
|
2604
|
+
if (currentViewMode === 'list') {
|
|
2605
|
+
document.querySelectorAll('.list-item').forEach(item => {
|
|
2606
|
+
const name = item.querySelector('.list-item-name');
|
|
2607
|
+
const location = item.querySelector('.list-item-location');
|
|
2608
|
+
const signature = item.querySelector('.list-item-signature');
|
|
2609
|
+
const text = [
|
|
2610
|
+
name ? name.textContent : '',
|
|
2611
|
+
location ? location.textContent : '',
|
|
2612
|
+
signature ? signature.textContent : ''
|
|
2613
|
+
].join(' ').toLowerCase();
|
|
2614
|
+
item.style.display = text.includes(query) ? '' : 'none';
|
|
2615
|
+
});
|
|
2616
|
+
|
|
2617
|
+
// Hide empty sections
|
|
2618
|
+
document.querySelectorAll('.list-section').forEach(section => {
|
|
2619
|
+
const visibleItems = section.querySelectorAll('.list-item[style=""], .list-item:not([style])');
|
|
2620
|
+
const hasVisible = Array.from(section.querySelectorAll('.list-item')).some(
|
|
2621
|
+
item => item.style.display !== 'none'
|
|
2622
|
+
);
|
|
2623
|
+
section.style.display = hasVisible ? '' : 'none';
|
|
2624
|
+
});
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// Handle visual view
|
|
2629
|
+
if (matches.length === 0) {
|
|
2630
|
+
// No matches - show message
|
|
2631
|
+
document.getElementById('details-content').innerHTML = \`
|
|
2632
|
+
<div class="empty-state">
|
|
2633
|
+
<h3>No matches</h3>
|
|
2634
|
+
<p>No results for "\${escapeHtml(query)}"</p>
|
|
2635
|
+
</div>
|
|
2636
|
+
\`;
|
|
2637
|
+
if (cy) cy.nodes().style('opacity', 0.2);
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// For custom views, show results in details panel with option to switch to call graph
|
|
2642
|
+
if (isCustomView) {
|
|
2643
|
+
const listHtml = matches.slice(0, 30).map(m =>
|
|
2644
|
+
'<div style="padding: 6px 0; cursor: pointer; color: #e94560; border-bottom: 1px solid #0f3460;" onclick="switchView(\\'callGraph\\'); setTimeout(() => focusOnNode(\\'' + m.data.id + '\\'), 100);">' +
|
|
2645
|
+
escapeHtml(m.data.label) +
|
|
2646
|
+
'<span style="color: #666; font-size: 11px; margin-left: 8px;">' + (m.data.file ? m.data.file.split("/").pop() : '') + '</span></div>'
|
|
2647
|
+
).join('');
|
|
2648
|
+
|
|
2649
|
+
document.getElementById('details-content').innerHTML = \`
|
|
2650
|
+
<div class="detail-row">
|
|
2651
|
+
<div class="detail-label">\${matches.length} matches found</div>
|
|
2652
|
+
<div style="font-size: 11px; color: #888; margin-bottom: 8px;">Click to view in Call Graph</div>
|
|
2653
|
+
<div style="margin-top: 8px;">\${listHtml}</div>
|
|
2654
|
+
\${matches.length > 30 ? '<div style="color: #666; margin-top: 8px;">...and ' + (matches.length - 30) + ' more</div>' : ''}
|
|
2655
|
+
</div>
|
|
2656
|
+
\`;
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// For call graph, rebuild with matching nodes
|
|
2661
|
+
if (currentView === 'callGraph' && fullCallGraphData) {
|
|
2662
|
+
if (matches.length === 1) {
|
|
2663
|
+
focusOnNode(matches[0].data.id);
|
|
2664
|
+
} else if (matches.length <= 30) {
|
|
2665
|
+
// Show matching nodes with their connections
|
|
2666
|
+
const matchIds = new Set(matches.map(m => m.data.id));
|
|
2667
|
+
const relevantEdges = fullCallGraphData.edges.filter(e =>
|
|
2668
|
+
matchIds.has(e.data.source) || matchIds.has(e.data.target)
|
|
2669
|
+
);
|
|
2670
|
+
|
|
2671
|
+
// Add connected nodes
|
|
2672
|
+
for (const edge of relevantEdges) {
|
|
2673
|
+
matchIds.add(edge.data.source);
|
|
2674
|
+
matchIds.add(edge.data.target);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const focusedNodes = fullCallGraphData.nodes.filter(n => matchIds.has(n.data.id));
|
|
2678
|
+
initCytoscape({ nodes: focusedNodes, edges: relevantEdges });
|
|
2679
|
+
|
|
2680
|
+
document.getElementById('view-description').innerHTML =
|
|
2681
|
+
'<strong>Search results</strong> - Found ' + matches.length + ' matches for "' + escapeHtml(query) + '"';
|
|
2682
|
+
} else {
|
|
2683
|
+
// Too many - show clickable list
|
|
2684
|
+
const listHtml = matches.slice(0, 30).map(m =>
|
|
2685
|
+
'<div style="padding: 6px 0; cursor: pointer; color: #e94560; border-bottom: 1px solid #0f3460;" onclick="focusOnNode(\\'' + m.data.id + '\\')">' +
|
|
2686
|
+
escapeHtml(m.data.label) +
|
|
2687
|
+
'<span style="color: #666; font-size: 11px; margin-left: 8px;">' + (m.data.file ? m.data.file.split("/").pop() : '') + '</span></div>'
|
|
2688
|
+
).join('');
|
|
2689
|
+
document.getElementById('details-content').innerHTML = \`
|
|
2690
|
+
<div class="detail-row">
|
|
2691
|
+
<div class="detail-label">\${matches.length} matches found</div>
|
|
2692
|
+
<div style="margin-top: 8px;">\${listHtml}</div>
|
|
2693
|
+
\${matches.length > 30 ? '<div style="color: #666; margin-top: 8px;">...and ' + (matches.length - 30) + ' more</div>' : ''}
|
|
2694
|
+
</div>
|
|
2695
|
+
\`;
|
|
2696
|
+
}
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
// For other views, highlight matches
|
|
2701
|
+
if (cy) {
|
|
2702
|
+
const matchIds = new Set(matches.map(m => m.data.id));
|
|
2703
|
+
cy.nodes().forEach(node => {
|
|
2704
|
+
const isMatch = matchIds.has(node.data('id'));
|
|
2705
|
+
node.style('opacity', isMatch ? 1 : 0.15);
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
// Fit to show matches
|
|
2709
|
+
const matchingNodes = cy.nodes().filter(n => matchIds.has(n.data('id')));
|
|
2710
|
+
if (matchingNodes.length > 0 && matchingNodes.length < 20) {
|
|
2711
|
+
cy.fit(matchingNodes, 80);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// ============================================
|
|
2717
|
+
// Custom View Render Functions
|
|
2718
|
+
// ============================================
|
|
2719
|
+
|
|
2720
|
+
let currentSmellFilter = 'all';
|
|
2721
|
+
let currentGalleryFilter = 'all';
|
|
2722
|
+
|
|
2723
|
+
function renderSmellsView() {
|
|
2724
|
+
const container = document.getElementById('smells-view');
|
|
2725
|
+
const smells = vizData.smells;
|
|
2726
|
+
|
|
2727
|
+
if (!smells || smells.smells.length === 0) {
|
|
2728
|
+
container.innerHTML = '<div class="empty-state"><h3>No Code Smells Detected</h3><p>Your codebase looks clean!</p></div>';
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const filtered = currentSmellFilter === 'all'
|
|
2733
|
+
? smells.smells
|
|
2734
|
+
: smells.smells.filter(s => s.type === currentSmellFilter || s.severity === currentSmellFilter);
|
|
2735
|
+
|
|
2736
|
+
container.innerHTML = \`
|
|
2737
|
+
<div class="smells-summary">
|
|
2738
|
+
<div class="smell-stat high">
|
|
2739
|
+
<div class="count">\${smells.summary.high}</div>
|
|
2740
|
+
<div class="label">High</div>
|
|
2741
|
+
</div>
|
|
2742
|
+
<div class="smell-stat medium">
|
|
2743
|
+
<div class="count">\${smells.summary.medium}</div>
|
|
2744
|
+
<div class="label">Medium</div>
|
|
2745
|
+
</div>
|
|
2746
|
+
<div class="smell-stat low">
|
|
2747
|
+
<div class="count">\${smells.summary.low}</div>
|
|
2748
|
+
<div class="label">Low</div>
|
|
2749
|
+
</div>
|
|
2750
|
+
<div class="smell-stat">
|
|
2751
|
+
<div class="count">\${smells.smells.length}</div>
|
|
2752
|
+
<div class="label">Total</div>
|
|
2753
|
+
</div>
|
|
2754
|
+
</div>
|
|
2755
|
+
|
|
2756
|
+
<div class="smell-filters">
|
|
2757
|
+
<button class="smell-filter-btn \${currentSmellFilter === 'all' ? 'active' : ''}" data-filter="all">All</button>
|
|
2758
|
+
<button class="smell-filter-btn \${currentSmellFilter === 'high' ? 'active' : ''}" data-filter="high">High</button>
|
|
2759
|
+
<button class="smell-filter-btn \${currentSmellFilter === 'medium' ? 'active' : ''}" data-filter="medium">Medium</button>
|
|
2760
|
+
<button class="smell-filter-btn \${currentSmellFilter === 'low' ? 'active' : ''}" data-filter="low">Low</button>
|
|
2761
|
+
<span style="margin-left: 16px; color: #444;">|</span>
|
|
2762
|
+
<button class="smell-filter-btn \${currentSmellFilter === 'large-file' ? 'active' : ''}" data-filter="large-file">Large Files</button>
|
|
2763
|
+
<button class="smell-filter-btn \${currentSmellFilter === 'long-function' ? 'active' : ''}" data-filter="long-function">Long Functions</button>
|
|
2764
|
+
<button class="smell-filter-btn \${currentSmellFilter === 'orphan' ? 'active' : ''}" data-filter="orphan">Orphan Code</button>
|
|
2765
|
+
</div>
|
|
2766
|
+
|
|
2767
|
+
<div class="smell-list">
|
|
2768
|
+
\${filtered.map(smell => \`
|
|
2769
|
+
<div class="smell-item \${smell.severity}" data-structure-id="\${smell.structureId || ''}" data-file="\${smell.filePath}">
|
|
2770
|
+
<span class="smell-badge \${smell.type}">\${smell.type.replace(/-/g, ' ')}</span>
|
|
2771
|
+
<div class="smell-info">
|
|
2772
|
+
<div class="smell-name">\${escapeHtml(smell.name)}</div>
|
|
2773
|
+
<div class="smell-desc">\${escapeHtml(smell.description)}</div>
|
|
2774
|
+
</div>
|
|
2775
|
+
<div class="smell-metric">\${smell.metric}</div>
|
|
2776
|
+
</div>
|
|
2777
|
+
\`).join('')}
|
|
2778
|
+
</div>
|
|
2779
|
+
\`;
|
|
2780
|
+
|
|
2781
|
+
// Add filter listeners
|
|
2782
|
+
container.querySelectorAll('.smell-filter-btn').forEach(btn => {
|
|
2783
|
+
btn.addEventListener('click', () => {
|
|
2784
|
+
currentSmellFilter = btn.dataset.filter;
|
|
2785
|
+
renderSmellsView();
|
|
2786
|
+
});
|
|
2787
|
+
});
|
|
2788
|
+
|
|
2789
|
+
// Add click listeners for smell items
|
|
2790
|
+
container.querySelectorAll('.smell-item').forEach(item => {
|
|
2791
|
+
item.addEventListener('click', () => {
|
|
2792
|
+
const structureId = item.dataset.structureId;
|
|
2793
|
+
const filePath = item.dataset.file;
|
|
2794
|
+
// Show details for the smell
|
|
2795
|
+
showSmellDetails(structureId, filePath);
|
|
2796
|
+
});
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
function showSmellDetails(structureId, filePath) {
|
|
2801
|
+
// Find structure in vizData
|
|
2802
|
+
let content = '<div class="detail-row"><div class="detail-label">File</div><div class="detail-value">' + escapeHtml(filePath) + '</div></div>';
|
|
2803
|
+
|
|
2804
|
+
if (structureId) {
|
|
2805
|
+
// Find the structure in call graph or class graph
|
|
2806
|
+
const allNodes = [...(vizData.graphs.callGraph?.nodes || []), ...(vizData.graphs.classes?.nodes || [])];
|
|
2807
|
+
const node = allNodes.find(n => n.data.id === 'structure:' + structureId);
|
|
2808
|
+
if (node) {
|
|
2809
|
+
content += '<div class="detail-row"><div class="detail-label">Name</div><div class="detail-value">' + escapeHtml(node.data.label) + '</div></div>';
|
|
2810
|
+
if (node.data.signature) {
|
|
2811
|
+
content += '<div class="detail-row"><div class="detail-label">Signature</div><div class="detail-value" style="font-family: monospace;">' + escapeHtml(node.data.signature) + '</div></div>';
|
|
2812
|
+
}
|
|
2813
|
+
if (node.data.line) {
|
|
2814
|
+
content += '<div class="detail-row"><div class="detail-label">Line</div><div class="detail-value">' + node.data.line + '</div></div>';
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
document.getElementById('details-content').innerHTML = content;
|
|
2820
|
+
document.getElementById('details-panel').classList.add('visible');
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
function renderHotspotsView() {
|
|
2824
|
+
const container = document.getElementById('hotspots-view');
|
|
2825
|
+
const hotspots = vizData.hotspots;
|
|
2826
|
+
|
|
2827
|
+
if (!hotspots) {
|
|
2828
|
+
container.innerHTML = '<div class="empty-state"><h3>No Hotspot Data</h3><p>Index your codebase first.</p></div>';
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function renderHotspotList(items, valueKey, valueLabel) {
|
|
2833
|
+
if (!items || items.length === 0) return '<div style="color: #666; padding: 12px;">No data</div>';
|
|
2834
|
+
return items.map((item, i) => \`
|
|
2835
|
+
<div class="hotspot-item" data-id="\${item.id}" data-file="\${item.file}">
|
|
2836
|
+
<div class="hotspot-rank">\${i + 1}</div>
|
|
2837
|
+
<div class="hotspot-info">
|
|
2838
|
+
<div class="hotspot-name">\${escapeHtml(item.name)}</div>
|
|
2839
|
+
<div class="hotspot-file">\${escapeHtml(item.file?.split('/').pop() || item.file || '')}</div>
|
|
2840
|
+
</div>
|
|
2841
|
+
<div class="hotspot-value">\${item[valueKey] || 0} \${valueLabel}</div>
|
|
2842
|
+
</div>
|
|
2843
|
+
\`).join('');
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
container.innerHTML = \`
|
|
2847
|
+
<div class="hotspots-grid">
|
|
2848
|
+
<div class="hotspot-section">
|
|
2849
|
+
<h3>Largest Functions</h3>
|
|
2850
|
+
<div class="hotspot-list">
|
|
2851
|
+
\${renderHotspotList(hotspots.largestFunctions, 'lines', 'lines')}
|
|
2852
|
+
</div>
|
|
2853
|
+
</div>
|
|
2854
|
+
<div class="hotspot-section">
|
|
2855
|
+
<h3>Most Connected</h3>
|
|
2856
|
+
<div class="hotspot-list">
|
|
2857
|
+
\${renderHotspotList(hotspots.mostConnected, 'total', 'connections')}
|
|
2858
|
+
</div>
|
|
2859
|
+
</div>
|
|
2860
|
+
<div class="hotspot-section">
|
|
2861
|
+
<h3>Densest Files</h3>
|
|
2862
|
+
<div class="hotspot-list">
|
|
2863
|
+
\${renderHotspotList(hotspots.densestFiles, 'structureCount', 'structures')}
|
|
2864
|
+
</div>
|
|
2865
|
+
</div>
|
|
2866
|
+
<div class="hotspot-section">
|
|
2867
|
+
<h3>Hub Functions (Most Called)</h3>
|
|
2868
|
+
<div class="hotspot-list">
|
|
2869
|
+
\${renderHotspotList(hotspots.hubFunctions, 'inbound', 'callers')}
|
|
2870
|
+
</div>
|
|
2871
|
+
</div>
|
|
2872
|
+
</div>
|
|
2873
|
+
\`;
|
|
2874
|
+
|
|
2875
|
+
// Add click listeners
|
|
2876
|
+
container.querySelectorAll('.hotspot-item').forEach(item => {
|
|
2877
|
+
item.addEventListener('click', () => {
|
|
2878
|
+
const id = item.dataset.id;
|
|
2879
|
+
const file = item.dataset.file;
|
|
2880
|
+
if (id && id !== '0') {
|
|
2881
|
+
// Switch to call graph and focus
|
|
2882
|
+
switchView('callGraph');
|
|
2883
|
+
setTimeout(() => focusOnNode('structure:' + id), 100);
|
|
2884
|
+
} else if (file) {
|
|
2885
|
+
// Show file details
|
|
2886
|
+
showSmellDetails(null, file);
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function renderGalleryView() {
|
|
2893
|
+
const container = document.getElementById('gallery-view');
|
|
2894
|
+
const gallery = vizData.gallery;
|
|
2895
|
+
|
|
2896
|
+
if (!gallery || gallery.items.length === 0) {
|
|
2897
|
+
container.innerHTML = '<div class="empty-state"><h3>No Gallery Items</h3><p>Decisions, patterns, and rejections from AI conversations will appear here.</p></div>';
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
const filtered = currentGalleryFilter === 'all'
|
|
2902
|
+
? gallery.items
|
|
2903
|
+
: gallery.items.filter(item => item.type === currentGalleryFilter);
|
|
2904
|
+
|
|
2905
|
+
container.innerHTML = \`
|
|
2906
|
+
<div class="gallery-filters">
|
|
2907
|
+
<button class="smell-filter-btn \${currentGalleryFilter === 'all' ? 'active' : ''}" data-filter="all">All (\${gallery.items.length})</button>
|
|
2908
|
+
<button class="smell-filter-btn \${currentGalleryFilter === 'decision' ? 'active' : ''}" data-filter="decision">Decisions (\${gallery.byType.decision})</button>
|
|
2909
|
+
<button class="smell-filter-btn \${currentGalleryFilter === 'pattern' ? 'active' : ''}" data-filter="pattern">Patterns (\${gallery.byType.pattern})</button>
|
|
2910
|
+
<button class="smell-filter-btn \${currentGalleryFilter === 'rejection' ? 'active' : ''}" data-filter="rejection">Rejections (\${gallery.byType.rejection})</button>
|
|
2911
|
+
</div>
|
|
2912
|
+
|
|
2913
|
+
<div class="gallery-grid">
|
|
2914
|
+
\${filtered.map(item => \`
|
|
2915
|
+
<div class="gallery-card" data-id="\${item.id}">
|
|
2916
|
+
<div class="gallery-card-header">
|
|
2917
|
+
<span class="gallery-type-badge \${item.type}">\${item.type}</span>
|
|
2918
|
+
<span class="gallery-timestamp">\${item.timestamp ? new Date(item.timestamp).toLocaleDateString() : ''}</span>
|
|
2919
|
+
</div>
|
|
2920
|
+
<div class="gallery-content">\${escapeHtml(item.content)}</div>
|
|
2921
|
+
\${item.affectedCode.length > 0 ? \`
|
|
2922
|
+
<div class="gallery-affected">
|
|
2923
|
+
\${item.affectedCode.slice(0, 5).map(code => \`
|
|
2924
|
+
<span class="gallery-code-chip" data-id="\${code.id}">\${escapeHtml(code.name)}</span>
|
|
2925
|
+
\`).join('')}
|
|
2926
|
+
\${item.affectedCode.length > 5 ? '<span class="gallery-code-chip">+' + (item.affectedCode.length - 5) + ' more</span>' : ''}
|
|
2927
|
+
</div>
|
|
2928
|
+
\` : ''}
|
|
2929
|
+
</div>
|
|
2930
|
+
\`).join('')}
|
|
2931
|
+
</div>
|
|
2932
|
+
\`;
|
|
2933
|
+
|
|
2934
|
+
// Add filter listeners
|
|
2935
|
+
container.querySelectorAll('.smell-filter-btn').forEach(btn => {
|
|
2936
|
+
btn.addEventListener('click', () => {
|
|
2937
|
+
currentGalleryFilter = btn.dataset.filter;
|
|
2938
|
+
renderGalleryView();
|
|
2939
|
+
});
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
// Add expand card listeners
|
|
2943
|
+
container.querySelectorAll('.gallery-card').forEach(card => {
|
|
2944
|
+
card.addEventListener('click', () => {
|
|
2945
|
+
card.classList.toggle('expanded');
|
|
2946
|
+
});
|
|
2947
|
+
});
|
|
2948
|
+
|
|
2949
|
+
// Add code chip listeners
|
|
2950
|
+
container.querySelectorAll('.gallery-code-chip').forEach(chip => {
|
|
2951
|
+
chip.addEventListener('click', (e) => {
|
|
2952
|
+
e.stopPropagation();
|
|
2953
|
+
const id = chip.dataset.id;
|
|
2954
|
+
if (id) {
|
|
2955
|
+
switchView('callGraph');
|
|
2956
|
+
setTimeout(() => focusOnNode('structure:' + id), 100);
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
function renderTimelineView() {
|
|
2963
|
+
const container = document.getElementById('timeline-view');
|
|
2964
|
+
const timeline = vizData.timeline;
|
|
2965
|
+
|
|
2966
|
+
if (!timeline || timeline.entries.length === 0) {
|
|
2967
|
+
container.innerHTML = '<div class="empty-state"><h3>No Timeline Data</h3><p>AI conversation history will appear here.</p></div>';
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// Find max extraction count for scaling bars
|
|
2972
|
+
const maxExtractions = Math.max(...timeline.entries.map(e => e.extractionCount || 1), 1);
|
|
2973
|
+
|
|
2974
|
+
container.innerHTML = \`
|
|
2975
|
+
<div class="timeline-container">
|
|
2976
|
+
<div class="timeline-header">
|
|
2977
|
+
<div>
|
|
2978
|
+
<strong>\${timeline.entries.length}</strong> conversations
|
|
2979
|
+
\${timeline.dateRange ? \` from \${new Date(timeline.dateRange.start).toLocaleDateString()} to \${new Date(timeline.dateRange.end).toLocaleDateString()}\` : ''}
|
|
2980
|
+
</div>
|
|
2981
|
+
</div>
|
|
2982
|
+
<div class="timeline-scroll">
|
|
2983
|
+
<div class="timeline-track">
|
|
2984
|
+
\${timeline.entries.map((entry, i) => {
|
|
2985
|
+
const barHeight = Math.max(20, (entry.extractionCount / maxExtractions) * 200);
|
|
2986
|
+
const date = new Date(entry.timestamp);
|
|
2987
|
+
return \`
|
|
2988
|
+
<div class="timeline-entry" data-index="\${i}">
|
|
2989
|
+
<div class="timeline-bar" style="height: \${barHeight}px;" title="\${entry.extractionCount} extractions"></div>
|
|
2990
|
+
<div class="timeline-dot"></div>
|
|
2991
|
+
<div class="timeline-date">\${date.toLocaleDateString()}</div>
|
|
2992
|
+
</div>
|
|
2993
|
+
\`;
|
|
2994
|
+
}).join('')}
|
|
2995
|
+
</div>
|
|
2996
|
+
</div>
|
|
2997
|
+
<div class="timeline-details" id="timeline-details">
|
|
2998
|
+
<div style="color: #666;">Click on a timeline entry to see details</div>
|
|
2999
|
+
</div>
|
|
3000
|
+
</div>
|
|
3001
|
+
\`;
|
|
3002
|
+
|
|
3003
|
+
// Add click listeners
|
|
3004
|
+
container.querySelectorAll('.timeline-entry').forEach(entry => {
|
|
3005
|
+
entry.addEventListener('click', () => {
|
|
3006
|
+
const index = parseInt(entry.dataset.index);
|
|
3007
|
+
const item = timeline.entries[index];
|
|
3008
|
+
showTimelineDetails(item);
|
|
3009
|
+
});
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
function showTimelineDetails(entry) {
|
|
3014
|
+
const detailsEl = document.getElementById('timeline-details');
|
|
3015
|
+
if (!detailsEl) return;
|
|
3016
|
+
|
|
3017
|
+
const date = new Date(entry.timestamp);
|
|
3018
|
+
detailsEl.innerHTML = \`
|
|
3019
|
+
<div style="display: flex; gap: 24px; flex-wrap: wrap;">
|
|
3020
|
+
<div>
|
|
3021
|
+
<div style="color: #888; font-size: 12px;">Date</div>
|
|
3022
|
+
<div>\${date.toLocaleString()}</div>
|
|
3023
|
+
</div>
|
|
3024
|
+
<div>
|
|
3025
|
+
<div style="color: #888; font-size: 12px;">Model</div>
|
|
3026
|
+
<div>\${entry.model || 'Unknown'}</div>
|
|
3027
|
+
</div>
|
|
3028
|
+
<div>
|
|
3029
|
+
<div style="color: #888; font-size: 12px;">Tool</div>
|
|
3030
|
+
<div>\${entry.tool || 'Unknown'}</div>
|
|
3031
|
+
</div>
|
|
3032
|
+
<div>
|
|
3033
|
+
<div style="color: #888; font-size: 12px;">Extractions</div>
|
|
3034
|
+
<div>\${entry.extractionCount}</div>
|
|
3035
|
+
</div>
|
|
3036
|
+
</div>
|
|
3037
|
+
\${entry.summary ? '<div style="margin-top: 16px;"><div style="color: #888; font-size: 12px;">Summary</div><div>' + escapeHtml(entry.summary) + '</div></div>' : ''}
|
|
3038
|
+
\${entry.touchedStructures.length > 0 ? \`
|
|
3039
|
+
<div style="margin-top: 16px;">
|
|
3040
|
+
<div style="color: #888; font-size: 12px;">Touched Structures</div>
|
|
3041
|
+
<div style="display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;">
|
|
3042
|
+
\${entry.touchedStructures.map(s => \`<span class="gallery-code-chip" data-id="\${s.id}">\${escapeHtml(s.name)}</span>\`).join('')}
|
|
3043
|
+
</div>
|
|
3044
|
+
</div>
|
|
3045
|
+
\` : ''}
|
|
3046
|
+
\`;
|
|
3047
|
+
|
|
3048
|
+
// Add code chip listeners
|
|
3049
|
+
detailsEl.querySelectorAll('.gallery-code-chip').forEach(chip => {
|
|
3050
|
+
chip.addEventListener('click', () => {
|
|
3051
|
+
const id = chip.dataset.id;
|
|
3052
|
+
if (id) {
|
|
3053
|
+
switchView('callGraph');
|
|
3054
|
+
setTimeout(() => focusOnNode('structure:' + id), 100);
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
let treemapRoot = null;
|
|
3061
|
+
let treemapCurrent = null;
|
|
3062
|
+
|
|
3063
|
+
function renderTreemapView() {
|
|
3064
|
+
const container = document.getElementById('treemap-view');
|
|
3065
|
+
const treemap = vizData.treemap;
|
|
3066
|
+
|
|
3067
|
+
if (!treemap || treemap.value === 0) {
|
|
3068
|
+
container.innerHTML = '<div class="empty-state"><h3>No Treemap Data</h3><p>Index your codebase first.</p></div>';
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
treemapRoot = treemap;
|
|
3073
|
+
treemapCurrent = treemap;
|
|
3074
|
+
|
|
3075
|
+
container.innerHTML = \`
|
|
3076
|
+
<div class="treemap-breadcrumb" id="treemap-breadcrumb">
|
|
3077
|
+
<span class="current">root</span>
|
|
3078
|
+
</div>
|
|
3079
|
+
<div id="treemap-container"></div>
|
|
3080
|
+
\`;
|
|
3081
|
+
|
|
3082
|
+
renderTreemap(treemap);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
function renderTreemap(data) {
|
|
3086
|
+
const containerEl = document.getElementById('treemap-container');
|
|
3087
|
+
if (!containerEl) return;
|
|
3088
|
+
|
|
3089
|
+
const width = containerEl.clientWidth || 800;
|
|
3090
|
+
const height = containerEl.clientHeight || 500;
|
|
3091
|
+
|
|
3092
|
+
// Clear previous
|
|
3093
|
+
containerEl.innerHTML = '';
|
|
3094
|
+
|
|
3095
|
+
// If no children, show empty state
|
|
3096
|
+
if (!data.children || data.children.length === 0) {
|
|
3097
|
+
containerEl.innerHTML = '<div class="empty-state"><h3>No children</h3><p>This node has no child items to display.</p></div>';
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// D3 treemap layout - use children of current node, not leaves
|
|
3102
|
+
const root = d3.hierarchy(data)
|
|
3103
|
+
.sum(d => d.value || 0)
|
|
3104
|
+
.sort((a, b) => b.value - a.value);
|
|
3105
|
+
|
|
3106
|
+
d3.treemap()
|
|
3107
|
+
.size([width, height])
|
|
3108
|
+
.padding(3)
|
|
3109
|
+
.paddingTop(20)
|
|
3110
|
+
.round(true)(root);
|
|
3111
|
+
|
|
3112
|
+
// Create SVG
|
|
3113
|
+
const svg = d3.select(containerEl)
|
|
3114
|
+
.append('svg')
|
|
3115
|
+
.attr('width', width)
|
|
3116
|
+
.attr('height', height);
|
|
3117
|
+
|
|
3118
|
+
// Color scale for structure types
|
|
3119
|
+
const typeColors = {
|
|
3120
|
+
'function': '#2563eb',
|
|
3121
|
+
'class': '#7c3aed',
|
|
3122
|
+
'method': '#0891b2',
|
|
3123
|
+
'interface': '#059669',
|
|
3124
|
+
'type': '#d97706',
|
|
3125
|
+
'variable': '#dc2626',
|
|
3126
|
+
'file': '#475569',
|
|
3127
|
+
'directory': '#334155',
|
|
3128
|
+
'structure': '#2563eb',
|
|
3129
|
+
};
|
|
3130
|
+
|
|
3131
|
+
// Get the direct children of root (depth 1)
|
|
3132
|
+
const children = root.children || [];
|
|
3133
|
+
|
|
3134
|
+
const cell = svg.selectAll('g')
|
|
3135
|
+
.data(children)
|
|
3136
|
+
.join('g')
|
|
3137
|
+
.attr('transform', d => \`translate(\${d.x0},\${d.y0})\`);
|
|
3138
|
+
|
|
3139
|
+
// Add background rect for each cell
|
|
3140
|
+
cell.append('rect')
|
|
3141
|
+
.attr('class', 'treemap-node')
|
|
3142
|
+
.attr('width', d => Math.max(0, d.x1 - d.x0))
|
|
3143
|
+
.attr('height', d => Math.max(0, d.y1 - d.y0))
|
|
3144
|
+
.attr('fill', d => typeColors[d.data.structureType] || typeColors[d.data.type] || '#475569')
|
|
3145
|
+
.attr('rx', 3)
|
|
3146
|
+
.style('cursor', d => (d.data.children && d.data.children.length > 0) ? 'pointer' : 'default')
|
|
3147
|
+
.on('click', (event, d) => {
|
|
3148
|
+
event.stopPropagation();
|
|
3149
|
+
if (d.data.children && d.data.children.length > 0) {
|
|
3150
|
+
// Has children - zoom in
|
|
3151
|
+
zoomToTreemapNode(d.data);
|
|
3152
|
+
} else {
|
|
3153
|
+
// No children - show details
|
|
3154
|
+
showSmellDetails(null, d.data.path);
|
|
3155
|
+
}
|
|
3156
|
+
});
|
|
3157
|
+
|
|
3158
|
+
// Add header label at top of cell
|
|
3159
|
+
cell.append('text')
|
|
3160
|
+
.attr('class', 'treemap-label')
|
|
3161
|
+
.attr('x', 6)
|
|
3162
|
+
.attr('y', 14)
|
|
3163
|
+
.attr('text-anchor', 'start')
|
|
3164
|
+
.attr('fill', 'white')
|
|
3165
|
+
.attr('font-size', '11px')
|
|
3166
|
+
.attr('font-weight', '500')
|
|
3167
|
+
.text(d => {
|
|
3168
|
+
const w = d.x1 - d.x0;
|
|
3169
|
+
if (w < 30) return '';
|
|
3170
|
+
const name = d.data.name;
|
|
3171
|
+
const maxChars = Math.floor((w - 12) / 6);
|
|
3172
|
+
return name.length > maxChars ? name.slice(0, maxChars - 1) + '…' : name;
|
|
3173
|
+
});
|
|
3174
|
+
|
|
3175
|
+
// Add value label below name
|
|
3176
|
+
cell.append('text')
|
|
3177
|
+
.attr('class', 'treemap-label')
|
|
3178
|
+
.attr('x', d => (d.x1 - d.x0) / 2)
|
|
3179
|
+
.attr('y', d => (d.y1 - d.y0) / 2 + 5)
|
|
3180
|
+
.attr('text-anchor', 'middle')
|
|
3181
|
+
.attr('fill', 'rgba(255,255,255,0.7)')
|
|
3182
|
+
.attr('font-size', '10px')
|
|
3183
|
+
.text(d => {
|
|
3184
|
+
const w = d.x1 - d.x0;
|
|
3185
|
+
const h = d.y1 - d.y0;
|
|
3186
|
+
if (w < 50 || h < 40) return '';
|
|
3187
|
+
return d.data.value + ' lines';
|
|
3188
|
+
});
|
|
3189
|
+
|
|
3190
|
+
// Add click indicator for zoomable nodes
|
|
3191
|
+
cell.append('text')
|
|
3192
|
+
.attr('x', d => d.x1 - d.x0 - 8)
|
|
3193
|
+
.attr('y', 14)
|
|
3194
|
+
.attr('text-anchor', 'end')
|
|
3195
|
+
.attr('fill', 'rgba(255,255,255,0.5)')
|
|
3196
|
+
.attr('font-size', '10px')
|
|
3197
|
+
.text(d => (d.data.children && d.data.children.length > 0) ? '→' : '');
|
|
3198
|
+
|
|
3199
|
+
// Add tooltips
|
|
3200
|
+
cell.append('title')
|
|
3201
|
+
.text(d => {
|
|
3202
|
+
const hasChildren = d.data.children && d.data.children.length > 0;
|
|
3203
|
+
return d.data.name + '\\n' +
|
|
3204
|
+
d.data.value + ' lines\\n' +
|
|
3205
|
+
(d.data.structureType || d.data.type) +
|
|
3206
|
+
(hasChildren ? '\\nClick to zoom in' : '');
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
function zoomToTreemapNode(node) {
|
|
3211
|
+
if (!node.children || node.children.length === 0) return;
|
|
3212
|
+
|
|
3213
|
+
treemapCurrent = node;
|
|
3214
|
+
updateTreemapBreadcrumb();
|
|
3215
|
+
renderTreemap(node);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
function updateTreemapBreadcrumb() {
|
|
3219
|
+
const breadcrumbEl = document.getElementById('treemap-breadcrumb');
|
|
3220
|
+
if (!breadcrumbEl) return;
|
|
3221
|
+
|
|
3222
|
+
// Build path from root to current
|
|
3223
|
+
const path = [];
|
|
3224
|
+
let node = treemapCurrent;
|
|
3225
|
+
|
|
3226
|
+
// Walk up using path comparison
|
|
3227
|
+
const findPath = (root, target, currentPath) => {
|
|
3228
|
+
if (root.path === target.path) {
|
|
3229
|
+
return [...currentPath, root];
|
|
3230
|
+
}
|
|
3231
|
+
if (root.children) {
|
|
3232
|
+
for (const child of root.children) {
|
|
3233
|
+
const result = findPath(child, target, [...currentPath, root]);
|
|
3234
|
+
if (result) return result;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
return null;
|
|
3238
|
+
};
|
|
3239
|
+
|
|
3240
|
+
const pathNodes = findPath(treemapRoot, treemapCurrent, []) || [treemapRoot];
|
|
3241
|
+
|
|
3242
|
+
breadcrumbEl.innerHTML = pathNodes.map((n, i) => {
|
|
3243
|
+
const isLast = i === pathNodes.length - 1;
|
|
3244
|
+
return \`<span class="\${isLast ? 'current' : ''}" data-path="\${n.path}">\${n.name}</span>\${isLast ? '' : ' / '}\`;
|
|
3245
|
+
}).join('');
|
|
3246
|
+
|
|
3247
|
+
// Add click listeners
|
|
3248
|
+
breadcrumbEl.querySelectorAll('span:not(.current)').forEach(span => {
|
|
3249
|
+
span.addEventListener('click', () => {
|
|
3250
|
+
const targetPath = span.dataset.path;
|
|
3251
|
+
const findNode = (root, path) => {
|
|
3252
|
+
if (root.path === path) return root;
|
|
3253
|
+
if (root.children) {
|
|
3254
|
+
for (const child of root.children) {
|
|
3255
|
+
const found = findNode(child, path);
|
|
3256
|
+
if (found) return found;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
return null;
|
|
3260
|
+
};
|
|
3261
|
+
const targetNode = findNode(treemapRoot, targetPath);
|
|
3262
|
+
if (targetNode) {
|
|
3263
|
+
zoomToTreemapNode(targetNode);
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
// Event listeners
|
|
3270
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
3271
|
+
tab.addEventListener('click', () => {
|
|
3272
|
+
// Clear history when manually switching tabs
|
|
3273
|
+
drillHistory = [];
|
|
3274
|
+
updateBackButton();
|
|
3275
|
+
switchView(tab.dataset.view);
|
|
3276
|
+
// Reset breadcrumb to just the view name
|
|
3277
|
+
updateBreadcrumb([{ label: getBreadcrumbLabel(tab.dataset.view) }]);
|
|
3278
|
+
});
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3281
|
+
document.getElementById('back-btn').addEventListener('click', goBack);
|
|
3282
|
+
|
|
3283
|
+
document.getElementById('layout-select').addEventListener('change', runLayout);
|
|
3284
|
+
|
|
3285
|
+
document.querySelectorAll('[id^="filter-"]').forEach(checkbox => {
|
|
3286
|
+
checkbox.addEventListener('change', applyFilters);
|
|
3287
|
+
});
|
|
3288
|
+
|
|
3289
|
+
document.getElementById('search').addEventListener('input', (e) => {
|
|
3290
|
+
searchNodes(e.target.value);
|
|
3291
|
+
});
|
|
3292
|
+
|
|
3293
|
+
// Visualize search results button
|
|
3294
|
+
document.getElementById('visualize-search-btn').addEventListener('click', visualizeSearchResults);
|
|
3295
|
+
|
|
3296
|
+
function visualizeSearchResults() {
|
|
3297
|
+
if (lastSearchMatches.length < 2) return;
|
|
3298
|
+
|
|
3299
|
+
// Get IDs of all matches
|
|
3300
|
+
const matchIds = new Set(lastSearchMatches.map(m => m.data.id));
|
|
3301
|
+
|
|
3302
|
+
// Find all edges between matches (using call graph data)
|
|
3303
|
+
const callGraphData = vizData.graphs.callGraph;
|
|
3304
|
+
const relevantEdges = [];
|
|
3305
|
+
const neighborIds = new Set();
|
|
3306
|
+
|
|
3307
|
+
if (callGraphData) {
|
|
3308
|
+
for (const edge of callGraphData.edges) {
|
|
3309
|
+
const sourceMatch = matchIds.has(edge.data.source);
|
|
3310
|
+
const targetMatch = matchIds.has(edge.data.target);
|
|
3311
|
+
|
|
3312
|
+
if (sourceMatch && targetMatch) {
|
|
3313
|
+
// Edge between two matches
|
|
3314
|
+
relevantEdges.push(edge);
|
|
3315
|
+
} else if (sourceMatch || targetMatch) {
|
|
3316
|
+
// Edge to/from a neighbor - add the neighbor
|
|
3317
|
+
if (sourceMatch) neighborIds.add(edge.data.target);
|
|
3318
|
+
if (targetMatch) neighborIds.add(edge.data.source);
|
|
3319
|
+
relevantEdges.push(edge);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// Build nodes: matches + neighbors (neighbors will be faded)
|
|
3325
|
+
const allNodes = [];
|
|
3326
|
+
const allNodeIds = new Set();
|
|
3327
|
+
|
|
3328
|
+
// Add match nodes
|
|
3329
|
+
for (const match of lastSearchMatches) {
|
|
3330
|
+
allNodes.push({
|
|
3331
|
+
...match,
|
|
3332
|
+
data: { ...match.data, searchMatch: true }
|
|
3333
|
+
});
|
|
3334
|
+
allNodeIds.add(match.data.id);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// Add neighbor nodes (from call graph)
|
|
3338
|
+
if (callGraphData) {
|
|
3339
|
+
for (const node of callGraphData.nodes) {
|
|
3340
|
+
if (neighborIds.has(node.data.id) && !allNodeIds.has(node.data.id)) {
|
|
3341
|
+
allNodes.push({
|
|
3342
|
+
...node,
|
|
3343
|
+
data: { ...node.data, searchMatch: false }
|
|
3344
|
+
});
|
|
3345
|
+
allNodeIds.add(node.data.id);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// Filter edges to only include those with both endpoints in our node set
|
|
3351
|
+
const finalEdges = relevantEdges.filter(e =>
|
|
3352
|
+
allNodeIds.has(e.data.source) && allNodeIds.has(e.data.target)
|
|
3353
|
+
);
|
|
3354
|
+
|
|
3355
|
+
// Clear drill history and switch to this custom view
|
|
3356
|
+
drillHistory = [];
|
|
3357
|
+
updateBackButton();
|
|
3358
|
+
|
|
3359
|
+
// Initialize cytoscape with custom styles for matches vs neighbors
|
|
3360
|
+
destroyCy();
|
|
3361
|
+
|
|
3362
|
+
// Show cy container
|
|
3363
|
+
document.getElementById('cy').style.display = 'block';
|
|
3364
|
+
document.getElementById('list-view').style.display = 'none';
|
|
3365
|
+
document.getElementById('legend').style.display = 'block';
|
|
3366
|
+
|
|
3367
|
+
// Hide custom views
|
|
3368
|
+
['smells', 'hotspots', 'gallery', 'timeline', 'treemap'].forEach(v => {
|
|
3369
|
+
const el = document.getElementById(v + '-view');
|
|
3370
|
+
if (el) el.classList.remove('active');
|
|
3371
|
+
});
|
|
3372
|
+
|
|
3373
|
+
initCytoscape({ nodes: allNodes, edges: finalEdges });
|
|
3374
|
+
|
|
3375
|
+
// Apply styles: highlight matches, fade neighbors
|
|
3376
|
+
if (cy) {
|
|
3377
|
+
cy.nodes().forEach(node => {
|
|
3378
|
+
if (node.data('searchMatch') === false) {
|
|
3379
|
+
node.style('opacity', 0.4);
|
|
3380
|
+
}
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// Update description
|
|
3385
|
+
document.getElementById('view-description').innerHTML =
|
|
3386
|
+
'<strong>Search Results Graph</strong> - ' + lastSearchMatches.length + ' matches for "' +
|
|
3387
|
+
escapeHtml(lastSearchQuery) + '" with their connections. Faded nodes are neighbors.';
|
|
3388
|
+
|
|
3389
|
+
// Update breadcrumb
|
|
3390
|
+
updateBreadcrumb([{ label: 'Search: ' + lastSearchQuery }]);
|
|
3391
|
+
|
|
3392
|
+
// Clear tab active state
|
|
3393
|
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
// View mode toggle (visual/list)
|
|
3397
|
+
document.querySelectorAll('.view-toggle-btn').forEach(btn => {
|
|
3398
|
+
btn.addEventListener('click', () => setViewMode(btn.dataset.mode));
|
|
3399
|
+
});
|
|
3400
|
+
|
|
3401
|
+
// Flow mode radio buttons
|
|
3402
|
+
document.querySelectorAll('input[name="flow-mode"]').forEach(radio => {
|
|
3403
|
+
radio.addEventListener('change', (e) => {
|
|
3404
|
+
flowMode = e.target.value;
|
|
3405
|
+
// Update hint text based on mode
|
|
3406
|
+
const hint = document.querySelector('#flow-mode-group p');
|
|
3407
|
+
if (hint) {
|
|
3408
|
+
if (flowMode === 'connections') {
|
|
3409
|
+
hint.textContent = 'Click a function to see its connections';
|
|
3410
|
+
} else if (flowMode === 'downstream') {
|
|
3411
|
+
hint.textContent = 'Click a function to trace what it calls';
|
|
3412
|
+
} else if (flowMode === 'upstream') {
|
|
3413
|
+
hint.textContent = 'Click a function to trace what calls it';
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
// Fullscreen toggle
|
|
3420
|
+
function toggleFullscreen() {
|
|
3421
|
+
const isFullscreen = document.body.classList.toggle('fullscreen');
|
|
3422
|
+
document.getElementById('exit-fullscreen').style.display = isFullscreen ? 'block' : 'none';
|
|
3423
|
+
|
|
3424
|
+
// When exiting fullscreen, reinitialize the view to fix layout
|
|
3425
|
+
if (!isFullscreen) {
|
|
3426
|
+
setTimeout(() => {
|
|
3427
|
+
switchView(currentView);
|
|
3428
|
+
}, 300);
|
|
3429
|
+
} else if (cy && currentViewMode === 'visual') {
|
|
3430
|
+
// Entering fullscreen - just resize
|
|
3431
|
+
setTimeout(() => {
|
|
3432
|
+
cy.resize();
|
|
3433
|
+
cy.fit(50);
|
|
3434
|
+
}, 100);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullscreen);
|
|
3439
|
+
document.getElementById('exit-fullscreen').addEventListener('click', toggleFullscreen);
|
|
3440
|
+
document.getElementById('close-details').addEventListener('click', clearDetails);
|
|
3441
|
+
|
|
3442
|
+
// ESC key to exit fullscreen
|
|
3443
|
+
document.addEventListener('keydown', (e) => {
|
|
3444
|
+
if (e.key === 'Escape' && document.body.classList.contains('fullscreen')) {
|
|
3445
|
+
toggleFullscreen();
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
// Initialize
|
|
3450
|
+
switchView('overview');
|
|
3451
|
+
updateBreadcrumb([{ label: 'Files' }]);
|
|
3452
|
+
maybeShowWelcome();
|
|
3453
|
+
</script>
|
|
3454
|
+
</body>
|
|
3455
|
+
</html>`;
|
|
3456
|
+
}
|
|
3457
|
+
function escapeHtml(text) {
|
|
3458
|
+
return text
|
|
3459
|
+
.replace(/&/g, '&')
|
|
3460
|
+
.replace(/</g, '<')
|
|
3461
|
+
.replace(/>/g, '>')
|
|
3462
|
+
.replace(/"/g, '"')
|
|
3463
|
+
.replace(/'/g, ''');
|
|
3464
|
+
}
|
|
3465
|
+
//# sourceMappingURL=template.js.map
|