skill-viewer 0.1.2
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 +54 -0
- package/dist/cli.js +1043 -0
- package/package.json +41 -0
- package/public/index.html +2219 -0
|
@@ -0,0 +1,2219 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Skills Viewer</title>
|
|
7
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
9
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
* {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--bg-primary: #ffffff;
|
|
19
|
+
--bg-secondary: #f5f5f5;
|
|
20
|
+
--bg-tertiary: #e8e8e8;
|
|
21
|
+
--text-primary: #1a1a1a;
|
|
22
|
+
--text-secondary: #555555;
|
|
23
|
+
--text-muted: #888888;
|
|
24
|
+
--accent: #0066cc;
|
|
25
|
+
--accent-hover: #0052a3;
|
|
26
|
+
--border: #dddddd;
|
|
27
|
+
--success: #28a745;
|
|
28
|
+
--warning: #f0ad4e;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
body {
|
|
32
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
33
|
+
background: var(--bg-primary);
|
|
34
|
+
color: var(--text-primary);
|
|
35
|
+
height: 100vh;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
header {
|
|
41
|
+
background: var(--bg-tertiary);
|
|
42
|
+
padding: 12px 20px;
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 16px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
header h1 {
|
|
50
|
+
font-size: 18px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
header .status {
|
|
55
|
+
font-size: 12px;
|
|
56
|
+
color: var(--text-muted);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.agent-dropdown {
|
|
60
|
+
position: relative;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.agent-dropdown-toggle {
|
|
64
|
+
background: var(--bg-secondary);
|
|
65
|
+
border: 1px solid var(--border);
|
|
66
|
+
border-radius: 6px;
|
|
67
|
+
padding: 4px 8px 4px 10px;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
font-size: 13px;
|
|
70
|
+
color: var(--text-secondary);
|
|
71
|
+
font-family: inherit;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 4px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.agent-dropdown-toggle:hover {
|
|
78
|
+
background: var(--bg-primary);
|
|
79
|
+
color: var(--text-primary);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.agent-dropdown.open .agent-dropdown-toggle {
|
|
83
|
+
background: var(--bg-primary);
|
|
84
|
+
border-color: var(--accent);
|
|
85
|
+
color: var(--text-primary);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.agent-dropdown-arrow {
|
|
89
|
+
font-size: 10px;
|
|
90
|
+
opacity: 0.6;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.agent-dropdown-menu {
|
|
94
|
+
display: none;
|
|
95
|
+
position: absolute;
|
|
96
|
+
top: 100%;
|
|
97
|
+
right: 0;
|
|
98
|
+
margin-top: 4px;
|
|
99
|
+
background: var(--bg-primary);
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
border-radius: 6px;
|
|
102
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
103
|
+
min-width: 160px;
|
|
104
|
+
z-index: 1000;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.agent-dropdown.open .agent-dropdown-menu {
|
|
109
|
+
display: block;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.agent-dropdown-item {
|
|
113
|
+
padding: 7px 12px;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
color: var(--text-secondary);
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: space-between;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.agent-dropdown-item:hover {
|
|
123
|
+
background: var(--bg-secondary);
|
|
124
|
+
color: var(--text-primary);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.agent-dropdown-item.active {
|
|
128
|
+
color: var(--accent);
|
|
129
|
+
font-weight: 500;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.agent-dropdown-item.active::after {
|
|
133
|
+
content: '\2713';
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.container {
|
|
138
|
+
display: grid;
|
|
139
|
+
grid-template-columns: 220px 320px 1fr;
|
|
140
|
+
height: calc(100vh - 49px);
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.panel {
|
|
145
|
+
background: var(--bg-secondary);
|
|
146
|
+
border-right: 1px solid var(--border);
|
|
147
|
+
overflow-y: auto;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.panel::-webkit-scrollbar {
|
|
151
|
+
width: 8px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.panel::-webkit-scrollbar-track {
|
|
155
|
+
background: var(--bg-tertiary);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.panel::-webkit-scrollbar-thumb {
|
|
159
|
+
background: var(--border);
|
|
160
|
+
border-radius: 4px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Sources Panel */
|
|
164
|
+
.sources-panel .section {
|
|
165
|
+
padding: 4px 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.sources-panel .section-header {
|
|
169
|
+
padding: 6px 12px;
|
|
170
|
+
font-size: 11px;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
color: var(--text-muted);
|
|
173
|
+
text-transform: uppercase;
|
|
174
|
+
letter-spacing: 0.5px;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 6px;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.sources-panel .section-header:hover {
|
|
182
|
+
color: var(--text-secondary);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.sources-panel .section-header .toggle {
|
|
186
|
+
font-size: 10px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.sources-panel .section-header .count {
|
|
190
|
+
background: var(--border);
|
|
191
|
+
padding: 1px 6px;
|
|
192
|
+
border-radius: 10px;
|
|
193
|
+
font-size: 10px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Sidebar item — every clickable leaf in the sidebar */
|
|
197
|
+
.sb-item {
|
|
198
|
+
position: relative;
|
|
199
|
+
padding: 3px 12px 3px 20px;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
font-size: 13px;
|
|
202
|
+
color: var(--text-primary);
|
|
203
|
+
border-left: 3px solid transparent;
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
text-overflow: ellipsis;
|
|
206
|
+
white-space: nowrap;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.sb-item:hover {
|
|
210
|
+
background: var(--bg-tertiary);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.sb-item.active {
|
|
214
|
+
background: var(--bg-tertiary);
|
|
215
|
+
border-left-color: var(--accent);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Source-type left border indicators */
|
|
219
|
+
.sb-item.sb-plugin {
|
|
220
|
+
border-left-color: #c8d6e5;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.sb-item.sb-user {
|
|
224
|
+
border-left-color: #d5c4f2;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.sb-item.sb-plugin.active {
|
|
228
|
+
border-left-color: var(--accent);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.sb-item.sb-user.active {
|
|
232
|
+
border-left-color: var(--accent);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Nested items under a group */
|
|
236
|
+
.sb-item.sb-nested {
|
|
237
|
+
padding-left: 32px;
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Group header — small muted uppercase label with toggle */
|
|
242
|
+
.sb-group {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
gap: 4px;
|
|
246
|
+
padding: 8px 12px 2px 14px;
|
|
247
|
+
font-size: 10px;
|
|
248
|
+
font-weight: 600;
|
|
249
|
+
color: var(--text-muted);
|
|
250
|
+
text-transform: uppercase;
|
|
251
|
+
letter-spacing: 0.4px;
|
|
252
|
+
cursor: pointer;
|
|
253
|
+
user-select: none;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.sb-group:hover {
|
|
257
|
+
color: var(--text-secondary);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.sb-group .sb-toggle {
|
|
261
|
+
font-size: 8px;
|
|
262
|
+
width: 8px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.sb-group .sb-group-name {
|
|
266
|
+
flex: 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.sb-group .sb-group-count {
|
|
270
|
+
font-size: 10px;
|
|
271
|
+
font-weight: 400;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.sb-group-children {
|
|
275
|
+
display: none;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.sb-group-wrap.expanded > .sb-group-children {
|
|
279
|
+
display: block;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Meta text (e.g. item count on projects) */
|
|
283
|
+
.sb-meta {
|
|
284
|
+
float: right;
|
|
285
|
+
font-size: 10px;
|
|
286
|
+
color: var(--text-muted);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Project add form */
|
|
290
|
+
.project-add-form {
|
|
291
|
+
padding: 6px 12px 6px 20px;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.project-add-row {
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: 6px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.project-add-row input {
|
|
300
|
+
flex: 1;
|
|
301
|
+
min-width: 0;
|
|
302
|
+
padding: 4px 8px;
|
|
303
|
+
border: 1px solid var(--border);
|
|
304
|
+
border-radius: 4px;
|
|
305
|
+
background: var(--bg-primary);
|
|
306
|
+
color: var(--text-primary);
|
|
307
|
+
font-size: 12px;
|
|
308
|
+
font-family: inherit;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.project-add-row input:focus {
|
|
312
|
+
outline: none;
|
|
313
|
+
border-color: var(--accent);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.project-add-row button {
|
|
317
|
+
padding: 4px 10px;
|
|
318
|
+
border: none;
|
|
319
|
+
border-radius: 4px;
|
|
320
|
+
background: var(--accent);
|
|
321
|
+
color: #fff;
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
font-size: 12px;
|
|
324
|
+
font-family: inherit;
|
|
325
|
+
white-space: nowrap;
|
|
326
|
+
flex-shrink: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.project-add-row button:hover {
|
|
330
|
+
background: var(--accent-hover);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.project-error {
|
|
334
|
+
color: #cc3333;
|
|
335
|
+
font-size: 11px;
|
|
336
|
+
margin-top: 4px;
|
|
337
|
+
display: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.remove-project-btn {
|
|
341
|
+
position: absolute;
|
|
342
|
+
right: 6px;
|
|
343
|
+
top: 50%;
|
|
344
|
+
transform: translateY(-50%);
|
|
345
|
+
background: none;
|
|
346
|
+
border: none;
|
|
347
|
+
color: var(--text-muted);
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
font-size: 12px;
|
|
350
|
+
padding: 2px 4px;
|
|
351
|
+
border-radius: 3px;
|
|
352
|
+
opacity: 0;
|
|
353
|
+
transition: opacity 0.1s;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.sb-item:hover .remove-project-btn {
|
|
357
|
+
opacity: 1;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.remove-project-btn:hover {
|
|
361
|
+
color: #cc3333;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Skills Panel */
|
|
365
|
+
.skills-panel {
|
|
366
|
+
display: flex;
|
|
367
|
+
flex-direction: column;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.search-box {
|
|
371
|
+
padding: 12px 16px;
|
|
372
|
+
background: var(--bg-tertiary);
|
|
373
|
+
border-bottom: 1px solid var(--border);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.search-box input {
|
|
377
|
+
width: 100%;
|
|
378
|
+
padding: 8px 12px;
|
|
379
|
+
background: var(--bg-secondary);
|
|
380
|
+
border: 1px solid var(--border);
|
|
381
|
+
border-radius: 6px;
|
|
382
|
+
color: var(--text-primary);
|
|
383
|
+
font-size: 14px;
|
|
384
|
+
outline: none;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.search-box input:focus {
|
|
388
|
+
border-color: var(--accent);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.search-box input::placeholder {
|
|
392
|
+
color: var(--text-muted);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.skills-list {
|
|
396
|
+
flex: 1;
|
|
397
|
+
overflow-y: auto;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.skills-list .skill-item {
|
|
401
|
+
border-bottom: 1px solid var(--border);
|
|
402
|
+
transition: background 0.15s ease;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.skills-list .skill-item .skill-header {
|
|
406
|
+
padding: 12px 16px;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: flex-start;
|
|
410
|
+
gap: 8px;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.skills-list .skill-item .skill-header:hover {
|
|
414
|
+
background: var(--bg-tertiary);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.skills-list .skill-item.active .skill-header {
|
|
418
|
+
background: var(--bg-tertiary);
|
|
419
|
+
border-left: 3px solid var(--accent);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.skills-list .skill-item .skill-toggle {
|
|
423
|
+
font-size: 10px;
|
|
424
|
+
color: var(--text-muted);
|
|
425
|
+
margin-top: 4px;
|
|
426
|
+
flex-shrink: 0;
|
|
427
|
+
width: 12px;
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
user-select: none;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.skills-list .skill-item .skill-toggle:hover {
|
|
433
|
+
color: var(--accent);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.skills-list .skill-item .skill-info {
|
|
437
|
+
flex: 1;
|
|
438
|
+
min-width: 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.skills-list .skill-item .name {
|
|
442
|
+
font-size: 14px;
|
|
443
|
+
font-weight: 500;
|
|
444
|
+
margin-bottom: 4px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.skills-list .skill-item .description {
|
|
448
|
+
font-size: 12px;
|
|
449
|
+
color: var(--text-secondary);
|
|
450
|
+
overflow: hidden;
|
|
451
|
+
text-overflow: ellipsis;
|
|
452
|
+
white-space: nowrap;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.skills-list .skill-item .filename {
|
|
456
|
+
font-size: 11px;
|
|
457
|
+
color: var(--text-muted);
|
|
458
|
+
margin-top: 4px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.skills-list .skill-item .inline-file-tree {
|
|
462
|
+
display: none;
|
|
463
|
+
padding: 4px 16px 12px 36px;
|
|
464
|
+
border-top: 1px solid var(--border);
|
|
465
|
+
background: var(--bg-tertiary);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.skills-list .skill-item .inline-file-tree.expanded {
|
|
469
|
+
display: block;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.skills-list .skill-item .inline-file-tree .file-tree-item {
|
|
473
|
+
padding: 4px 6px;
|
|
474
|
+
font-size: 12px;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.skills-list .skill-item .inline-file-tree .file-tree-item .size {
|
|
478
|
+
font-size: 10px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.empty-state {
|
|
482
|
+
padding: 40px 20px;
|
|
483
|
+
text-align: center;
|
|
484
|
+
color: var(--text-muted);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* Detail Panel */
|
|
488
|
+
.detail-panel {
|
|
489
|
+
background: var(--bg-primary);
|
|
490
|
+
overflow-y: auto;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.detail-panel .empty {
|
|
494
|
+
display: flex;
|
|
495
|
+
align-items: center;
|
|
496
|
+
justify-content: center;
|
|
497
|
+
height: 100%;
|
|
498
|
+
color: var(--text-muted);
|
|
499
|
+
font-size: 14px;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.detail-content {
|
|
503
|
+
padding: 20px;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.detail-header {
|
|
507
|
+
margin-bottom: 20px;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.detail-header h2 {
|
|
511
|
+
font-size: 24px;
|
|
512
|
+
font-weight: 600;
|
|
513
|
+
margin-bottom: 8px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.detail-header .description {
|
|
517
|
+
font-size: 14px;
|
|
518
|
+
color: var(--text-secondary);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.detail-header .path {
|
|
522
|
+
font-size: 12px;
|
|
523
|
+
color: var(--text-muted);
|
|
524
|
+
margin-top: 8px;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.section-title {
|
|
528
|
+
font-size: 12px;
|
|
529
|
+
font-weight: 600;
|
|
530
|
+
color: var(--text-muted);
|
|
531
|
+
text-transform: uppercase;
|
|
532
|
+
letter-spacing: 0.5px;
|
|
533
|
+
margin-bottom: 12px;
|
|
534
|
+
padding-bottom: 8px;
|
|
535
|
+
border-bottom: 1px solid var(--border);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.frontmatter-table {
|
|
539
|
+
display: grid;
|
|
540
|
+
gap: 8px;
|
|
541
|
+
margin-bottom: 24px;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.frontmatter-row {
|
|
545
|
+
display: grid;
|
|
546
|
+
grid-template-columns: 120px 1fr;
|
|
547
|
+
background: var(--bg-secondary);
|
|
548
|
+
padding: 8px 12px;
|
|
549
|
+
border-radius: 6px;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.frontmatter-row .key {
|
|
553
|
+
font-size: 12px;
|
|
554
|
+
font-weight: 500;
|
|
555
|
+
color: var(--accent);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.frontmatter-row .value {
|
|
559
|
+
font-size: 12px;
|
|
560
|
+
color: var(--text-primary);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.content-preview {
|
|
564
|
+
background: var(--bg-secondary);
|
|
565
|
+
padding: 16px;
|
|
566
|
+
border-radius: 8px;
|
|
567
|
+
margin-bottom: 24px;
|
|
568
|
+
overflow-x: auto;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.content-preview pre {
|
|
572
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
573
|
+
font-size: 13px;
|
|
574
|
+
line-height: 1.6;
|
|
575
|
+
white-space: pre-wrap;
|
|
576
|
+
word-wrap: break-word;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.content-preview pre code.hljs {
|
|
580
|
+
background: transparent;
|
|
581
|
+
padding: 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.references-tree {
|
|
585
|
+
margin-bottom: 24px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.reference-group {
|
|
589
|
+
margin-bottom: 16px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.reference-item {
|
|
593
|
+
display: flex;
|
|
594
|
+
align-items: center;
|
|
595
|
+
padding: 8px 12px;
|
|
596
|
+
background: var(--bg-secondary);
|
|
597
|
+
border-radius: 6px;
|
|
598
|
+
margin-bottom: 4px;
|
|
599
|
+
cursor: pointer;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.reference-item:hover {
|
|
603
|
+
background: var(--bg-tertiary);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.reference-item .icon {
|
|
607
|
+
margin-right: 8px;
|
|
608
|
+
font-size: 12px;
|
|
609
|
+
color: var(--text-muted);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.reference-item .name {
|
|
613
|
+
font-size: 13px;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.reference-dir {
|
|
617
|
+
margin-bottom: 8px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.reference-dir-header {
|
|
621
|
+
display: flex;
|
|
622
|
+
align-items: center;
|
|
623
|
+
padding: 8px 12px;
|
|
624
|
+
background: var(--bg-tertiary);
|
|
625
|
+
border-radius: 6px;
|
|
626
|
+
cursor: pointer;
|
|
627
|
+
margin-bottom: 4px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.reference-dir-header:hover {
|
|
631
|
+
background: var(--bg-secondary);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.reference-dir-header .toggle {
|
|
635
|
+
margin-right: 8px;
|
|
636
|
+
font-size: 10px;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.reference-dir-header .name {
|
|
640
|
+
font-size: 13px;
|
|
641
|
+
color: var(--text-secondary);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.reference-dir-children {
|
|
645
|
+
padding-left: 16px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.file-tree {
|
|
649
|
+
margin-bottom: 24px;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.file-tree-item {
|
|
653
|
+
display: flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
padding: 6px 8px;
|
|
656
|
+
cursor: pointer;
|
|
657
|
+
border-radius: 4px;
|
|
658
|
+
transition: background 0.1s ease;
|
|
659
|
+
font-size: 13px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.file-tree-item:hover {
|
|
663
|
+
background: var(--bg-secondary);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.file-tree-item .indent {
|
|
667
|
+
display: inline-block;
|
|
668
|
+
width: 16px;
|
|
669
|
+
flex-shrink: 0;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.file-tree-item .icon {
|
|
673
|
+
margin-right: 6px;
|
|
674
|
+
font-size: 12px;
|
|
675
|
+
flex-shrink: 0;
|
|
676
|
+
width: 16px;
|
|
677
|
+
text-align: center;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.file-tree-item .name {
|
|
681
|
+
flex: 1;
|
|
682
|
+
overflow: hidden;
|
|
683
|
+
text-overflow: ellipsis;
|
|
684
|
+
white-space: nowrap;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.file-tree-item .size {
|
|
688
|
+
font-size: 11px;
|
|
689
|
+
color: var(--text-muted);
|
|
690
|
+
margin-left: 8px;
|
|
691
|
+
flex-shrink: 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.file-tree-item.directory > .name {
|
|
695
|
+
font-weight: 500;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.file-tree-children {
|
|
699
|
+
display: none;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.file-tree-children.expanded {
|
|
703
|
+
display: block;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.modal {
|
|
707
|
+
display: none;
|
|
708
|
+
position: fixed;
|
|
709
|
+
top: 0;
|
|
710
|
+
left: 0;
|
|
711
|
+
width: 100%;
|
|
712
|
+
height: 100%;
|
|
713
|
+
background: rgba(0, 0, 0, 0.5);
|
|
714
|
+
z-index: 1000;
|
|
715
|
+
justify-content: center;
|
|
716
|
+
align-items: center;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.modal.active {
|
|
720
|
+
display: flex;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.modal-content {
|
|
724
|
+
background: var(--bg-primary);
|
|
725
|
+
width: 80%;
|
|
726
|
+
max-width: 800px;
|
|
727
|
+
max-height: 80vh;
|
|
728
|
+
border-radius: 12px;
|
|
729
|
+
overflow: hidden;
|
|
730
|
+
display: flex;
|
|
731
|
+
flex-direction: column;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.modal-header {
|
|
735
|
+
padding: 16px 20px;
|
|
736
|
+
background: var(--bg-tertiary);
|
|
737
|
+
border-bottom: 1px solid var(--border);
|
|
738
|
+
display: flex;
|
|
739
|
+
justify-content: space-between;
|
|
740
|
+
align-items: center;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.modal-header h3 {
|
|
744
|
+
font-size: 16px;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.modal-close {
|
|
748
|
+
cursor: pointer;
|
|
749
|
+
color: var(--text-muted);
|
|
750
|
+
font-size: 20px;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.modal-close:hover {
|
|
754
|
+
color: var(--text-primary);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.modal-body {
|
|
758
|
+
padding: 20px;
|
|
759
|
+
overflow-y: auto;
|
|
760
|
+
flex: 1;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.modal-body pre {
|
|
764
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
765
|
+
font-size: 13px;
|
|
766
|
+
line-height: 1.6;
|
|
767
|
+
white-space: pre-wrap;
|
|
768
|
+
word-wrap: break-word;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.loading {
|
|
772
|
+
display: flex;
|
|
773
|
+
align-items: center;
|
|
774
|
+
justify-content: center;
|
|
775
|
+
padding: 20px;
|
|
776
|
+
color: var(--text-muted);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.error {
|
|
780
|
+
padding: 20px;
|
|
781
|
+
background: rgba(220, 53, 69, 0.1);
|
|
782
|
+
border: 1px solid rgba(220, 53, 69, 0.3);
|
|
783
|
+
border-radius: 8px;
|
|
784
|
+
color: #dc3545;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.global-search {
|
|
788
|
+
position: relative;
|
|
789
|
+
flex: 1;
|
|
790
|
+
max-width: 400px;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.global-search input {
|
|
794
|
+
width: 100%;
|
|
795
|
+
padding: 6px 12px 6px 28px;
|
|
796
|
+
background: var(--bg-secondary);
|
|
797
|
+
border: 1px solid var(--border);
|
|
798
|
+
border-radius: 6px;
|
|
799
|
+
color: var(--text-primary);
|
|
800
|
+
font-size: 13px;
|
|
801
|
+
outline: none;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.global-search input:focus {
|
|
805
|
+
border-color: var(--accent);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.global-search .search-icon {
|
|
809
|
+
position: absolute;
|
|
810
|
+
left: 8px;
|
|
811
|
+
top: 50%;
|
|
812
|
+
transform: translateY(-50%);
|
|
813
|
+
font-size: 12px;
|
|
814
|
+
color: var(--text-muted);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.global-search-results {
|
|
818
|
+
display: none;
|
|
819
|
+
position: absolute;
|
|
820
|
+
top: 100%;
|
|
821
|
+
left: 0;
|
|
822
|
+
right: 0;
|
|
823
|
+
background: var(--bg-primary);
|
|
824
|
+
border: 1px solid var(--border);
|
|
825
|
+
border-radius: 0 0 8px 8px;
|
|
826
|
+
max-height: 400px;
|
|
827
|
+
overflow-y: auto;
|
|
828
|
+
z-index: 100;
|
|
829
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.global-search-results.active {
|
|
833
|
+
display: block;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.search-result-item {
|
|
837
|
+
padding: 10px 12px;
|
|
838
|
+
cursor: pointer;
|
|
839
|
+
border-bottom: 1px solid var(--border);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.search-result-item:hover {
|
|
843
|
+
background: var(--bg-secondary);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.search-result-item:last-child {
|
|
847
|
+
border-bottom: none;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.search-result-item .result-name {
|
|
851
|
+
font-size: 13px;
|
|
852
|
+
font-weight: 500;
|
|
853
|
+
margin-bottom: 2px;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.search-result-item .result-source {
|
|
857
|
+
font-size: 11px;
|
|
858
|
+
color: var(--accent);
|
|
859
|
+
margin-bottom: 4px;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.search-result-item .result-snippet {
|
|
863
|
+
font-size: 12px;
|
|
864
|
+
color: var(--text-secondary);
|
|
865
|
+
line-height: 1.4;
|
|
866
|
+
overflow: hidden;
|
|
867
|
+
text-overflow: ellipsis;
|
|
868
|
+
white-space: nowrap;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.skill-item .badges {
|
|
872
|
+
display: flex;
|
|
873
|
+
gap: 6px;
|
|
874
|
+
margin-top: 6px;
|
|
875
|
+
flex-wrap: wrap;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.badge {
|
|
879
|
+
display: inline-flex;
|
|
880
|
+
align-items: center;
|
|
881
|
+
padding: 2px 6px;
|
|
882
|
+
border-radius: 4px;
|
|
883
|
+
font-size: 10px;
|
|
884
|
+
font-weight: 500;
|
|
885
|
+
gap: 3px;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.badge.recency-fresh {
|
|
889
|
+
background: rgba(40, 167, 69, 0.1);
|
|
890
|
+
color: #28a745;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.badge.recency-aging {
|
|
894
|
+
background: rgba(240, 173, 78, 0.1);
|
|
895
|
+
color: #c89100;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.badge.recency-stale {
|
|
899
|
+
background: rgba(220, 53, 69, 0.1);
|
|
900
|
+
color: #dc3545;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.badge.tag {
|
|
904
|
+
background: rgba(0, 102, 204, 0.08);
|
|
905
|
+
color: var(--accent);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.badge.gap {
|
|
909
|
+
background: rgba(220, 53, 69, 0.06);
|
|
910
|
+
color: #dc3545;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.tag-filters {
|
|
914
|
+
padding: 8px 16px;
|
|
915
|
+
background: var(--bg-tertiary);
|
|
916
|
+
border-bottom: 1px solid var(--border);
|
|
917
|
+
display: flex;
|
|
918
|
+
gap: 4px;
|
|
919
|
+
flex-wrap: wrap;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.tag-filter {
|
|
923
|
+
padding: 2px 8px;
|
|
924
|
+
border-radius: 12px;
|
|
925
|
+
font-size: 11px;
|
|
926
|
+
cursor: pointer;
|
|
927
|
+
background: var(--bg-secondary);
|
|
928
|
+
color: var(--text-secondary);
|
|
929
|
+
border: 1px solid transparent;
|
|
930
|
+
transition: all 0.1s ease;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.tag-filter:hover {
|
|
934
|
+
border-color: var(--accent);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.tag-filter.active {
|
|
938
|
+
background: var(--accent);
|
|
939
|
+
color: white;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.graph-overlay {
|
|
943
|
+
display: none;
|
|
944
|
+
position: fixed;
|
|
945
|
+
top: 49px;
|
|
946
|
+
left: 0;
|
|
947
|
+
right: 0;
|
|
948
|
+
bottom: 0;
|
|
949
|
+
background: rgba(255, 255, 255, 0.97);
|
|
950
|
+
z-index: 50;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.graph-overlay.active {
|
|
954
|
+
display: block;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.graph-overlay svg {
|
|
958
|
+
width: 100%;
|
|
959
|
+
height: 100%;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.graph-overlay .graph-controls {
|
|
963
|
+
position: absolute;
|
|
964
|
+
top: 12px;
|
|
965
|
+
right: 12px;
|
|
966
|
+
display: flex;
|
|
967
|
+
gap: 8px;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.graph-overlay .graph-controls button {
|
|
971
|
+
background: var(--bg-secondary);
|
|
972
|
+
border: 1px solid var(--border);
|
|
973
|
+
border-radius: 6px;
|
|
974
|
+
padding: 4px 10px;
|
|
975
|
+
cursor: pointer;
|
|
976
|
+
font-size: 12px;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.graph-node {
|
|
980
|
+
cursor: pointer;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.graph-node circle {
|
|
984
|
+
stroke: var(--border);
|
|
985
|
+
stroke-width: 1.5px;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.graph-node text {
|
|
989
|
+
font-size: 10px;
|
|
990
|
+
fill: var(--text-primary);
|
|
991
|
+
pointer-events: none;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.graph-link {
|
|
995
|
+
stroke: var(--border);
|
|
996
|
+
stroke-opacity: 0.6;
|
|
997
|
+
fill: none;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.graph-link-arrow {
|
|
1001
|
+
fill: var(--border);
|
|
1002
|
+
opacity: 0.6;
|
|
1003
|
+
}
|
|
1004
|
+
</style>
|
|
1005
|
+
</head>
|
|
1006
|
+
<body>
|
|
1007
|
+
<header>
|
|
1008
|
+
<h1>Skills Viewer</h1>
|
|
1009
|
+
<div class="global-search">
|
|
1010
|
+
<span class="search-icon">🔍</span>
|
|
1011
|
+
<input type="text" id="global-search-input" placeholder="Search all skills... (Cmd+K)">
|
|
1012
|
+
<div class="global-search-results" id="global-search-results"></div>
|
|
1013
|
+
</div>
|
|
1014
|
+
<span class="status" id="status">Loading...</span>
|
|
1015
|
+
<div style="margin-left: auto; display: flex; align-items: center; gap: 8px;">
|
|
1016
|
+
<div class="agent-dropdown" id="agent-dropdown">
|
|
1017
|
+
<button class="agent-dropdown-toggle" id="agent-dropdown-toggle">
|
|
1018
|
+
<span id="agent-dropdown-label">Claude Code</span>
|
|
1019
|
+
<span class="agent-dropdown-arrow">▾</span>
|
|
1020
|
+
</button>
|
|
1021
|
+
<div class="agent-dropdown-menu" id="agent-dropdown-menu"></div>
|
|
1022
|
+
</div>
|
|
1023
|
+
<button id="graph-toggle" title="Dependency Graph" style="
|
|
1024
|
+
background: var(--bg-secondary);
|
|
1025
|
+
border: 1px solid var(--border);
|
|
1026
|
+
border-radius: 6px;
|
|
1027
|
+
padding: 4px 10px;
|
|
1028
|
+
cursor: pointer;
|
|
1029
|
+
font-size: 13px;
|
|
1030
|
+
color: var(--text-secondary);
|
|
1031
|
+
">Graph</button>
|
|
1032
|
+
</div>
|
|
1033
|
+
</header>
|
|
1034
|
+
|
|
1035
|
+
<div class="container">
|
|
1036
|
+
<!-- Sources Panel -->
|
|
1037
|
+
<div class="panel sources-panel" id="sources-panel">
|
|
1038
|
+
<div class="section" id="skills-section">
|
|
1039
|
+
<div class="section-header">
|
|
1040
|
+
<span class="toggle">▼</span>
|
|
1041
|
+
Skills
|
|
1042
|
+
<span class="count" id="skills-count">0</span>
|
|
1043
|
+
</div>
|
|
1044
|
+
<div class="section-items" id="skills-items"></div>
|
|
1045
|
+
</div>
|
|
1046
|
+
<div class="section" id="commands-section">
|
|
1047
|
+
<div class="section-header">
|
|
1048
|
+
<span class="toggle">▼</span>
|
|
1049
|
+
Commands
|
|
1050
|
+
<span class="count" id="commands-count">0</span>
|
|
1051
|
+
</div>
|
|
1052
|
+
<div class="section-items" id="commands-items"></div>
|
|
1053
|
+
</div>
|
|
1054
|
+
<div class="section" id="projects-section">
|
|
1055
|
+
<div class="section-header">
|
|
1056
|
+
<span class="toggle">▼</span>
|
|
1057
|
+
Projects
|
|
1058
|
+
<span class="count" id="projects-count">0</span>
|
|
1059
|
+
</div>
|
|
1060
|
+
<div class="section-items">
|
|
1061
|
+
<div id="project-items"></div>
|
|
1062
|
+
<div class="project-add-form">
|
|
1063
|
+
<div class="project-add-row">
|
|
1064
|
+
<input type="text" id="project-dir-input" placeholder="/path/to/project">
|
|
1065
|
+
<button id="add-project-btn">Add</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="project-error" id="project-error"></div>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
|
|
1073
|
+
<!-- Skills Panel -->
|
|
1074
|
+
<div class="panel skills-panel" id="skills-panel">
|
|
1075
|
+
<div class="search-box">
|
|
1076
|
+
<input type="text" id="search-input" placeholder="Search skills...">
|
|
1077
|
+
</div>
|
|
1078
|
+
<div class="tag-filters" id="tag-filters" style="display: none;"></div>
|
|
1079
|
+
<div class="skills-list" id="skills-list">
|
|
1080
|
+
<div class="empty-state">Select a source to view skills</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
|
|
1084
|
+
<!-- Detail Panel -->
|
|
1085
|
+
<div class="panel detail-panel" id="detail-panel">
|
|
1086
|
+
<div class="empty">Select a skill to view details</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
</div>
|
|
1089
|
+
|
|
1090
|
+
<!-- Modal for reference files -->
|
|
1091
|
+
<div class="modal" id="modal">
|
|
1092
|
+
<div class="modal-content">
|
|
1093
|
+
<div class="modal-header">
|
|
1094
|
+
<h3 id="modal-title">Reference File</h3>
|
|
1095
|
+
<span class="modal-close" id="modal-close">×</span>
|
|
1096
|
+
</div>
|
|
1097
|
+
<div class="modal-body" id="modal-body">
|
|
1098
|
+
<pre id="modal-content"></pre>
|
|
1099
|
+
</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
|
|
1103
|
+
<div class="graph-overlay" id="graph-overlay">
|
|
1104
|
+
<div class="graph-controls">
|
|
1105
|
+
<button id="graph-close">Close</button>
|
|
1106
|
+
</div>
|
|
1107
|
+
<svg id="graph-svg"></svg>
|
|
1108
|
+
</div>
|
|
1109
|
+
|
|
1110
|
+
<script>
|
|
1111
|
+
// State
|
|
1112
|
+
let sources = null;
|
|
1113
|
+
let currentSource = null;
|
|
1114
|
+
let currentSkills = [];
|
|
1115
|
+
let currentSkill = null;
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
// DOM Elements
|
|
1119
|
+
const statusEl = document.getElementById('status');
|
|
1120
|
+
const sourcesPanel = document.getElementById('sources-panel');
|
|
1121
|
+
const skillsList = document.getElementById('skills-list');
|
|
1122
|
+
const detailPanel = document.getElementById('detail-panel');
|
|
1123
|
+
const searchInput = document.getElementById('search-input');
|
|
1124
|
+
const modal = document.getElementById('modal');
|
|
1125
|
+
const modalTitle = document.getElementById('modal-title');
|
|
1126
|
+
const modalContentEl = document.getElementById('modal-content');
|
|
1127
|
+
const modalClose = document.getElementById('modal-close');
|
|
1128
|
+
const globalSearchInput = document.getElementById('global-search-input');
|
|
1129
|
+
const globalSearchResults = document.getElementById('global-search-results');
|
|
1130
|
+
let searchDebounce = null;
|
|
1131
|
+
const graphToggle = document.getElementById('graph-toggle');
|
|
1132
|
+
const graphOverlay = document.getElementById('graph-overlay');
|
|
1133
|
+
const graphClose = document.getElementById('graph-close');
|
|
1134
|
+
const graphSvg = d3.select('#graph-svg');
|
|
1135
|
+
let graphData = null;
|
|
1136
|
+
const tagFiltersEl = document.getElementById('tag-filters');
|
|
1137
|
+
let activeTagFilters = new Set();
|
|
1138
|
+
|
|
1139
|
+
// API calls
|
|
1140
|
+
function encodePathForUrl(p) {
|
|
1141
|
+
return p.split('/').map(s => encodeURIComponent(s)).join('/');
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function fetchSources() {
|
|
1145
|
+
const res = await fetch('/api/sources');
|
|
1146
|
+
return res.json();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
async function fetchSkills(sourcePath) {
|
|
1150
|
+
const res = await fetch(`/api/skills?source=${encodeURIComponent(sourcePath)}`);
|
|
1151
|
+
return res.json();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function encodePathForUrl(p) { return p.split('/').map(s => encodeURIComponent(s)).join('/'); }
|
|
1155
|
+
|
|
1156
|
+
async function fetchSkill(skillPath) {
|
|
1157
|
+
const res = await fetch(`/api/skill${encodePathForUrl(skillPath)}`);
|
|
1158
|
+
return res.json();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async function fetchReference(refPath) {
|
|
1162
|
+
const res = await fetch(`/api/reference${encodePathForUrl(refPath)}`);
|
|
1163
|
+
return res.json();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Render functions
|
|
1167
|
+
function clearActive() {
|
|
1168
|
+
document.querySelectorAll('.sb-item.active').forEach(el => el.classList.remove('active'));
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function renderSources(sources) {
|
|
1172
|
+
const skillsItems = document.getElementById('skills-items');
|
|
1173
|
+
const skillsCount = document.getElementById('skills-count');
|
|
1174
|
+
let html = '';
|
|
1175
|
+
let total = 0;
|
|
1176
|
+
|
|
1177
|
+
// Separate single-skill and multi-skill plugins
|
|
1178
|
+
const multiPlugins = [];
|
|
1179
|
+
const soloSkills = [];
|
|
1180
|
+
for (const p of sources.plugins) {
|
|
1181
|
+
const skills = p.skills || [];
|
|
1182
|
+
total += skills.length;
|
|
1183
|
+
if (skills.length > 1) {
|
|
1184
|
+
multiPlugins.push(p);
|
|
1185
|
+
} else if (skills.length === 1) {
|
|
1186
|
+
soloSkills.push({ ...skills[0], sourcePath: p.path });
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Plugins
|
|
1191
|
+
if (multiPlugins.length > 0 || soloSkills.length > 0) {
|
|
1192
|
+
html += `<div class="sb-group" style="pointer-events:none;"><span class="sb-group-name">Plugins</span></div>`;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Multi-skill plugins as groups
|
|
1196
|
+
for (const p of multiPlugins) {
|
|
1197
|
+
const children = p.skills.map(s =>
|
|
1198
|
+
`<div class="sb-item sb-nested sb-plugin" data-path="${escapeHtml(s.path)}" data-source="${escapeHtml(p.path)}" data-type="skill">${escapeHtml(s.name)}</div>`
|
|
1199
|
+
).join('');
|
|
1200
|
+
html += `
|
|
1201
|
+
<div class="sb-group-wrap" data-path="${escapeHtml(p.path)}">
|
|
1202
|
+
<div class="sb-group">
|
|
1203
|
+
<span class="sb-toggle">▶</span>
|
|
1204
|
+
<span class="sb-group-name">${escapeHtml(p.name)}</span>
|
|
1205
|
+
<span class="sb-group-count">${p.skills.length}</span>
|
|
1206
|
+
</div>
|
|
1207
|
+
<div class="sb-group-children">${children}</div>
|
|
1208
|
+
</div>`;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Solo plugin skills as flat items
|
|
1212
|
+
for (const s of soloSkills) {
|
|
1213
|
+
html += `<div class="sb-item sb-plugin" data-path="${escapeHtml(s.path)}" data-source="${escapeHtml(s.sourcePath)}" data-type="skill">${escapeHtml(s.name)}</div>`;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// User skills
|
|
1217
|
+
const customFiles = sources.custom.length > 0 ? (sources.custom[0].files || []) : [];
|
|
1218
|
+
if (customFiles.length > 0) {
|
|
1219
|
+
total += customFiles.length;
|
|
1220
|
+
html += `<div class="sb-group" style="pointer-events:none;"><span class="sb-group-name">User Skills</span></div>`;
|
|
1221
|
+
for (const f of customFiles) {
|
|
1222
|
+
html += `<div class="sb-item sb-nested sb-user" data-path="${escapeHtml(f.path)}" data-source="${escapeHtml(sources.custom[0].path)}" data-type="skill">${escapeHtml(f.name)}</div>`;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
skillsCount.textContent = total;
|
|
1227
|
+
skillsItems.innerHTML = html;
|
|
1228
|
+
|
|
1229
|
+
// Commands
|
|
1230
|
+
const commandsItems = document.getElementById('commands-items');
|
|
1231
|
+
const commandsCount = document.getElementById('commands-count');
|
|
1232
|
+
let cmdHtml = '';
|
|
1233
|
+
let cmdTotal = 0;
|
|
1234
|
+
for (const c of sources.commands) {
|
|
1235
|
+
for (const f of (c.files || [])) {
|
|
1236
|
+
cmdTotal++;
|
|
1237
|
+
cmdHtml += `<div class="sb-item" data-path="${escapeHtml(f.path)}" data-source="${escapeHtml(c.path)}" data-type="command">${escapeHtml(f.name)}</div>`;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
commandsCount.textContent = cmdTotal;
|
|
1241
|
+
commandsItems.innerHTML = cmdHtml;
|
|
1242
|
+
document.getElementById('commands-section').style.display = cmdTotal === 0 ? 'none' : '';
|
|
1243
|
+
|
|
1244
|
+
// Projects
|
|
1245
|
+
const projectItems = document.getElementById('project-items');
|
|
1246
|
+
const projectsCount = document.getElementById('projects-count');
|
|
1247
|
+
const projectCount = sources.projects ? sources.projects.length : 0;
|
|
1248
|
+
projectsCount.textContent = projectCount;
|
|
1249
|
+
if (projectCount > 0) {
|
|
1250
|
+
projectItems.innerHTML = sources.projects.map(p => `
|
|
1251
|
+
<div class="sb-item" data-path="${escapeHtml(p.path)}" data-type="project">
|
|
1252
|
+
${escapeHtml(p.name)}<span class="sb-meta">${p.commandCount + p.skillCount}</span>
|
|
1253
|
+
<button class="remove-project-btn" data-project-dir="${escapeHtml(p.projectDir)}" title="Remove">✕</button>
|
|
1254
|
+
</div>
|
|
1255
|
+
`).join('');
|
|
1256
|
+
} else {
|
|
1257
|
+
projectItems.innerHTML = '';
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// --- Event handlers ---
|
|
1261
|
+
|
|
1262
|
+
// Group expand/collapse + load middle panel
|
|
1263
|
+
document.querySelectorAll('.sb-group-wrap > .sb-group').forEach(header => {
|
|
1264
|
+
header.addEventListener('click', () => {
|
|
1265
|
+
const wrap = header.closest('.sb-group-wrap');
|
|
1266
|
+
const toggle = header.querySelector('.sb-toggle');
|
|
1267
|
+
const expanded = wrap.classList.toggle('expanded');
|
|
1268
|
+
toggle.textContent = expanded ? '▼' : '▶';
|
|
1269
|
+
selectSource(wrap.dataset.path, header);
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
// All clickable items
|
|
1274
|
+
document.querySelectorAll('.sb-item').forEach(el => {
|
|
1275
|
+
el.addEventListener('click', async (e) => {
|
|
1276
|
+
if (e.target.classList.contains('remove-project-btn')) return;
|
|
1277
|
+
clearActive();
|
|
1278
|
+
el.classList.add('active');
|
|
1279
|
+
if (el.dataset.type === 'skill' || el.dataset.type === 'command') {
|
|
1280
|
+
const sourcePath = el.dataset.source;
|
|
1281
|
+
if (sourcePath && sourcePath !== currentSource) {
|
|
1282
|
+
await selectSource(sourcePath, el);
|
|
1283
|
+
}
|
|
1284
|
+
selectSkill(el.dataset.path);
|
|
1285
|
+
scrollMiddleToSkill(el.dataset.path);
|
|
1286
|
+
} else {
|
|
1287
|
+
selectSource(el.dataset.path, el);
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
// Remove project
|
|
1293
|
+
document.querySelectorAll('.remove-project-btn').forEach(btn => {
|
|
1294
|
+
btn.addEventListener('click', async (e) => {
|
|
1295
|
+
e.stopPropagation();
|
|
1296
|
+
const res = await fetch('/api/projects', {
|
|
1297
|
+
method: 'DELETE',
|
|
1298
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1299
|
+
body: JSON.stringify({ path: btn.dataset.projectDir }),
|
|
1300
|
+
});
|
|
1301
|
+
const data = await res.json();
|
|
1302
|
+
if (data.ok) renderSources(data.sources);
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
// Section toggles
|
|
1307
|
+
document.querySelectorAll('.section-header').forEach(el => {
|
|
1308
|
+
el.addEventListener('click', () => {
|
|
1309
|
+
const items = el.nextElementSibling;
|
|
1310
|
+
const toggle = el.querySelector('.toggle');
|
|
1311
|
+
if (items.style.display === 'none') {
|
|
1312
|
+
items.style.display = 'block';
|
|
1313
|
+
toggle.textContent = '▼';
|
|
1314
|
+
} else {
|
|
1315
|
+
items.style.display = 'none';
|
|
1316
|
+
toggle.textContent = '▶';
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function getRecencyBadge(health) {
|
|
1323
|
+
if (!health || !health.age_days) return '';
|
|
1324
|
+
const days = health.age_days;
|
|
1325
|
+
if (days < 14) return '<span class="badge recency-fresh">' + Math.round(days) + 'd ago</span>';
|
|
1326
|
+
if (days < 60) return '<span class="badge recency-aging">' + Math.round(days) + 'd ago</span>';
|
|
1327
|
+
return '<span class="badge recency-stale">' + Math.round(days) + 'd ago</span>';
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function getHealthBadges(skill) {
|
|
1331
|
+
if (!skill.health) return '';
|
|
1332
|
+
let badges = getRecencyBadge(skill.health);
|
|
1333
|
+
|
|
1334
|
+
if (skill.health.word_count && skill.health.word_count > 1000) {
|
|
1335
|
+
badges += `<span class="badge tag">${skill.health.word_count} words</span>`;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (skill.structural_tags) {
|
|
1339
|
+
skill.structural_tags.forEach(tag => {
|
|
1340
|
+
const label = tag.replace('has-', '').replace('-', ' ');
|
|
1341
|
+
badges += `<span class="badge tag">${escapeHtml(label)}</span>`;
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (skill.health.completeness_gaps) {
|
|
1346
|
+
skill.health.completeness_gaps.forEach(gap => {
|
|
1347
|
+
const label = gap.replace('-', ' ');
|
|
1348
|
+
badges += `<span class="badge gap">${escapeHtml(label)}</span>`;
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return badges ? `<div class="badges">${badges}</div>` : '';
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function renderSkills(skills, filter = '') {
|
|
1356
|
+
const filtered = filter
|
|
1357
|
+
? skills.filter(s =>
|
|
1358
|
+
s.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
1359
|
+
s.description.toLowerCase().includes(filter.toLowerCase())
|
|
1360
|
+
)
|
|
1361
|
+
: skills;
|
|
1362
|
+
|
|
1363
|
+
if (filtered.length === 0) {
|
|
1364
|
+
skillsList.innerHTML = `<div class="empty-state">No skills found</div>`;
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
skillsList.innerHTML = filtered.map(s => {
|
|
1369
|
+
const treeDir = s.skill_dir || s.path.substring(0, s.path.lastIndexOf('/'));
|
|
1370
|
+
return `
|
|
1371
|
+
<div class="skill-item" data-path="${escapeHtml(s.path)}" data-tree-dir="${escapeHtml(treeDir)}">
|
|
1372
|
+
<div class="skill-header">
|
|
1373
|
+
<span class="skill-toggle">▶</span>
|
|
1374
|
+
<div class="skill-info">
|
|
1375
|
+
<div class="name">${escapeHtml(s.name)}</div>
|
|
1376
|
+
<div class="description">${escapeHtml(s.description || 'No description')}</div>
|
|
1377
|
+
${getHealthBadges(s)}
|
|
1378
|
+
</div>
|
|
1379
|
+
</div>
|
|
1380
|
+
<div class="inline-file-tree"></div>
|
|
1381
|
+
</div>
|
|
1382
|
+
`}).join('');
|
|
1383
|
+
|
|
1384
|
+
bindSkillItemHandlers();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function extractAvailableTags(skills) {
|
|
1388
|
+
const tags = new Map(); // tag -> count
|
|
1389
|
+
skills.forEach(s => {
|
|
1390
|
+
if (s.tool_references) {
|
|
1391
|
+
s.tool_references.forEach(t => {
|
|
1392
|
+
tags.set(`tool:${t}`, (tags.get(`tool:${t}`) || 0) + 1);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
if (s.structural_tags) {
|
|
1396
|
+
s.structural_tags.forEach(t => {
|
|
1397
|
+
tags.set(t, (tags.get(t) || 0) + 1);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
return [...tags.entries()]
|
|
1402
|
+
.filter(([, count]) => count >= 2)
|
|
1403
|
+
.sort((a, b) => b[1] - a[1])
|
|
1404
|
+
.slice(0, 15);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function renderTagFilters(skills) {
|
|
1408
|
+
const tags = extractAvailableTags(skills);
|
|
1409
|
+
if (tags.length === 0) {
|
|
1410
|
+
tagFiltersEl.style.display = 'none';
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
tagFiltersEl.style.display = 'flex';
|
|
1415
|
+
tagFiltersEl.innerHTML = tags.map(([tag, count]) => {
|
|
1416
|
+
const label = tag.replace('has-', '').replace('tool:', '');
|
|
1417
|
+
const isActive = activeTagFilters.has(tag);
|
|
1418
|
+
return `<span class="tag-filter ${isActive ? 'active' : ''}" data-tag="${escapeHtml(tag)}">${escapeHtml(label)} (${count})</span>`;
|
|
1419
|
+
}).join('');
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function bindSkillItemHandlers() {
|
|
1423
|
+
document.querySelectorAll('.skill-item').forEach(el => {
|
|
1424
|
+
// Clicking the toggle expands/collapses the inline file tree
|
|
1425
|
+
const toggle = el.querySelector('.skill-toggle');
|
|
1426
|
+
const treeContainer = el.querySelector('.inline-file-tree');
|
|
1427
|
+
|
|
1428
|
+
toggle.addEventListener('click', async (e) => {
|
|
1429
|
+
e.stopPropagation();
|
|
1430
|
+
const isExpanded = treeContainer.classList.contains('expanded');
|
|
1431
|
+
|
|
1432
|
+
if (isExpanded) {
|
|
1433
|
+
treeContainer.classList.remove('expanded');
|
|
1434
|
+
toggle.innerHTML = '▶';
|
|
1435
|
+
} else {
|
|
1436
|
+
treeContainer.classList.add('expanded');
|
|
1437
|
+
toggle.innerHTML = '▼';
|
|
1438
|
+
|
|
1439
|
+
// Load tree if not already loaded
|
|
1440
|
+
if (!treeContainer.dataset.loaded) {
|
|
1441
|
+
treeContainer.innerHTML = '<div class="loading" style="padding:8px;font-size:12px;">Loading...</div>';
|
|
1442
|
+
try {
|
|
1443
|
+
const dirPath = el.dataset.treeDir;
|
|
1444
|
+
const res = await fetch(`/api/skill-tree${encodePathForUrl(dirPath)}`);
|
|
1445
|
+
const data = await res.json();
|
|
1446
|
+
if (data.tree && data.tree.length > 0) {
|
|
1447
|
+
treeContainer.innerHTML = renderFileTree(data.tree);
|
|
1448
|
+
// Bind inline tree click handlers
|
|
1449
|
+
treeContainer.querySelectorAll('.file-tree-item.directory').forEach(dir => {
|
|
1450
|
+
dir.addEventListener('click', (ev) => {
|
|
1451
|
+
ev.stopPropagation();
|
|
1452
|
+
const childrenId = dir.dataset.childrenId;
|
|
1453
|
+
const childrenEl = document.getElementById(childrenId);
|
|
1454
|
+
if (childrenEl) {
|
|
1455
|
+
childrenEl.classList.toggle('expanded');
|
|
1456
|
+
dir.querySelector('.icon').textContent = childrenEl.classList.contains('expanded') ? '\u{1F4C2}' : '\u{1F4C1}';
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1460
|
+
treeContainer.querySelectorAll('.file-tree-item.file').forEach(file => {
|
|
1461
|
+
file.addEventListener('click', async (ev) => {
|
|
1462
|
+
ev.stopPropagation();
|
|
1463
|
+
const filePath = file.dataset.treeFilePath;
|
|
1464
|
+
const fileName = file.querySelector('.name').textContent;
|
|
1465
|
+
detailPanel.innerHTML = '<div class="loading">Loading file...</div>';
|
|
1466
|
+
try {
|
|
1467
|
+
const ref = await fetchReference(filePath);
|
|
1468
|
+
detailPanel.innerHTML = `
|
|
1469
|
+
<div class="detail-content">
|
|
1470
|
+
<div class="detail-header">
|
|
1471
|
+
<h2>${escapeHtml(ref.name)}</h2>
|
|
1472
|
+
<div class="path">${escapeHtml(ref.path)}</div>
|
|
1473
|
+
</div>
|
|
1474
|
+
<div class="content-preview">
|
|
1475
|
+
${renderFileContent(ref.path, ref.content)}
|
|
1476
|
+
</div>
|
|
1477
|
+
</div>
|
|
1478
|
+
`;
|
|
1479
|
+
highlightDetailPanel();
|
|
1480
|
+
statusEl.textContent = `Viewing: ${ref.name}`;
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
detailPanel.innerHTML = `
|
|
1483
|
+
<div class="detail-content">
|
|
1484
|
+
<div class="detail-header">
|
|
1485
|
+
<h2>${escapeHtml(fileName)}</h2>
|
|
1486
|
+
</div>
|
|
1487
|
+
<div class="error">Unable to display file content</div>
|
|
1488
|
+
</div>
|
|
1489
|
+
`;
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
} else {
|
|
1494
|
+
treeContainer.innerHTML = '<div style="padding:8px;font-size:12px;color:var(--text-muted)">No files</div>';
|
|
1495
|
+
}
|
|
1496
|
+
treeContainer.dataset.loaded = 'true';
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
treeContainer.innerHTML = '<div style="padding:8px;font-size:12px;color:#dc3545">Error loading files</div>';
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
// Clicking the header (not toggle) selects the skill in detail panel
|
|
1505
|
+
el.querySelector('.skill-info').addEventListener('click', (e) => {
|
|
1506
|
+
e.stopPropagation();
|
|
1507
|
+
selectSkill(el.dataset.path, el);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function applyFilters() {
|
|
1513
|
+
const textFilter = searchInput.value.toLowerCase();
|
|
1514
|
+
let filtered = currentSkills;
|
|
1515
|
+
|
|
1516
|
+
if (textFilter) {
|
|
1517
|
+
filtered = filtered.filter(s =>
|
|
1518
|
+
s.name.toLowerCase().includes(textFilter) ||
|
|
1519
|
+
(s.description || '').toLowerCase().includes(textFilter)
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (activeTagFilters.size > 0) {
|
|
1524
|
+
filtered = filtered.filter(s => {
|
|
1525
|
+
const skillTags = new Set([
|
|
1526
|
+
...(s.tool_references || []).map(t => `tool:${t}`),
|
|
1527
|
+
...(s.structural_tags || []),
|
|
1528
|
+
]);
|
|
1529
|
+
return [...activeTagFilters].every(tag => skillTags.has(tag));
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
if (filtered.length === 0) {
|
|
1534
|
+
skillsList.innerHTML = '<div class="empty-state">No skills match filters</div>';
|
|
1535
|
+
} else {
|
|
1536
|
+
skillsList.innerHTML = filtered.map(s => {
|
|
1537
|
+
const treeDir = s.skill_dir || s.path.substring(0, s.path.lastIndexOf('/'));
|
|
1538
|
+
return `
|
|
1539
|
+
<div class="skill-item" data-path="${escapeHtml(s.path)}" data-tree-dir="${escapeHtml(treeDir)}">
|
|
1540
|
+
<div class="skill-header">
|
|
1541
|
+
<span class="skill-toggle">▶</span>
|
|
1542
|
+
<div class="skill-info">
|
|
1543
|
+
<div class="name">${escapeHtml(s.name)}</div>
|
|
1544
|
+
<div class="description">${escapeHtml(s.description || 'No description')}</div>
|
|
1545
|
+
${getHealthBadges(s)}
|
|
1546
|
+
</div>
|
|
1547
|
+
</div>
|
|
1548
|
+
<div class="inline-file-tree"></div>
|
|
1549
|
+
</div>
|
|
1550
|
+
`}).join('');
|
|
1551
|
+
bindSkillItemHandlers();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
renderTagFilters(currentSkills);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
tagFiltersEl.addEventListener('click', (e) => {
|
|
1558
|
+
const filter = e.target.closest('.tag-filter');
|
|
1559
|
+
if (!filter) return;
|
|
1560
|
+
|
|
1561
|
+
const tag = filter.dataset.tag;
|
|
1562
|
+
if (activeTagFilters.has(tag)) {
|
|
1563
|
+
activeTagFilters.delete(tag);
|
|
1564
|
+
} else {
|
|
1565
|
+
activeTagFilters.add(tag);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
applyFilters();
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// Top-level simple fields worth showing as key-value rows
|
|
1572
|
+
const METADATA_KEYS = ['name', 'description', 'argument-hint'];
|
|
1573
|
+
|
|
1574
|
+
function renderSkillDetail(skill) {
|
|
1575
|
+
// Metadata: clean key-value rows for simple top-level fields
|
|
1576
|
+
const metadataEntries = METADATA_KEYS
|
|
1577
|
+
.filter(k => skill.frontmatter[k] && skill.frontmatter[k].trim())
|
|
1578
|
+
.map(k => [k, skill.frontmatter[k]]);
|
|
1579
|
+
|
|
1580
|
+
const metadataHtml = metadataEntries.map(([k, v]) => `
|
|
1581
|
+
<div class="frontmatter-row">
|
|
1582
|
+
<div class="key">${escapeHtml(k)}</div>
|
|
1583
|
+
<div class="value">${escapeHtml(v)}</div>
|
|
1584
|
+
</div>
|
|
1585
|
+
`).join('');
|
|
1586
|
+
|
|
1587
|
+
// Raw YAML: show full frontmatter as syntax-highlighted YAML
|
|
1588
|
+
const rawYaml = skill.frontmatter_raw || '';
|
|
1589
|
+
const hasConfig = rawYaml && rawYaml.trim().split('\n').length > metadataEntries.length;
|
|
1590
|
+
|
|
1591
|
+
detailPanel.innerHTML = `
|
|
1592
|
+
<div class="detail-content">
|
|
1593
|
+
<div class="detail-header">
|
|
1594
|
+
<h2>${escapeHtml(skill.name)}</h2>
|
|
1595
|
+
<div class="description">${escapeHtml(skill.description || 'No description')}</div>
|
|
1596
|
+
<div class="path">${escapeHtml(skill.path)}</div>
|
|
1597
|
+
</div>
|
|
1598
|
+
|
|
1599
|
+
<div class="section-title">Metadata</div>
|
|
1600
|
+
<div class="frontmatter-table">
|
|
1601
|
+
${metadataHtml || '<div class="empty-state">No metadata</div>'}
|
|
1602
|
+
</div>
|
|
1603
|
+
|
|
1604
|
+
${hasConfig ? `
|
|
1605
|
+
<div class="section-title">Configuration</div>
|
|
1606
|
+
<div class="content-preview">
|
|
1607
|
+
<pre><code class="language-yaml">${escapeHtml(rawYaml)}</code></pre>
|
|
1608
|
+
</div>
|
|
1609
|
+
` : ''}
|
|
1610
|
+
|
|
1611
|
+
<div class="section-title">Content</div>
|
|
1612
|
+
<div class="content-preview">
|
|
1613
|
+
<pre>${escapeHtml(skill.content)}</pre>
|
|
1614
|
+
</div>
|
|
1615
|
+
</div>
|
|
1616
|
+
`;
|
|
1617
|
+
|
|
1618
|
+
highlightDetailPanel();
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const FILE_ICONS = {
|
|
1622
|
+
'.md': '📄', '.py': '🐍', '.sh': '⚙️', '.json': '📋',
|
|
1623
|
+
'.html': '🌐', '.yaml': '📋', '.yml': '📋', '.txt': '📝',
|
|
1624
|
+
'.png': '🖼️', '.jpg': '🖼️', '.svg': '🖼️',
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
function getFileIcon(entry) {
|
|
1628
|
+
if (entry.type === 'directory') return '📁';
|
|
1629
|
+
return FILE_ICONS[entry.extension] || '📄';
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function formatFileSize(bytes) {
|
|
1633
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1634
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1635
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function renderFileTree(tree, depth = 0) {
|
|
1639
|
+
return tree.map(entry => {
|
|
1640
|
+
const icon = getFileIcon(entry);
|
|
1641
|
+
const indent = '<span class="indent"></span>'.repeat(depth);
|
|
1642
|
+
const sizeStr = entry.size != null ? `<span class="size">${formatFileSize(entry.size)}</span>` : '';
|
|
1643
|
+
|
|
1644
|
+
if (entry.type === 'directory') {
|
|
1645
|
+
const childrenId = `tree-${entry.path.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
1646
|
+
return `
|
|
1647
|
+
<div class="file-tree-item directory" data-tree-path="${escapeHtml(entry.path)}" data-children-id="${childrenId}">
|
|
1648
|
+
${indent}
|
|
1649
|
+
<span class="icon">${icon}</span>
|
|
1650
|
+
<span class="name">${escapeHtml(entry.name)}/</span>
|
|
1651
|
+
</div>
|
|
1652
|
+
<div class="file-tree-children" id="${childrenId}">
|
|
1653
|
+
${entry.children ? renderFileTree(entry.children, depth + 1) : ''}
|
|
1654
|
+
</div>
|
|
1655
|
+
`;
|
|
1656
|
+
} else {
|
|
1657
|
+
return `
|
|
1658
|
+
<div class="file-tree-item file" data-tree-file-path="${escapeHtml(entry.path)}" data-ext="${escapeHtml(entry.extension || '')}">
|
|
1659
|
+
${indent}
|
|
1660
|
+
<span class="icon">${icon}</span>
|
|
1661
|
+
<span class="name">${escapeHtml(entry.name)}</span>
|
|
1662
|
+
${sizeStr}
|
|
1663
|
+
</div>
|
|
1664
|
+
`;
|
|
1665
|
+
}
|
|
1666
|
+
}).join('');
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function escapeHtml(text) {
|
|
1670
|
+
const div = document.createElement('div');
|
|
1671
|
+
div.textContent = text;
|
|
1672
|
+
return div.innerHTML;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const EXT_TO_LANG = {
|
|
1676
|
+
'.py': 'python', '.js': 'javascript', '.ts': 'typescript',
|
|
1677
|
+
'.sh': 'bash', '.bash': 'bash', '.zsh': 'bash',
|
|
1678
|
+
'.json': 'json', '.yaml': 'yaml', '.yml': 'yaml',
|
|
1679
|
+
'.html': 'xml', '.xml': 'xml', '.css': 'css',
|
|
1680
|
+
'.md': 'markdown', '.sql': 'sql', '.rb': 'ruby',
|
|
1681
|
+
'.go': 'go', '.rs': 'rust', '.java': 'java',
|
|
1682
|
+
'.c': 'c', '.cpp': 'cpp', '.h': 'c',
|
|
1683
|
+
'.toml': 'ini', '.ini': 'ini', '.cfg': 'ini',
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
function getLangFromPath(filePath) {
|
|
1687
|
+
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
|
1688
|
+
return EXT_TO_LANG[ext] || null;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function renderFileContent(filePath, content) {
|
|
1692
|
+
const lang = getLangFromPath(filePath);
|
|
1693
|
+
if (lang) {
|
|
1694
|
+
const escaped = escapeHtml(content);
|
|
1695
|
+
return `<pre><code class="language-${lang}">${escaped}</code></pre>`;
|
|
1696
|
+
}
|
|
1697
|
+
return `<pre>${escapeHtml(content)}</pre>`;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function highlightDetailPanel() {
|
|
1701
|
+
detailPanel.querySelectorAll('pre code').forEach(block => {
|
|
1702
|
+
hljs.highlightElement(block);
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Event handlers
|
|
1707
|
+
function scrollMiddleToSkill(skillPath) {
|
|
1708
|
+
const item = document.querySelector(`.skill-item[data-path="${CSS.escape(skillPath)}"]`);
|
|
1709
|
+
if (item) {
|
|
1710
|
+
item.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1711
|
+
document.querySelectorAll('.skill-item').forEach(e => e.classList.remove('active'));
|
|
1712
|
+
item.classList.add('active');
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async function selectSource(path, el) {
|
|
1717
|
+
// Update active state
|
|
1718
|
+
document.querySelectorAll('.source-item').forEach(e => e.classList.remove('active'));
|
|
1719
|
+
el.classList.add('active');
|
|
1720
|
+
|
|
1721
|
+
currentSource = path;
|
|
1722
|
+
currentSkill = null;
|
|
1723
|
+
|
|
1724
|
+
// Show loading
|
|
1725
|
+
skillsList.innerHTML = '<div class="loading">Loading skills...</div>';
|
|
1726
|
+
detailPanel.innerHTML = '<div class="empty">Select a skill to view details</div>';
|
|
1727
|
+
|
|
1728
|
+
// Fetch skills
|
|
1729
|
+
try {
|
|
1730
|
+
currentSkills = await fetchSkills(path);
|
|
1731
|
+
activeTagFilters.clear();
|
|
1732
|
+
renderSkills(currentSkills);
|
|
1733
|
+
renderTagFilters(currentSkills);
|
|
1734
|
+
statusEl.textContent = `${currentSkills.length} skills loaded`;
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
skillsList.innerHTML = `<div class="error">Error loading skills: ${escapeHtml(err.message)}</div>`;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
async function selectSkill(path, el) {
|
|
1741
|
+
// Update active state
|
|
1742
|
+
document.querySelectorAll('.skill-item').forEach(e => e.classList.remove('active'));
|
|
1743
|
+
if (el) el.classList.add('active');
|
|
1744
|
+
|
|
1745
|
+
currentSkill = path;
|
|
1746
|
+
|
|
1747
|
+
// Show loading
|
|
1748
|
+
detailPanel.innerHTML = '<div class="loading">Loading skill...</div>';
|
|
1749
|
+
|
|
1750
|
+
// Fetch skill
|
|
1751
|
+
try {
|
|
1752
|
+
const skill = await fetchSkill(path);
|
|
1753
|
+
renderSkillDetail(skill);
|
|
1754
|
+
statusEl.textContent = `Viewing: ${skill.name}`;
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
detailPanel.innerHTML = `<div class="error">Error loading skill: ${escapeHtml(err.message)}</div>`;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Search handler
|
|
1761
|
+
searchInput.addEventListener('input', () => applyFilters());
|
|
1762
|
+
|
|
1763
|
+
// Modal handlers
|
|
1764
|
+
modalClose.addEventListener('click', () => {
|
|
1765
|
+
modal.classList.remove('active');
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
modal.addEventListener('click', (e) => {
|
|
1769
|
+
if (e.target === modal) {
|
|
1770
|
+
modal.classList.remove('active');
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
// Reference click handler (delegated)
|
|
1775
|
+
detailPanel.addEventListener('click', async (e) => {
|
|
1776
|
+
const treeDir = e.target.closest('.file-tree-item.directory');
|
|
1777
|
+
const treeFile = e.target.closest('.file-tree-item.file');
|
|
1778
|
+
const refItem = e.target.closest('.reference-item');
|
|
1779
|
+
|
|
1780
|
+
if (treeDir) {
|
|
1781
|
+
const childrenId = treeDir.dataset.childrenId;
|
|
1782
|
+
const childrenEl = document.getElementById(childrenId);
|
|
1783
|
+
if (childrenEl) {
|
|
1784
|
+
childrenEl.classList.toggle('expanded');
|
|
1785
|
+
const iconEl = treeDir.querySelector('.icon');
|
|
1786
|
+
iconEl.textContent = childrenEl.classList.contains('expanded') ? '📂' : '📁';
|
|
1787
|
+
}
|
|
1788
|
+
} else if (treeFile) {
|
|
1789
|
+
const filePath = treeFile.dataset.treeFilePath;
|
|
1790
|
+
const fileName = treeFile.querySelector('.name').textContent;
|
|
1791
|
+
detailPanel.innerHTML = '<div class="loading">Loading file...</div>';
|
|
1792
|
+
try {
|
|
1793
|
+
const ref = await fetchReference(filePath);
|
|
1794
|
+
detailPanel.innerHTML = `
|
|
1795
|
+
<div class="detail-content">
|
|
1796
|
+
<div class="detail-header">
|
|
1797
|
+
<h2>${escapeHtml(ref.name)}</h2>
|
|
1798
|
+
<div class="path">${escapeHtml(ref.path)}</div>
|
|
1799
|
+
</div>
|
|
1800
|
+
<div class="content-preview">
|
|
1801
|
+
${renderFileContent(ref.path, ref.content)}
|
|
1802
|
+
</div>
|
|
1803
|
+
</div>
|
|
1804
|
+
`;
|
|
1805
|
+
highlightDetailPanel();
|
|
1806
|
+
statusEl.textContent = `Viewing: ${ref.name}`;
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
detailPanel.innerHTML = `
|
|
1809
|
+
<div class="detail-content">
|
|
1810
|
+
<div class="detail-header">
|
|
1811
|
+
<h2>${escapeHtml(fileName)}</h2>
|
|
1812
|
+
</div>
|
|
1813
|
+
<div class="error">Unable to display file content</div>
|
|
1814
|
+
</div>
|
|
1815
|
+
`;
|
|
1816
|
+
}
|
|
1817
|
+
} else if (refItem) {
|
|
1818
|
+
const path = refItem.dataset.path;
|
|
1819
|
+
try {
|
|
1820
|
+
const ref = await fetchReference(path);
|
|
1821
|
+
detailPanel.innerHTML = `
|
|
1822
|
+
<div class="detail-content">
|
|
1823
|
+
<div class="detail-header">
|
|
1824
|
+
<h2>${escapeHtml(ref.name)}</h2>
|
|
1825
|
+
<div class="path">${escapeHtml(ref.path)}</div>
|
|
1826
|
+
</div>
|
|
1827
|
+
<div class="content-preview">
|
|
1828
|
+
${renderFileContent(ref.path, ref.content)}
|
|
1829
|
+
</div>
|
|
1830
|
+
</div>
|
|
1831
|
+
`;
|
|
1832
|
+
highlightDetailPanel();
|
|
1833
|
+
statusEl.textContent = `Viewing: ${ref.name}`;
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
detailPanel.innerHTML = `<div class="error">Error loading reference: ${escapeHtml(err.message)}</div>`;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
// Global search
|
|
1841
|
+
globalSearchInput.addEventListener('input', (e) => {
|
|
1842
|
+
clearTimeout(searchDebounce);
|
|
1843
|
+
const query = e.target.value.trim();
|
|
1844
|
+
|
|
1845
|
+
if (query.length < 2) {
|
|
1846
|
+
globalSearchResults.classList.remove('active');
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
searchDebounce = setTimeout(async () => {
|
|
1851
|
+
try {
|
|
1852
|
+
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
|
1853
|
+
const results = await res.json();
|
|
1854
|
+
renderGlobalSearchResults(results);
|
|
1855
|
+
} catch (err) {
|
|
1856
|
+
globalSearchResults.innerHTML = `<div class="search-result-item"><div class="result-name">Error: ${escapeHtml(err.message)}</div></div>`;
|
|
1857
|
+
globalSearchResults.classList.add('active');
|
|
1858
|
+
}
|
|
1859
|
+
}, 250);
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
function renderGlobalSearchResults(results) {
|
|
1863
|
+
if (results.length === 0) {
|
|
1864
|
+
globalSearchResults.innerHTML = '<div class="search-result-item"><div class="result-name" style="color: var(--text-muted)">No results</div></div>';
|
|
1865
|
+
} else {
|
|
1866
|
+
globalSearchResults.innerHTML = results.slice(0, 20).map(r => `
|
|
1867
|
+
<div class="search-result-item" data-path="${escapeHtml(r.path)}">
|
|
1868
|
+
<div class="result-name">${escapeHtml(r.name)}</div>
|
|
1869
|
+
<div class="result-source">${escapeHtml(r.source_name)} · ${r.source_type}</div>
|
|
1870
|
+
<div class="result-snippet">${escapeHtml(r.snippet)}</div>
|
|
1871
|
+
</div>
|
|
1872
|
+
`).join('');
|
|
1873
|
+
}
|
|
1874
|
+
globalSearchResults.classList.add('active');
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Click on search result -> load that skill
|
|
1878
|
+
globalSearchResults.addEventListener('click', async (e) => {
|
|
1879
|
+
const item = e.target.closest('.search-result-item');
|
|
1880
|
+
if (!item || !item.dataset.path) return;
|
|
1881
|
+
|
|
1882
|
+
globalSearchResults.classList.remove('active');
|
|
1883
|
+
globalSearchInput.value = '';
|
|
1884
|
+
|
|
1885
|
+
detailPanel.innerHTML = '<div class="loading">Loading skill...</div>';
|
|
1886
|
+
try {
|
|
1887
|
+
const skill = await fetchSkill(item.dataset.path);
|
|
1888
|
+
renderSkillDetail(skill);
|
|
1889
|
+
currentSkill = item.dataset.path;
|
|
1890
|
+
statusEl.textContent = `Viewing: ${skill.name}`;
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
detailPanel.innerHTML = `<div class="error">Error: ${escapeHtml(err.message)}</div>`;
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// Close search results when clicking outside
|
|
1897
|
+
document.addEventListener('click', (e) => {
|
|
1898
|
+
if (!e.target.closest('.global-search')) {
|
|
1899
|
+
globalSearchResults.classList.remove('active');
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
// Keyboard shortcut: Cmd+K to focus global search
|
|
1904
|
+
document.addEventListener('keydown', (e) => {
|
|
1905
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
1906
|
+
e.preventDefault();
|
|
1907
|
+
globalSearchInput.focus();
|
|
1908
|
+
globalSearchInput.select();
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
// Graph overlay
|
|
1913
|
+
const SOURCE_COLORS = {
|
|
1914
|
+
plugin: '#0066cc',
|
|
1915
|
+
custom: '#28a745',
|
|
1916
|
+
command: '#f0ad4e',
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
graphToggle.addEventListener('click', async () => {
|
|
1920
|
+
if (graphOverlay.classList.contains('active')) {
|
|
1921
|
+
graphOverlay.classList.remove('active');
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
graphOverlay.classList.add('active');
|
|
1926
|
+
|
|
1927
|
+
if (!graphData) {
|
|
1928
|
+
try {
|
|
1929
|
+
const res = await fetch('/api/graph');
|
|
1930
|
+
graphData = await res.json();
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
graphOverlay.innerHTML = `<div class="error" style="margin:20px">Error loading graph: ${escapeHtml(err.message)}</div>`;
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
renderGraph(graphData);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
graphClose.addEventListener('click', () => {
|
|
1941
|
+
graphOverlay.classList.remove('active');
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
function renderGraph(data) {
|
|
1945
|
+
graphSvg.selectAll('*').remove();
|
|
1946
|
+
|
|
1947
|
+
const width = graphOverlay.clientWidth;
|
|
1948
|
+
const height = graphOverlay.clientHeight;
|
|
1949
|
+
|
|
1950
|
+
const svg = graphSvg
|
|
1951
|
+
.attr('width', width)
|
|
1952
|
+
.attr('height', height);
|
|
1953
|
+
|
|
1954
|
+
// Create a container group for zoom/pan
|
|
1955
|
+
const container = svg.append('g');
|
|
1956
|
+
|
|
1957
|
+
// Arrow marker
|
|
1958
|
+
svg.append('defs').append('marker')
|
|
1959
|
+
.attr('id', 'arrowhead')
|
|
1960
|
+
.attr('viewBox', '0 -5 10 10')
|
|
1961
|
+
.attr('refX', 20)
|
|
1962
|
+
.attr('refY', 0)
|
|
1963
|
+
.attr('markerWidth', 6)
|
|
1964
|
+
.attr('markerHeight', 6)
|
|
1965
|
+
.attr('orient', 'auto')
|
|
1966
|
+
.append('path')
|
|
1967
|
+
.attr('d', 'M0,-5L10,0L0,5')
|
|
1968
|
+
.attr('class', 'graph-link-arrow');
|
|
1969
|
+
|
|
1970
|
+
// Deep clone data to avoid d3 mutation issues on re-render
|
|
1971
|
+
const nodes = data.nodes.map(n => ({...n}));
|
|
1972
|
+
const edges = data.edges.map(e => ({...e}));
|
|
1973
|
+
|
|
1974
|
+
const simulation = d3.forceSimulation(nodes)
|
|
1975
|
+
.force('link', d3.forceLink(edges).id(d => d.id).distance(120))
|
|
1976
|
+
.force('charge', d3.forceManyBody().strength(-300))
|
|
1977
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
1978
|
+
.force('collision', d3.forceCollide().radius(40));
|
|
1979
|
+
|
|
1980
|
+
const link = container.append('g')
|
|
1981
|
+
.selectAll('line')
|
|
1982
|
+
.data(edges)
|
|
1983
|
+
.join('line')
|
|
1984
|
+
.attr('class', 'graph-link')
|
|
1985
|
+
.attr('marker-end', 'url(#arrowhead)');
|
|
1986
|
+
|
|
1987
|
+
const node = container.append('g')
|
|
1988
|
+
.selectAll('g')
|
|
1989
|
+
.data(nodes)
|
|
1990
|
+
.join('g')
|
|
1991
|
+
.attr('class', 'graph-node')
|
|
1992
|
+
.call(d3.drag()
|
|
1993
|
+
.on('start', (event, d) => {
|
|
1994
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
1995
|
+
d.fx = d.x;
|
|
1996
|
+
d.fy = d.y;
|
|
1997
|
+
})
|
|
1998
|
+
.on('drag', (event, d) => {
|
|
1999
|
+
d.fx = event.x;
|
|
2000
|
+
d.fy = event.y;
|
|
2001
|
+
})
|
|
2002
|
+
.on('end', (event, d) => {
|
|
2003
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
2004
|
+
d.fx = null;
|
|
2005
|
+
d.fy = null;
|
|
2006
|
+
})
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
// Size nodes by word count
|
|
2010
|
+
node.append('circle')
|
|
2011
|
+
.attr('r', d => Math.max(6, Math.min(18, Math.sqrt(d.word_count || 100) / 2)))
|
|
2012
|
+
.attr('fill', d => SOURCE_COLORS[d.source_type] || '#999');
|
|
2013
|
+
|
|
2014
|
+
node.append('text')
|
|
2015
|
+
.attr('dx', 14)
|
|
2016
|
+
.attr('dy', 4)
|
|
2017
|
+
.text(d => d.name);
|
|
2018
|
+
|
|
2019
|
+
// Click node to navigate to skill
|
|
2020
|
+
node.on('click', async (event, d) => {
|
|
2021
|
+
graphOverlay.classList.remove('active');
|
|
2022
|
+
detailPanel.innerHTML = '<div class="loading">Loading skill...</div>';
|
|
2023
|
+
try {
|
|
2024
|
+
const skill = await fetchSkill(d.id);
|
|
2025
|
+
renderSkillDetail(skill);
|
|
2026
|
+
currentSkill = d.id;
|
|
2027
|
+
statusEl.textContent = `Viewing: ${skill.name}`;
|
|
2028
|
+
} catch (err) {
|
|
2029
|
+
detailPanel.innerHTML = `<div class="error">Error: ${escapeHtml(err.message)}</div>`;
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
// Tooltip on hover
|
|
2034
|
+
node.append('title')
|
|
2035
|
+
.text(d => `${d.name}\n${d.source_name} (${d.source_type})\n${d.word_count || '?'} words`);
|
|
2036
|
+
|
|
2037
|
+
simulation.on('tick', () => {
|
|
2038
|
+
link
|
|
2039
|
+
.attr('x1', d => d.source.x)
|
|
2040
|
+
.attr('y1', d => d.source.y)
|
|
2041
|
+
.attr('x2', d => d.target.x)
|
|
2042
|
+
.attr('y2', d => d.target.y);
|
|
2043
|
+
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
// Zoom - apply transform to container group only
|
|
2047
|
+
const zoom = d3.zoom()
|
|
2048
|
+
.scaleExtent([0.2, 4])
|
|
2049
|
+
.on('zoom', (event) => {
|
|
2050
|
+
container.attr('transform', event.transform);
|
|
2051
|
+
});
|
|
2052
|
+
svg.call(zoom);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Add project handler
|
|
2056
|
+
document.getElementById('add-project-btn').addEventListener('click', async () => {
|
|
2057
|
+
const input = document.getElementById('project-dir-input');
|
|
2058
|
+
const errorEl = document.getElementById('project-error');
|
|
2059
|
+
const dir = input.value.trim();
|
|
2060
|
+
if (!dir) return;
|
|
2061
|
+
errorEl.style.display = 'none';
|
|
2062
|
+
try {
|
|
2063
|
+
const res = await fetch('/api/projects', {
|
|
2064
|
+
method: 'POST',
|
|
2065
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2066
|
+
body: JSON.stringify({ path: dir }),
|
|
2067
|
+
});
|
|
2068
|
+
const data = await res.json();
|
|
2069
|
+
if (!res.ok) {
|
|
2070
|
+
errorEl.textContent = data.error;
|
|
2071
|
+
errorEl.style.display = 'block';
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
input.value = '';
|
|
2075
|
+
renderSources(data.sources);
|
|
2076
|
+
} catch (err) {
|
|
2077
|
+
errorEl.textContent = err.message;
|
|
2078
|
+
errorEl.style.display = 'block';
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
document.getElementById('project-dir-input').addEventListener('keydown', (e) => {
|
|
2082
|
+
if (e.key === 'Enter') document.getElementById('add-project-btn').click();
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
// Agent switcher
|
|
2086
|
+
const agentDropdown = document.getElementById('agent-dropdown');
|
|
2087
|
+
const agentToggle = document.getElementById('agent-dropdown-toggle');
|
|
2088
|
+
const agentMenu = document.getElementById('agent-dropdown-menu');
|
|
2089
|
+
const agentLabel = document.getElementById('agent-dropdown-label');
|
|
2090
|
+
let activeAgentId = null;
|
|
2091
|
+
|
|
2092
|
+
agentToggle.addEventListener('click', (e) => {
|
|
2093
|
+
e.stopPropagation();
|
|
2094
|
+
agentDropdown.classList.toggle('open');
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
document.addEventListener('click', () => {
|
|
2098
|
+
agentDropdown.classList.remove('open');
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
async function loadAgents() {
|
|
2102
|
+
try {
|
|
2103
|
+
const res = await fetch('/api/agents');
|
|
2104
|
+
const data = await res.json();
|
|
2105
|
+
activeAgentId = data.active_id;
|
|
2106
|
+
const active = data.agents.find(a => a.id === data.active_id);
|
|
2107
|
+
if (active) agentLabel.textContent = active.name;
|
|
2108
|
+
agentMenu.innerHTML = data.agents.map(a =>
|
|
2109
|
+
`<div class="agent-dropdown-item ${a.id === data.active_id ? 'active' : ''}" data-id="${escapeHtml(a.id)}">${escapeHtml(a.name)}</div>`
|
|
2110
|
+
).join('');
|
|
2111
|
+
agentMenu.querySelectorAll('.agent-dropdown-item').forEach(item => {
|
|
2112
|
+
item.addEventListener('click', () => switchAgent(item.dataset.id));
|
|
2113
|
+
});
|
|
2114
|
+
} catch (err) {
|
|
2115
|
+
console.error('Failed to load agents:', err);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async function switchAgent(agentId) {
|
|
2120
|
+
if (agentId === activeAgentId) {
|
|
2121
|
+
agentDropdown.classList.remove('open');
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
agentDropdown.classList.remove('open');
|
|
2125
|
+
statusEl.textContent = 'Switching agent...';
|
|
2126
|
+
currentSource = null;
|
|
2127
|
+
currentSkills = [];
|
|
2128
|
+
currentSkill = null;
|
|
2129
|
+
graphData = null;
|
|
2130
|
+
|
|
2131
|
+
try {
|
|
2132
|
+
const res = await fetch('/api/agent', {
|
|
2133
|
+
method: 'POST',
|
|
2134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2135
|
+
body: JSON.stringify({ id: agentId }),
|
|
2136
|
+
});
|
|
2137
|
+
const data = await res.json();
|
|
2138
|
+
if (data.ok) {
|
|
2139
|
+
activeAgentId = agentId;
|
|
2140
|
+
agentLabel.textContent = agentMenu.querySelector(`[data-id="${agentId}"]`).textContent;
|
|
2141
|
+
agentMenu.querySelectorAll('.agent-dropdown-item').forEach(el => {
|
|
2142
|
+
el.classList.toggle('active', el.dataset.id === agentId);
|
|
2143
|
+
});
|
|
2144
|
+
sources = data.sources;
|
|
2145
|
+
renderSources(sources);
|
|
2146
|
+
skillsList.innerHTML = '<div class="empty-state">Select a source</div>';
|
|
2147
|
+
detailPanel.innerHTML = '<div class="empty">Select a skill to view details</div>';
|
|
2148
|
+
statusEl.textContent = 'Ready';
|
|
2149
|
+
} else {
|
|
2150
|
+
statusEl.textContent = `Error: ${data.error}`;
|
|
2151
|
+
}
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
statusEl.textContent = `Error: ${err.message}`;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// Initialize
|
|
2158
|
+
async function init() {
|
|
2159
|
+
try {
|
|
2160
|
+
await loadAgents();
|
|
2161
|
+
sources = await fetchSources();
|
|
2162
|
+
renderSources(sources);
|
|
2163
|
+
statusEl.textContent = 'Ready';
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
statusEl.textContent = `Error: ${err.message}`;
|
|
2166
|
+
sourcesPanel.innerHTML = `<div class="error">Error loading sources: ${escapeHtml(err.message)}</div>`;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// WebSocket for live reload
|
|
2171
|
+
function connectWebSocket() {
|
|
2172
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
2173
|
+
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
|
|
2174
|
+
|
|
2175
|
+
ws.onmessage = (event) => {
|
|
2176
|
+
const data = JSON.parse(event.data);
|
|
2177
|
+
if (data.type === 'skill_changed') {
|
|
2178
|
+
console.log('Skill changed:', data.path);
|
|
2179
|
+
statusEl.textContent = 'File changed — refreshing...';
|
|
2180
|
+
|
|
2181
|
+
// Invalidate graph cache
|
|
2182
|
+
graphData = null;
|
|
2183
|
+
|
|
2184
|
+
// Refresh current view
|
|
2185
|
+
if (currentSkill) {
|
|
2186
|
+
fetchSkill(currentSkill).then(skill => {
|
|
2187
|
+
renderSkillDetail(skill);
|
|
2188
|
+
statusEl.textContent = `Viewing: ${skill.name} (refreshed)`;
|
|
2189
|
+
}).catch(() => {});
|
|
2190
|
+
}
|
|
2191
|
+
if (currentSource) {
|
|
2192
|
+
fetchSkills(currentSource).then(skills => {
|
|
2193
|
+
currentSkills = skills;
|
|
2194
|
+
applyFilters();
|
|
2195
|
+
}).catch(() => {});
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// Refresh sources
|
|
2199
|
+
fetchSources().then(s => {
|
|
2200
|
+
sources = s;
|
|
2201
|
+
renderSources(s);
|
|
2202
|
+
}).catch(() => {});
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
ws.onclose = () => {
|
|
2207
|
+
setTimeout(connectWebSocket, 2000);
|
|
2208
|
+
};
|
|
2209
|
+
|
|
2210
|
+
ws.onerror = () => {
|
|
2211
|
+
ws.close();
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
connectWebSocket();
|
|
2216
|
+
init();
|
|
2217
|
+
</script>
|
|
2218
|
+
</body>
|
|
2219
|
+
</html>
|