agentlytics 0.1.18 → 0.1.20
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/cache.js +3 -2
- package/editors/antigravity.js +504 -31
- package/editors/base.js +87 -0
- package/editors/claude.js +11 -1
- package/editors/codex.js +11 -0
- package/editors/copilot.js +11 -1
- package/editors/cursor.js +11 -1
- package/editors/gemini.js +11 -1
- package/editors/goose.js +30 -8
- package/editors/index.js +40 -1
- package/editors/kiro.js +11 -1
- package/editors/opencode.js +4 -22
- package/editors/vscode.js +11 -1
- package/editors/windsurf.js +21 -10
- package/editors/zed.js +23 -3
- package/index.js +40 -38
- package/package.json +1 -1
- package/server.js +101 -0
- package/ui/src/App.jsx +75 -16
- package/ui/src/components/AiAuditCard.jsx +254 -0
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/lib/api.js +19 -0
- package/ui/src/pages/Artifacts.jsx +600 -0
- package/ui/src/pages/CostAnalysis.jsx +2 -1
- package/ui/src/pages/Dashboard.jsx +2 -1
- package/ui/src/pages/ProjectDetail.jsx +8 -2
- package/ui/src/pages/Projects.jsx +2 -1
- package/ui/src/pages/RelayDashboard.jsx +2 -1
- package/ui/src/pages/Settings.jsx +2 -1
- package/ui/src/pages/Subscriptions.jsx +2 -1
package/server.js
CHANGED
|
@@ -333,6 +333,107 @@ app.put('/api/config', (req, res) => {
|
|
|
333
333
|
}
|
|
334
334
|
});
|
|
335
335
|
|
|
336
|
+
app.get('/api/check-ai', async (req, res) => {
|
|
337
|
+
const folder = req.query.folder;
|
|
338
|
+
if (!folder) return res.status(400).json({ error: 'folder query param required' });
|
|
339
|
+
try {
|
|
340
|
+
const { execFile } = require('child_process');
|
|
341
|
+
const isWindows = process.platform === 'win32';
|
|
342
|
+
// On Windows, use npx.cmd with shell; on Unix, use npx directly
|
|
343
|
+
const cmd = isWindows ? 'npx.cmd' : 'npx';
|
|
344
|
+
const result = await new Promise((resolve, reject) => {
|
|
345
|
+
execFile(cmd, ['-y', 'check-ai', '--json', folder], {
|
|
346
|
+
timeout: 60000,
|
|
347
|
+
maxBuffer: 1024 * 1024,
|
|
348
|
+
shell: isWindows
|
|
349
|
+
}, (err, stdout) => {
|
|
350
|
+
try {
|
|
351
|
+
const json = JSON.parse(stdout);
|
|
352
|
+
resolve(json);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
reject(new Error(err ? err.message : 'Failed to parse check-ai output'));
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
res.json(result);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
res.status(500).json({ error: err.message });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ============================================================
|
|
365
|
+
// Artifacts — delegates to editors/index.js getAllArtifacts
|
|
366
|
+
// ============================================================
|
|
367
|
+
|
|
368
|
+
app.get('/api/artifacts', (req, res) => {
|
|
369
|
+
try {
|
|
370
|
+
const { getAllArtifacts } = require('./editors');
|
|
371
|
+
const projects = cache.getCachedProjects({ hiddenFolders: getHiddenFolders() });
|
|
372
|
+
const result = [];
|
|
373
|
+
|
|
374
|
+
for (const project of projects) {
|
|
375
|
+
const folder = project.folder;
|
|
376
|
+
if (!folder) continue;
|
|
377
|
+
|
|
378
|
+
const artifacts = getAllArtifacts(folder);
|
|
379
|
+
if (artifacts.length === 0) continue;
|
|
380
|
+
|
|
381
|
+
// Group by editor
|
|
382
|
+
const byEditor = {};
|
|
383
|
+
for (const a of artifacts) {
|
|
384
|
+
if (!byEditor[a.editor]) byEditor[a.editor] = { editor: a.editor, label: a.editorLabel, files: [] };
|
|
385
|
+
byEditor[a.editor].files.push(a);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
result.push({
|
|
389
|
+
folder,
|
|
390
|
+
name: project.name || path.basename(folder),
|
|
391
|
+
totalArtifacts: artifacts.length,
|
|
392
|
+
editors: Object.values(byEditor),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Sort by total artifacts descending
|
|
397
|
+
result.sort((a, b) => b.totalArtifacts - a.totalArtifacts);
|
|
398
|
+
res.json(result);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
res.status(500).json({ error: err.message });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
app.get('/api/artifact-content', (req, res) => {
|
|
405
|
+
try {
|
|
406
|
+
const filePath = req.query.path;
|
|
407
|
+
if (!filePath) return res.status(400).json({ error: 'path query param required' });
|
|
408
|
+
|
|
409
|
+
// Security: validate file exists in known artifact results for at least one project
|
|
410
|
+
const { getAllArtifacts } = require('./editors');
|
|
411
|
+
const projects = cache.getCachedProjects({ hiddenFolders: getHiddenFolders() });
|
|
412
|
+
let allowed = false;
|
|
413
|
+
for (const project of projects) {
|
|
414
|
+
if (!project.folder) continue;
|
|
415
|
+
const artifacts = getAllArtifacts(project.folder);
|
|
416
|
+
if (artifacts.some(a => a.path === filePath)) { allowed = true; break; }
|
|
417
|
+
}
|
|
418
|
+
// Also check global/editor-level artifacts not tied to any project (e.g. brain files)
|
|
419
|
+
if (!allowed) {
|
|
420
|
+
try {
|
|
421
|
+
const artifacts = getAllArtifacts(null);
|
|
422
|
+
if (artifacts.some(a => a.path === filePath)) allowed = true;
|
|
423
|
+
} catch { /* skip */ }
|
|
424
|
+
}
|
|
425
|
+
if (!allowed) return res.status(403).json({ error: 'Not an artifact file' });
|
|
426
|
+
|
|
427
|
+
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'File not found' });
|
|
428
|
+
|
|
429
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
430
|
+
const stat = fs.statSync(filePath);
|
|
431
|
+
res.json({ path: filePath, name: path.basename(filePath), content, size: stat.size, modifiedAt: stat.mtime.getTime() });
|
|
432
|
+
} catch (err) {
|
|
433
|
+
res.status(500).json({ error: err.message });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
336
437
|
app.get('/api/all-projects', (req, res) => {
|
|
337
438
|
try {
|
|
338
439
|
res.json(cache.getCachedProjects({ ...parseDateOpts(req.query), includeHidden: true }));
|
package/ui/src/App.jsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
|
-
import { Routes, Route, NavLink } from 'react-router-dom'
|
|
3
|
-
import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, CreditCard, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Plug, Copy, Check, Settings as SettingsIcon } from 'lucide-react'
|
|
2
|
+
import { Routes, Route, NavLink, useLocation } from 'react-router-dom'
|
|
3
|
+
import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, CreditCard, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Plug, Copy, Check, Settings as SettingsIcon, Package, ChevronDown } from 'lucide-react'
|
|
4
4
|
import { fetchOverview, refetchAgents, fetchMode, fetchRelayConfig, getAuthToken, setOnAuthFailure } from './lib/api'
|
|
5
5
|
import { useTheme } from './lib/theme'
|
|
6
6
|
import AnimatedLogo from './components/AnimatedLogo'
|
|
7
|
+
import AnimatedLoader from './components/AnimatedLoader'
|
|
7
8
|
import LoginScreen from './components/LoginScreen'
|
|
8
9
|
import Dashboard from './pages/Dashboard'
|
|
9
10
|
import Sessions from './pages/Sessions'
|
|
@@ -13,11 +14,58 @@ import Projects from './pages/Projects'
|
|
|
13
14
|
import ProjectDetail from './pages/ProjectDetail'
|
|
14
15
|
import CostAnalysis from './pages/CostAnalysis'
|
|
15
16
|
import SqlViewer from './pages/SqlViewer'
|
|
17
|
+
import Artifacts from './pages/Artifacts'
|
|
16
18
|
import Settings from './pages/Settings'
|
|
17
19
|
import Subscriptions from './pages/Subscriptions'
|
|
18
20
|
import RelayDashboard from './pages/RelayDashboard'
|
|
19
21
|
import RelayUserDetail from './pages/RelayUserDetail'
|
|
20
22
|
|
|
23
|
+
function NavDropdown({ icon: Icon, label, items }) {
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
const location = useLocation()
|
|
26
|
+
const isActive = items.some(i => i.to === location.pathname)
|
|
27
|
+
const timeout = useRef(null)
|
|
28
|
+
|
|
29
|
+
const enter = () => { clearTimeout(timeout.current); setOpen(true) }
|
|
30
|
+
const leave = () => { timeout.current = setTimeout(() => setOpen(false), 150) }
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="relative" onMouseEnter={enter} onMouseLeave={leave}>
|
|
34
|
+
<button
|
|
35
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 text-[12px] rounded transition ${
|
|
36
|
+
isActive ? 'bg-[var(--c-card)] text-[var(--c-white)]' : 'text-[var(--c-text2)] hover:text-[var(--c-white)]'
|
|
37
|
+
}`}
|
|
38
|
+
>
|
|
39
|
+
<Icon size={12} />
|
|
40
|
+
{label}
|
|
41
|
+
<ChevronDown size={10} style={{ opacity: 0.5 }} />
|
|
42
|
+
</button>
|
|
43
|
+
{open && (
|
|
44
|
+
<div
|
|
45
|
+
className="absolute top-full left-0 mt-1 py-1 rounded shadow-lg min-w-[160px] z-[100]"
|
|
46
|
+
style={{ background: 'var(--c-bg)', border: '1px solid var(--c-border)' }}
|
|
47
|
+
>
|
|
48
|
+
{items.map(({ to, icon: SubIcon, label: subLabel }) => (
|
|
49
|
+
<NavLink
|
|
50
|
+
key={to}
|
|
51
|
+
to={to}
|
|
52
|
+
onClick={() => setOpen(false)}
|
|
53
|
+
className={({ isActive: a }) =>
|
|
54
|
+
`flex items-center gap-2 px-3 py-1.5 text-[12px] transition ${
|
|
55
|
+
a ? 'bg-[var(--c-bg3)] text-[var(--c-white)]' : 'text-[var(--c-text2)] hover:text-[var(--c-white)] hover:bg-[var(--c-bg3)]'
|
|
56
|
+
}`
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<SubIcon size={12} />
|
|
60
|
+
{subLabel}
|
|
61
|
+
</NavLink>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
export default function App() {
|
|
22
70
|
const [overview, setOverview] = useState(null)
|
|
23
71
|
const [refetchState, setRefetchState] = useState(null) // null | { scanned, total }
|
|
@@ -82,16 +130,24 @@ export default function App() {
|
|
|
82
130
|
const isRelay = mode === 'relay'
|
|
83
131
|
const showLogin = isRelay && needsAuth && !authed
|
|
84
132
|
|
|
133
|
+
const location = useLocation()
|
|
134
|
+
const isFullWidth = location.pathname === '/artifacts'
|
|
135
|
+
|
|
85
136
|
const nav = isRelay ? [
|
|
86
137
|
{ to: '/', icon: Users, label: 'Team' },
|
|
87
138
|
] : [
|
|
88
139
|
{ to: '/', icon: Activity, label: 'Dashboard' },
|
|
89
|
-
{ to: '/projects', icon: FolderOpen, label: 'Projects' },
|
|
90
140
|
{ to: '/sessions', icon: MessageSquare, label: 'Sessions' },
|
|
91
|
-
{ to: '/
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
141
|
+
{ to: '/projects', icon: FolderOpen, label: 'Projects' },
|
|
142
|
+
{ icon: DollarSign, label: 'Costs', children: [
|
|
143
|
+
{ to: '/costs', icon: DollarSign, label: 'Cost Analysis' },
|
|
144
|
+
{ to: '/subscriptions', icon: CreditCard, label: 'Subscriptions' },
|
|
145
|
+
]},
|
|
146
|
+
{ icon: BarChart3, label: 'Insights', children: [
|
|
147
|
+
{ to: '/analysis', icon: BarChart3, label: 'Deep Analysis' },
|
|
148
|
+
{ to: '/compare', icon: GitCompare, label: 'Compare' },
|
|
149
|
+
]},
|
|
150
|
+
{ to: '/artifacts', icon: Package, label: 'Artifacts' },
|
|
95
151
|
{ to: '/sql', icon: Database, label: 'SQL' },
|
|
96
152
|
]
|
|
97
153
|
|
|
@@ -107,19 +163,21 @@ export default function App() {
|
|
|
107
163
|
Agentlytics{isRelay && <span className="ml-1.5 text-[10px] font-medium px-1.5 py-0.5" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>relay</span>}
|
|
108
164
|
</span>
|
|
109
165
|
<nav className="flex gap-0.5 ml-2">
|
|
110
|
-
{nav.map((
|
|
166
|
+
{nav.map((item) => item.children ? (
|
|
167
|
+
<NavDropdown key={item.label} icon={item.icon} label={item.label} items={item.children} />
|
|
168
|
+
) : (
|
|
111
169
|
<NavLink
|
|
112
|
-
key={to}
|
|
113
|
-
to={to}
|
|
114
|
-
end={to === '/'}
|
|
170
|
+
key={item.to}
|
|
171
|
+
to={item.to}
|
|
172
|
+
end={item.to === '/'}
|
|
115
173
|
className={({ isActive }) =>
|
|
116
174
|
`flex items-center gap-1.5 px-2.5 py-1 text-[12px] rounded transition ${
|
|
117
175
|
isActive ? 'bg-[var(--c-card)] text-[var(--c-white)]' : 'text-[var(--c-text2)] hover:text-[var(--c-white)]'
|
|
118
176
|
}`
|
|
119
177
|
}
|
|
120
178
|
>
|
|
121
|
-
<
|
|
122
|
-
{label}
|
|
179
|
+
<item.icon size={12} />
|
|
180
|
+
{item.label}
|
|
123
181
|
</NavLink>
|
|
124
182
|
))}
|
|
125
183
|
</nav>
|
|
@@ -196,9 +254,9 @@ export default function App() {
|
|
|
196
254
|
</div>
|
|
197
255
|
)}
|
|
198
256
|
|
|
199
|
-
<main className={isRelay ? 'px-0' : 'p-4 max-w-[1400px] mx-auto'}>
|
|
257
|
+
<main className={isRelay ? 'px-0' : isFullWidth ? 'p-0 overflow-hidden' : 'p-4 max-w-[1400px] mx-auto'}>
|
|
200
258
|
{mode === null ? (
|
|
201
|
-
<
|
|
259
|
+
<AnimatedLoader label="Loading..." />
|
|
202
260
|
) : isRelay ? (
|
|
203
261
|
<Routes>
|
|
204
262
|
<Route path="/" element={<RelayDashboard />} />
|
|
@@ -216,13 +274,14 @@ export default function App() {
|
|
|
216
274
|
<Route path="/analysis" element={<DeepAnalysis overview={overview} />} />
|
|
217
275
|
<Route path="/compare" element={<Compare overview={overview} />} />
|
|
218
276
|
<Route path="/subscriptions" element={<Subscriptions />} />
|
|
277
|
+
<Route path="/artifacts" element={<Artifacts />} />
|
|
219
278
|
<Route path="/sql" element={<SqlViewer />} />
|
|
220
279
|
<Route path="/settings" element={<Settings />} />
|
|
221
280
|
</Routes>
|
|
222
281
|
)}
|
|
223
282
|
</main>
|
|
224
283
|
|
|
225
|
-
<footer className=
|
|
284
|
+
<footer className={`border-t mt-8 px-4 py-3 flex items-center justify-between text-[11px]${isFullWidth ? ' hidden' : ''}`} style={{ borderColor: 'var(--c-border)', color: 'var(--c-text3)' }}>
|
|
226
285
|
<div className="flex items-center gap-3">
|
|
227
286
|
<a href="https://github.com/f/agentlytics" target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 hover:text-[var(--c-text)] transition">
|
|
228
287
|
<Github size={11} />
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { ShieldCheck, Loader2, CheckCircle2, AlertTriangle, RefreshCw, Info, ChevronRight, ChevronDown, Brush, FileText, FlaskConical, Bot, Lock, Puzzle, Plug, Package } from 'lucide-react'
|
|
3
|
+
import { fetchCheckAi } from '../lib/api'
|
|
4
|
+
import SectionTitle from './SectionTitle'
|
|
5
|
+
|
|
6
|
+
const GRADE_COLORS = {
|
|
7
|
+
'A+': '#22c55e', A: '#22c55e',
|
|
8
|
+
'B+': '#4ade80', B: '#4ade80',
|
|
9
|
+
'C+': '#facc15', C: '#facc15',
|
|
10
|
+
'D+': '#f97316', D: '#f97316',
|
|
11
|
+
F: '#ef4444',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SECTION_ICONS = {
|
|
15
|
+
'Repo Hygiene': Brush,
|
|
16
|
+
'Grounding Docs': FileText,
|
|
17
|
+
'Testing': FlaskConical,
|
|
18
|
+
'Agent Configs': Bot,
|
|
19
|
+
'AI Context': Lock,
|
|
20
|
+
'Prompts & Skills': Puzzle,
|
|
21
|
+
'MCP': Plug,
|
|
22
|
+
'AI Dependencies': Package,
|
|
23
|
+
'AI Deps': Package,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Tip({ missing }) {
|
|
27
|
+
const [open, setOpen] = useState(false)
|
|
28
|
+
const ref = useRef(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!open) return
|
|
32
|
+
const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
|
|
33
|
+
document.addEventListener('mousedown', handler)
|
|
34
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
35
|
+
}, [open])
|
|
36
|
+
|
|
37
|
+
const weighted = missing.filter(f => f.weight > 0)
|
|
38
|
+
const items = weighted.length > 0 ? weighted : missing.slice(0, 5)
|
|
39
|
+
if (items.length === 0) return null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<span ref={ref} className="relative flex-shrink-0">
|
|
43
|
+
<button
|
|
44
|
+
onClick={(e) => { e.stopPropagation(); setOpen(!open) }}
|
|
45
|
+
className="flex items-center justify-center w-4 h-4 rounded-full transition hover:opacity-80"
|
|
46
|
+
style={{ color: 'var(--c-text3)', background: 'var(--c-bg3)' }}
|
|
47
|
+
>
|
|
48
|
+
<Info size={10} />
|
|
49
|
+
</button>
|
|
50
|
+
{open && (
|
|
51
|
+
<div
|
|
52
|
+
className="absolute right-0 top-6 z-50 w-64 p-2.5 rounded shadow-lg"
|
|
53
|
+
style={{ background: 'var(--c-bg)', border: '1px solid var(--c-border)', boxShadow: '0 4px 24px rgba(0,0,0,0.3)' }}
|
|
54
|
+
>
|
|
55
|
+
<div className="text-[11px] font-medium mb-1.5" style={{ color: 'var(--c-white)' }}>How to improve</div>
|
|
56
|
+
<div className="space-y-1">
|
|
57
|
+
{items.map(f => (
|
|
58
|
+
<div key={f.id} className="text-[11px]" style={{ color: 'var(--c-text2)' }}>
|
|
59
|
+
<span style={{ color: 'var(--c-text)' }}>+ {f.label}</span>
|
|
60
|
+
{f.weight > 0 && <span style={{ color: 'var(--c-text3)' }}> ({f.weight}pt)</span>}
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default function AiAuditCard({ folder }) {
|
|
71
|
+
console.log('AiAuditCard rendering, folder:', folder)
|
|
72
|
+
const [audit, setAudit] = useState(null)
|
|
73
|
+
const [loading, setLoading] = useState(false)
|
|
74
|
+
const [error, setError] = useState(null)
|
|
75
|
+
const [expanded, setExpanded] = useState(new Set())
|
|
76
|
+
|
|
77
|
+
const runAudit = async () => {
|
|
78
|
+
if (!folder) return
|
|
79
|
+
setLoading(true)
|
|
80
|
+
setError(null)
|
|
81
|
+
try {
|
|
82
|
+
const result = await fetchCheckAi(folder)
|
|
83
|
+
if (result.error) throw new Error(result.error)
|
|
84
|
+
setAudit(result)
|
|
85
|
+
} catch (e) {
|
|
86
|
+
setError(e.message)
|
|
87
|
+
}
|
|
88
|
+
setLoading(false)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
console.log('AiAuditCard useEffect triggered, folder:', folder)
|
|
93
|
+
runAudit()
|
|
94
|
+
}, [folder])
|
|
95
|
+
|
|
96
|
+
if (loading) {
|
|
97
|
+
return (
|
|
98
|
+
<div className="card p-4">
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<Loader2 size={14} className="animate-spin" style={{ color: 'var(--c-accent)' }} />
|
|
101
|
+
<span className="text-[12px]" style={{ color: 'var(--c-text2)' }}>Running AI readiness audit...</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (error) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="card p-4">
|
|
110
|
+
<div className="flex items-center justify-between">
|
|
111
|
+
<div className="flex items-center gap-2">
|
|
112
|
+
<AlertTriangle size={14} style={{ color: '#ef4444' }} />
|
|
113
|
+
<span className="text-[12px]" style={{ color: 'var(--c-text2)' }}>Audit failed: {error}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<button
|
|
116
|
+
onClick={runAudit}
|
|
117
|
+
className="flex items-center gap-1.5 px-2.5 py-1 text-[11px] rounded transition hover:opacity-80"
|
|
118
|
+
style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}
|
|
119
|
+
>
|
|
120
|
+
<RefreshCw size={10} />
|
|
121
|
+
Retry
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!audit) return null
|
|
129
|
+
|
|
130
|
+
const gradeColor = GRADE_COLORS[audit.grade] || 'var(--c-text2)'
|
|
131
|
+
const sections = audit.sections || {}
|
|
132
|
+
const findings = audit.findings || []
|
|
133
|
+
|
|
134
|
+
const findingsBySection = {}
|
|
135
|
+
for (const f of findings) {
|
|
136
|
+
const sec = f.section || 'Other'
|
|
137
|
+
if (!findingsBySection[sec]) findingsBySection[sec] = []
|
|
138
|
+
findingsBySection[sec].push(f)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="card p-4 space-y-3">
|
|
143
|
+
{/* Header row */}
|
|
144
|
+
<div className="flex items-center gap-3">
|
|
145
|
+
<ShieldCheck size={16} style={{ color: gradeColor }} />
|
|
146
|
+
<div>
|
|
147
|
+
<SectionTitle>ai readiness audit</SectionTitle>
|
|
148
|
+
<div className="text-[10px] -mt-1.5" style={{ color: 'var(--c-text3)' }}>powered by <code style={{ fontFamily: 'JetBrains Mono, monospace' }}>npx check-ai</code></div>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-2 ml-auto">
|
|
151
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>
|
|
152
|
+
{audit.checks?.passed || 0}/{audit.checks?.total || 0} checks
|
|
153
|
+
· {audit.points?.earned || 0}/{audit.points?.max || 0} pts
|
|
154
|
+
</span>
|
|
155
|
+
<button
|
|
156
|
+
onClick={runAudit}
|
|
157
|
+
className="flex items-center gap-1 px-2 py-1 text-[11px] rounded transition hover:opacity-80"
|
|
158
|
+
style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}
|
|
159
|
+
>
|
|
160
|
+
<RefreshCw size={10} />
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Score row */}
|
|
166
|
+
<div className="flex items-center gap-3">
|
|
167
|
+
<div
|
|
168
|
+
className="w-11 h-11 rounded-lg flex items-center justify-center text-base font-black flex-shrink-0"
|
|
169
|
+
style={{ background: `${gradeColor}15`, color: gradeColor, border: `1.5px solid ${gradeColor}30` }}
|
|
170
|
+
>
|
|
171
|
+
{audit.grade}
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex-1 min-w-0">
|
|
174
|
+
<div className="flex items-baseline gap-1.5">
|
|
175
|
+
<span className="text-base font-bold" style={{ color: 'var(--c-white)' }}>{audit.score}</span>
|
|
176
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>/10</span>
|
|
177
|
+
<span className="text-[11px] ml-1" style={{ color: 'var(--c-text2)' }}>{audit.label}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="w-full h-2 rounded-full overflow-hidden mt-1" style={{ background: 'var(--c-code-bg)' }}>
|
|
180
|
+
<div className="h-full rounded-full" style={{ width: `${((audit.score || 0) / 10 * 100).toFixed(1)}%`, background: gradeColor }} />
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Section rows */}
|
|
186
|
+
<div>
|
|
187
|
+
{Object.entries(sections).map(([name, sec]) => {
|
|
188
|
+
const Icon = SECTION_ICONS[name] || FileText
|
|
189
|
+
const sectionFindings = findingsBySection[name] || []
|
|
190
|
+
const passed = sectionFindings.filter(f => f.found)
|
|
191
|
+
const missing = sectionFindings.filter(f => !f.found)
|
|
192
|
+
const pct = sec.pct || 0
|
|
193
|
+
const barColor = pct >= 70 ? '#22c55e' : pct >= 40 ? '#facc15' : pct > 0 ? '#f97316' : 'var(--c-text3)'
|
|
194
|
+
|
|
195
|
+
// Build description from found findings
|
|
196
|
+
const details = []
|
|
197
|
+
for (const f of passed) {
|
|
198
|
+
if (f.detail) details.push(f.detail)
|
|
199
|
+
else if (f.matchedPath) details.push(f.matchedPath)
|
|
200
|
+
else if (f.matches && f.matches.length > 0) details.push(f.matches.join(', '))
|
|
201
|
+
else details.push(f.label)
|
|
202
|
+
}
|
|
203
|
+
const desc = details.length > 0
|
|
204
|
+
? details.slice(0, 3).join(' · ') + (details.length > 3 ? ' …' : '')
|
|
205
|
+
: 'none detected'
|
|
206
|
+
|
|
207
|
+
const isOpen = expanded.has(name)
|
|
208
|
+
const toggle = () => setExpanded(prev => {
|
|
209
|
+
const next = new Set(prev)
|
|
210
|
+
if (next.has(name)) next.delete(name)
|
|
211
|
+
else next.add(name)
|
|
212
|
+
return next
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div key={name} style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
217
|
+
{/* Main row — clickable */}
|
|
218
|
+
<div className="flex items-center gap-2.5 py-2 cursor-pointer transition hover:bg-[var(--c-bg3)]" onClick={toggle}>
|
|
219
|
+
{isOpen
|
|
220
|
+
? <ChevronDown size={12} style={{ color: 'var(--c-text3)', flexShrink: 0 }} />
|
|
221
|
+
: <ChevronRight size={12} style={{ color: 'var(--c-text3)', flexShrink: 0 }} />
|
|
222
|
+
}
|
|
223
|
+
<Icon size={14} style={{ color: 'var(--c-text3)', flexShrink: 0 }} />
|
|
224
|
+
<span className="text-[12px] font-medium w-32 flex-shrink-0" style={{ color: 'var(--c-white)' }}>{name}</span>
|
|
225
|
+
<span className="text-[11px] flex-1 truncate" style={{ color: passed.length > 0 ? 'var(--c-text2)' : 'var(--c-text3)' }}>
|
|
226
|
+
{desc}
|
|
227
|
+
</span>
|
|
228
|
+
<span className="text-[11px] flex-shrink-0" style={{ color: 'var(--c-text3)' }}>{passed.length}/{sectionFindings.length}</span>
|
|
229
|
+
<div className="w-20 h-2 rounded-full overflow-hidden flex-shrink-0" style={{ background: 'var(--c-code-bg)' }}>
|
|
230
|
+
<div className="h-full rounded-full" style={{ width: `${pct}%`, background: barColor }} />
|
|
231
|
+
</div>
|
|
232
|
+
<span className="text-[11px] w-9 text-right font-bold flex-shrink-0" style={{ color: barColor }}>{pct}%</span>
|
|
233
|
+
{missing.length > 0 && <Tip missing={missing} />}
|
|
234
|
+
</div>
|
|
235
|
+
{/* Expanded: found findings */}
|
|
236
|
+
{isOpen && passed.length > 0 && (
|
|
237
|
+
<div className="flex flex-wrap gap-x-4 gap-y-0.5 pb-2 pl-12">
|
|
238
|
+
{passed.map(f => (
|
|
239
|
+
<span key={f.id} className="inline-flex items-center gap-1.5 text-[11px] py-0.5">
|
|
240
|
+
<CheckCircle2 size={10} style={{ color: '#22c55e', flexShrink: 0 }} />
|
|
241
|
+
<span style={{ color: 'var(--c-text)' }}>{f.label}</span>
|
|
242
|
+
{f.matchedPath && <span style={{ color: 'var(--c-text3)' }}>{f.matchedPath}</span>}
|
|
243
|
+
{f.detail && <span style={{ color: 'var(--c-text2)' }}>— {f.detail}</span>}
|
|
244
|
+
</span>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)
|
|
250
|
+
})}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import AnimatedLogo from './AnimatedLogo'
|
|
2
|
+
|
|
3
|
+
export default function AnimatedLoader({ label = 'Loading...' }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex flex-col items-center justify-center py-16 gap-3">
|
|
6
|
+
<div style={{ opacity: 0.7 }}>
|
|
7
|
+
<AnimatedLogo size={32} />
|
|
8
|
+
</div>
|
|
9
|
+
{label && (
|
|
10
|
+
<span className="text-[12px]" style={{ color: 'var(--c-text3)' }}>{label}</span>
|
|
11
|
+
)}
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
package/ui/src/lib/api.js
CHANGED
|
@@ -203,11 +203,30 @@ export async function fetchToolCalls(name, opts = {}) {
|
|
|
203
203
|
return res.json();
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
export async function fetchCheckAi(folder) {
|
|
207
|
+
const q = new URLSearchParams({ folder });
|
|
208
|
+
const res = await fetch(`${BASE}/api/check-ai?${q}`);
|
|
209
|
+
return res.json();
|
|
210
|
+
}
|
|
211
|
+
|
|
206
212
|
export async function fetchUsage() {
|
|
207
213
|
const res = await fetch(`${BASE}/api/usage`);
|
|
208
214
|
return res.json();
|
|
209
215
|
}
|
|
210
216
|
|
|
217
|
+
// ── Artifacts API ──
|
|
218
|
+
|
|
219
|
+
export async function fetchArtifacts() {
|
|
220
|
+
const res = await fetch(`${BASE}/api/artifacts`);
|
|
221
|
+
return res.json();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function fetchArtifactContent(filePath) {
|
|
225
|
+
const q = new URLSearchParams({ path: filePath });
|
|
226
|
+
const res = await fetch(`${BASE}/api/artifact-content?${q}`);
|
|
227
|
+
return res.json();
|
|
228
|
+
}
|
|
229
|
+
|
|
211
230
|
// ── Relay API ──
|
|
212
231
|
|
|
213
232
|
export async function fetchMode() {
|