total-recall-brain 3.1.2 → 3.2.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/extension/background.js +99 -0
- package/extension/content-script.css +16 -0
- package/extension/content-script.js +134 -0
- package/extension/icons/README.md +27 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/lib/brain-client.js +47 -0
- package/extension/manifest.json +39 -0
- package/extension/options/options.css +218 -0
- package/extension/options/options.html +72 -0
- package/extension/options/options.js +99 -0
- package/extension/popup/popup.css +221 -0
- package/extension/popup/popup.html +44 -0
- package/extension/popup/popup.js +107 -0
- package/extension/sidepanel/sidepanel.css +340 -0
- package/extension/sidepanel/sidepanel.html +38 -0
- package/extension/sidepanel/sidepanel.js +229 -0
- package/frontend/README.md +73 -0
- package/frontend/index.html +17 -0
- package/frontend/package.json +31 -0
- package/frontend/src/App.css +2 -0
- package/frontend/src/App.tsx +390 -0
- package/frontend/src/api.ts +688 -0
- package/frontend/src/assets/hero.png +0 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/assets/vite.svg +1 -0
- package/frontend/src/components/BrainSelector.tsx +272 -0
- package/frontend/src/components/DaemonLogsTab.tsx +54 -0
- package/frontend/src/components/Graph3D.tsx +850 -0
- package/frontend/src/components/MarkdownUtils.tsx +99 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
- package/frontend/src/components/TaskQueueTab.tsx +185 -0
- package/frontend/src/index.css +1589 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/pages/ApiKeysPage.tsx +288 -0
- package/frontend/src/pages/AutomationsPage.tsx +354 -0
- package/frontend/src/pages/ChatPage.tsx +831 -0
- package/frontend/src/pages/DeploymentsPage.tsx +275 -0
- package/frontend/src/pages/FilesPage.tsx +384 -0
- package/frontend/src/pages/HealthPage.tsx +91 -0
- package/frontend/src/pages/IntegrationsPage.tsx +382 -0
- package/frontend/src/pages/LoginPage.tsx +237 -0
- package/frontend/src/pages/MemoryPage.tsx +1192 -0
- package/frontend/src/pages/SandboxPage.tsx +113 -0
- package/frontend/src/pages/SettingsPage.tsx +662 -0
- package/frontend/src/pages/SkillsPage.tsx +618 -0
- package/frontend/src/pages/TasksPage.tsx +392 -0
- package/frontend/src/pages/UsagePage.tsx +260 -0
- package/frontend/src/types.ts +163 -0
- package/frontend/tsconfig.app.json +25 -0
- package/frontend/tsconfig.json +7 -0
- package/frontend/tsconfig.node.json +24 -0
- package/frontend/vite.config.ts +24 -0
- package/package.json +8 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
- package/src/cli/backup.mjs +172 -1
- package/src/cli/setup.mjs +24 -0
- package/src/server/rest.mjs +56 -7
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
listTasks,
|
|
4
|
+
createTask,
|
|
5
|
+
listResearch,
|
|
6
|
+
createResearch,
|
|
7
|
+
fetchLogs,
|
|
8
|
+
triggerRecompile,
|
|
9
|
+
readMemory
|
|
10
|
+
} from '../api'
|
|
11
|
+
import type { Task, MemoryNode } from '../types'
|
|
12
|
+
import TaskQueueTab from '../components/TaskQueueTab'
|
|
13
|
+
import ResearchAgendaTab from '../components/ResearchAgendaTab'
|
|
14
|
+
import type { ResearchItem } from '../components/ResearchAgendaTab'
|
|
15
|
+
import DaemonLogsTab from '../components/DaemonLogsTab'
|
|
16
|
+
|
|
17
|
+
export default function TasksPage() {
|
|
18
|
+
const [tasks, setTasks] = useState<Task[]>([])
|
|
19
|
+
const [researchItems, setResearchItems] = useState<ResearchItem[]>([])
|
|
20
|
+
const [researchCounts, setResearchCounts] = useState({ pending: 0, in_progress: 0, done: 0, failed: 0 })
|
|
21
|
+
const [logs, setLogs] = useState('(loading live cognitive stream...)')
|
|
22
|
+
const [activeTab, setActiveTab] = useState<'tasks' | 'research' | 'logs'>('tasks')
|
|
23
|
+
|
|
24
|
+
const [loading, setLoading] = useState(true)
|
|
25
|
+
const [error, setError] = useState('')
|
|
26
|
+
const [recompiling, setRecompiling] = useState(false)
|
|
27
|
+
const [actionSuccess, setActionSuccess] = useState('')
|
|
28
|
+
|
|
29
|
+
// Expanded research and lazy-loaded nodes
|
|
30
|
+
const [expandedResearchId, setExpandedResearchId] = useState<string | null>(null)
|
|
31
|
+
const [loadedDiscoveries, setLoadedDiscoveries] = useState<Record<string, MemoryNode | null>>({})
|
|
32
|
+
const [loadingNodeSlugs, setLoadingNodeSlugs] = useState<Record<string, boolean>>({})
|
|
33
|
+
|
|
34
|
+
const handleToggleExpand = async (item: ResearchItem) => {
|
|
35
|
+
if (expandedResearchId === item.id) {
|
|
36
|
+
setExpandedResearchId(null)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setExpandedResearchId(item.id)
|
|
41
|
+
|
|
42
|
+
if (item.node_slug && !loadedDiscoveries[item.node_slug] && !loadingNodeSlugs[item.node_slug]) {
|
|
43
|
+
setLoadingNodeSlugs(prev => ({ ...prev, [item.node_slug!]: true }))
|
|
44
|
+
try {
|
|
45
|
+
const node = await readMemory(item.node_slug)
|
|
46
|
+
setLoadedDiscoveries(prev => ({ ...prev, [item.node_slug!]: node }))
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('Failed to load memory node for research', e)
|
|
49
|
+
} finally {
|
|
50
|
+
setLoadingNodeSlugs(prev => ({ ...prev, [item.node_slug!]: false }))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Task creation form state
|
|
56
|
+
const [showTaskForm, setShowTaskForm] = useState(false)
|
|
57
|
+
const [taskCategory, setTaskCategory] = useState('fact-seeker')
|
|
58
|
+
const [taskTarget, setTaskTarget] = useState('')
|
|
59
|
+
const [taskBody, setTaskBody] = useState('')
|
|
60
|
+
const [taskSubmitting, setTaskSubmitting] = useState(false)
|
|
61
|
+
|
|
62
|
+
// Research creation form state
|
|
63
|
+
const [showResearchForm, setShowResearchForm] = useState(false)
|
|
64
|
+
const [researchTopic, setResearchTopic] = useState('')
|
|
65
|
+
const [researchPriority, setResearchPriority] = useState<'low' | 'medium' | 'high' | 'critical'>('medium')
|
|
66
|
+
const [researchNotes, setResearchNotes] = useState('')
|
|
67
|
+
const [researchSubmitting, setResearchSubmitting] = useState(false)
|
|
68
|
+
|
|
69
|
+
// Fetch Scheduler Tasks
|
|
70
|
+
const fetchTasks = useCallback(async () => {
|
|
71
|
+
try {
|
|
72
|
+
const data = await listTasks()
|
|
73
|
+
setTasks(data || [])
|
|
74
|
+
setError('')
|
|
75
|
+
} catch (e) {
|
|
76
|
+
setError((e as Error).message)
|
|
77
|
+
}
|
|
78
|
+
}, [])
|
|
79
|
+
|
|
80
|
+
// Fetch Research Agenda
|
|
81
|
+
const fetchResearch = useCallback(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const res = await listResearch('all')
|
|
84
|
+
setResearchItems(res.items || [])
|
|
85
|
+
setResearchCounts(res.counts || { pending: 0, in_progress: 0, done: 0, failed: 0 })
|
|
86
|
+
setError('')
|
|
87
|
+
} catch (e) {
|
|
88
|
+
setError((e as Error).message)
|
|
89
|
+
}
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
// Fetch Daemon (Cognitive Engine) Logs
|
|
93
|
+
const fetchDaemonLogs = useCallback(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const data = await fetchLogs('daemon')
|
|
96
|
+
setLogs(data.content || '(no logs streamed yet)')
|
|
97
|
+
} catch (e) {
|
|
98
|
+
setLogs(`Error connecting to cognitive logs: ${(e as Error).message}`)
|
|
99
|
+
}
|
|
100
|
+
}, [])
|
|
101
|
+
|
|
102
|
+
// Combined Refresh
|
|
103
|
+
const refreshAll = useCallback(async () => {
|
|
104
|
+
setLoading(true)
|
|
105
|
+
await Promise.all([fetchTasks(), fetchResearch(), fetchDaemonLogs()])
|
|
106
|
+
setLoading(false)
|
|
107
|
+
}, [fetchTasks, fetchResearch, fetchDaemonLogs])
|
|
108
|
+
|
|
109
|
+
// Polling loop
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const init = async () => {
|
|
112
|
+
await Promise.all([fetchTasks(), fetchResearch(), fetchDaemonLogs()])
|
|
113
|
+
setLoading(false)
|
|
114
|
+
}
|
|
115
|
+
void init()
|
|
116
|
+
|
|
117
|
+
const interval = setInterval(() => {
|
|
118
|
+
void fetchTasks()
|
|
119
|
+
void fetchResearch()
|
|
120
|
+
void fetchDaemonLogs()
|
|
121
|
+
}, 3000)
|
|
122
|
+
|
|
123
|
+
return () => clearInterval(interval)
|
|
124
|
+
}, [fetchTasks, fetchResearch, fetchDaemonLogs])
|
|
125
|
+
|
|
126
|
+
// Trigger Brain Recompilation
|
|
127
|
+
const handleRecompile = async () => {
|
|
128
|
+
setRecompiling(true)
|
|
129
|
+
setError('')
|
|
130
|
+
setActionSuccess('')
|
|
131
|
+
try {
|
|
132
|
+
const res = await triggerRecompile()
|
|
133
|
+
setActionSuccess(res.message || 'Brain surface recompiled successfully!')
|
|
134
|
+
setTimeout(() => setActionSuccess(''), 4000)
|
|
135
|
+
void refreshAll()
|
|
136
|
+
} catch (e) {
|
|
137
|
+
setError((e as Error).message)
|
|
138
|
+
} finally {
|
|
139
|
+
setRecompiling(false)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Seed Autonomous Gaps to fill the queue
|
|
144
|
+
const handleSeedGaps = async () => {
|
|
145
|
+
setError('')
|
|
146
|
+
setActionSuccess('')
|
|
147
|
+
try {
|
|
148
|
+
await createTask(
|
|
149
|
+
'fact-seeker',
|
|
150
|
+
'Audit latest Chrome Extension MV3 API updates',
|
|
151
|
+
'Verify if chrome.declarativeNetRequest rulesets have new declarative properties.'
|
|
152
|
+
)
|
|
153
|
+
await createTask(
|
|
154
|
+
'post-mortem-daily',
|
|
155
|
+
'Ingest yesterday\'s IDE compile session logs',
|
|
156
|
+
'Analyze TypeScript build latency and suggest performance optimizations.'
|
|
157
|
+
)
|
|
158
|
+
await createTask(
|
|
159
|
+
'clarity-rewriter',
|
|
160
|
+
'Harden security.yml authentication encryption',
|
|
161
|
+
'Enforce bcrypt rounds to 12 and verify cross-origin session domain locks.'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
await createResearch(
|
|
165
|
+
'TypeScript 5.4 advanced mapped types and template literal constraints',
|
|
166
|
+
'high',
|
|
167
|
+
'Evaluate mapped type performance on large monorepos.'
|
|
168
|
+
)
|
|
169
|
+
await createResearch(
|
|
170
|
+
'Glassmorphism backdrop-filter performance optimizations on mobile Safari',
|
|
171
|
+
'medium',
|
|
172
|
+
'Prevent GPU compositing lag by limiting blur radius.'
|
|
173
|
+
)
|
|
174
|
+
await createResearch(
|
|
175
|
+
'Local LLM quantization (GGUF vs EXL2) memory efficiency on macOS',
|
|
176
|
+
'high',
|
|
177
|
+
'Check metal buffer usage in Ollama backend models.'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
setActionSuccess('Autonomous gaps seeded successfully! Task queue and research agenda are now full.')
|
|
181
|
+
setTimeout(() => setActionSuccess(''), 5000)
|
|
182
|
+
void refreshAll()
|
|
183
|
+
} catch (e) {
|
|
184
|
+
setError((e as Error).message)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create Task Handler
|
|
189
|
+
const handleCreateTask = async (e: React.FormEvent) => {
|
|
190
|
+
e.preventDefault()
|
|
191
|
+
if (!taskTarget.trim()) return
|
|
192
|
+
setTaskSubmitting(true)
|
|
193
|
+
setError('')
|
|
194
|
+
try {
|
|
195
|
+
await createTask(taskCategory, taskTarget.trim(), taskBody.trim())
|
|
196
|
+
setTaskTarget('')
|
|
197
|
+
setTaskBody('')
|
|
198
|
+
setShowTaskForm(false)
|
|
199
|
+
setActionSuccess('Task enqueued successfully!')
|
|
200
|
+
setTimeout(() => setActionSuccess(''), 3000)
|
|
201
|
+
void fetchTasks()
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setError((err as Error).message)
|
|
204
|
+
} finally {
|
|
205
|
+
setTaskSubmitting(false)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create Research Handler
|
|
210
|
+
const handleCreateResearch = async (e: React.FormEvent) => {
|
|
211
|
+
e.preventDefault()
|
|
212
|
+
if (!researchTopic.trim()) return
|
|
213
|
+
setResearchSubmitting(true)
|
|
214
|
+
setError('')
|
|
215
|
+
try {
|
|
216
|
+
await createResearch(researchTopic.trim(), researchPriority, researchNotes.trim() || undefined)
|
|
217
|
+
setResearchTopic('')
|
|
218
|
+
setResearchNotes('')
|
|
219
|
+
setShowResearchForm(false)
|
|
220
|
+
setActionSuccess('Research project enqueued successfully!')
|
|
221
|
+
setTimeout(() => setActionSuccess(''), 3000)
|
|
222
|
+
void fetchResearch()
|
|
223
|
+
} catch (err) {
|
|
224
|
+
setError((err as Error).message)
|
|
225
|
+
} finally {
|
|
226
|
+
setResearchSubmitting(false)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="page">
|
|
232
|
+
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
|
233
|
+
<div>
|
|
234
|
+
<h1>Cognitive Dashboard</h1>
|
|
235
|
+
<p>Real-time background reasoning engine, autonomous research, and active schedulers</p>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div style={{ display: 'flex', gap: 10 }}>
|
|
239
|
+
<button
|
|
240
|
+
className="btn btn-ghost btn-sm"
|
|
241
|
+
onClick={handleSeedGaps}
|
|
242
|
+
style={{
|
|
243
|
+
border: '1px solid rgba(59, 130, 246, 0.4)',
|
|
244
|
+
background: 'rgba(59, 130, 246, 0.05)',
|
|
245
|
+
color: '#60a5fa'
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
🔥 Seed Gaps
|
|
249
|
+
</button>
|
|
250
|
+
|
|
251
|
+
<button
|
|
252
|
+
className="btn btn-sm"
|
|
253
|
+
onClick={handleRecompile}
|
|
254
|
+
disabled={recompiling}
|
|
255
|
+
style={{
|
|
256
|
+
background: recompiling ? 'var(--bg-secondary)' : 'linear-gradient(135deg, #3b82f6, #1d4ed8)',
|
|
257
|
+
color: '#ffffff',
|
|
258
|
+
border: 'none',
|
|
259
|
+
cursor: recompiling ? 'not-allowed' : 'pointer'
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{recompiling ? (
|
|
263
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
264
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ animation: 'spin 1s linear infinite' }}>
|
|
265
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
|
266
|
+
</svg>
|
|
267
|
+
Compiling...
|
|
268
|
+
</span>
|
|
269
|
+
) : 'Recompile Surface'}
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{error && <div className="badge badge-error" style={{ marginBottom: 16, display: 'inline-flex' }}>⚠️ {error}</div>}
|
|
275
|
+
{actionSuccess && <div className="badge badge-success" style={{ marginBottom: 16, display: 'inline-flex', background: 'rgba(16, 185, 129, 0.1)', border: '1px solid #10b981', color: '#34d399' }}>✓ {actionSuccess}</div>}
|
|
276
|
+
|
|
277
|
+
{/* Tabs Layout */}
|
|
278
|
+
<div style={{
|
|
279
|
+
display: 'flex',
|
|
280
|
+
borderBottom: '1px solid var(--border)',
|
|
281
|
+
marginBottom: 20,
|
|
282
|
+
gap: 20,
|
|
283
|
+
}}>
|
|
284
|
+
<button
|
|
285
|
+
onClick={() => setActiveTab('tasks')}
|
|
286
|
+
style={{
|
|
287
|
+
background: 'none',
|
|
288
|
+
border: 'none',
|
|
289
|
+
borderBottom: activeTab === 'tasks' ? '2px solid #3b82f6' : '2px solid transparent',
|
|
290
|
+
color: activeTab === 'tasks' ? 'var(--text-primary)' : 'var(--text-tertiary)',
|
|
291
|
+
padding: '10px 4px',
|
|
292
|
+
cursor: 'pointer',
|
|
293
|
+
fontSize: 14,
|
|
294
|
+
fontWeight: 500,
|
|
295
|
+
display: 'flex',
|
|
296
|
+
alignItems: 'center',
|
|
297
|
+
gap: 8
|
|
298
|
+
}}
|
|
299
|
+
>
|
|
300
|
+
📅 Task Queue
|
|
301
|
+
<span className="badge badge-accent" style={{ fontSize: 10, padding: '2px 6px' }}>{tasks.length}</span>
|
|
302
|
+
</button>
|
|
303
|
+
|
|
304
|
+
<button
|
|
305
|
+
onClick={() => setActiveTab('research')}
|
|
306
|
+
style={{
|
|
307
|
+
background: 'none',
|
|
308
|
+
border: 'none',
|
|
309
|
+
borderBottom: activeTab === 'research' ? '2px solid #3b82f6' : '2px solid transparent',
|
|
310
|
+
color: activeTab === 'research' ? 'var(--text-primary)' : 'var(--text-tertiary)',
|
|
311
|
+
padding: '10px 4px',
|
|
312
|
+
cursor: 'pointer',
|
|
313
|
+
fontSize: 14,
|
|
314
|
+
fontWeight: 500,
|
|
315
|
+
display: 'flex',
|
|
316
|
+
alignItems: 'center',
|
|
317
|
+
gap: 8
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
320
|
+
🔬 Research Agenda
|
|
321
|
+
<span className="badge" style={{ fontSize: 10, padding: '2px 6px', background: 'rgba(16, 185, 129, 0.1)', color: '#34d399', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
|
|
322
|
+
{researchCounts.pending + researchCounts.in_progress}
|
|
323
|
+
</span>
|
|
324
|
+
</button>
|
|
325
|
+
|
|
326
|
+
<button
|
|
327
|
+
onClick={() => setActiveTab('logs')}
|
|
328
|
+
style={{
|
|
329
|
+
background: 'none',
|
|
330
|
+
border: 'none',
|
|
331
|
+
borderBottom: activeTab === 'logs' ? '2px solid #3b82f6' : '2px solid transparent',
|
|
332
|
+
color: activeTab === 'logs' ? 'var(--text-primary)' : 'var(--text-tertiary)',
|
|
333
|
+
padding: '10px 4px',
|
|
334
|
+
cursor: 'pointer',
|
|
335
|
+
fontSize: 14,
|
|
336
|
+
fontWeight: 500,
|
|
337
|
+
display: 'flex',
|
|
338
|
+
alignItems: 'center',
|
|
339
|
+
gap: 8
|
|
340
|
+
}}
|
|
341
|
+
>
|
|
342
|
+
📟 Live Brain Stream
|
|
343
|
+
<span className="pulse" style={{ background: '#10b981', width: 6, height: 6, borderRadius: '50%' }} />
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
{/* Tab Panels */}
|
|
348
|
+
{activeTab === 'tasks' && (
|
|
349
|
+
<TaskQueueTab
|
|
350
|
+
tasks={tasks}
|
|
351
|
+
loading={loading}
|
|
352
|
+
showTaskForm={showTaskForm}
|
|
353
|
+
setShowTaskForm={setShowTaskForm}
|
|
354
|
+
taskCategory={taskCategory}
|
|
355
|
+
setTaskCategory={setTaskCategory}
|
|
356
|
+
taskTarget={taskTarget}
|
|
357
|
+
setTaskTarget={setTaskTarget}
|
|
358
|
+
taskBody={taskBody}
|
|
359
|
+
setTaskBody={setTaskBody}
|
|
360
|
+
taskSubmitting={taskSubmitting}
|
|
361
|
+
handleCreateTask={handleCreateTask}
|
|
362
|
+
/>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{activeTab === 'research' && (
|
|
366
|
+
<ResearchAgendaTab
|
|
367
|
+
researchItems={researchItems}
|
|
368
|
+
loading={loading}
|
|
369
|
+
showResearchForm={showResearchForm}
|
|
370
|
+
setShowResearchForm={setShowResearchForm}
|
|
371
|
+
researchTopic={researchTopic}
|
|
372
|
+
setResearchTopic={setResearchTopic}
|
|
373
|
+
researchPriority={researchPriority}
|
|
374
|
+
setResearchPriority={setResearchPriority}
|
|
375
|
+
researchNotes={researchNotes}
|
|
376
|
+
setResearchNotes={setResearchNotes}
|
|
377
|
+
researchSubmitting={researchSubmitting}
|
|
378
|
+
handleCreateResearch={handleCreateResearch}
|
|
379
|
+
expandedResearchId={expandedResearchId}
|
|
380
|
+
handleToggleExpand={handleToggleExpand}
|
|
381
|
+
loadedDiscoveries={loadedDiscoveries}
|
|
382
|
+
loadingNodeSlugs={loadingNodeSlugs}
|
|
383
|
+
refreshResearch={fetchResearch}
|
|
384
|
+
/>
|
|
385
|
+
)}
|
|
386
|
+
|
|
387
|
+
{activeTab === 'logs' && (
|
|
388
|
+
<DaemonLogsTab logs={logs} activeTab={activeTab} />
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchUsageStats, fetchConfigJson } from '../api';
|
|
3
|
+
import type { ConfigJson } from '../types';
|
|
4
|
+
|
|
5
|
+
interface UsageBreakdown {
|
|
6
|
+
dailyUsd: number;
|
|
7
|
+
weeklyUsd: number;
|
|
8
|
+
dailyTokens?: number;
|
|
9
|
+
weeklyTokens?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface UsageData {
|
|
13
|
+
timestamp: string;
|
|
14
|
+
dailyUsd: number;
|
|
15
|
+
weeklyUsd: number;
|
|
16
|
+
breakdown: {
|
|
17
|
+
gemini: UsageBreakdown;
|
|
18
|
+
claude: UsageBreakdown;
|
|
19
|
+
codex: UsageBreakdown;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function UsagePage() {
|
|
24
|
+
const [usage, setUsage] = useState<UsageData | null>(null);
|
|
25
|
+
const [config, setConfig] = useState<ConfigJson | null>(null);
|
|
26
|
+
const [loading, setLoading] = useState(true);
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const fetchUsageAndConfig = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const stats = await fetchUsageStats();
|
|
32
|
+
setUsage(stats);
|
|
33
|
+
|
|
34
|
+
const configData = await fetchConfigJson();
|
|
35
|
+
setConfig(configData);
|
|
36
|
+
setError(null);
|
|
37
|
+
} catch (err: unknown) {
|
|
38
|
+
setError((err as Error).message || 'Failed to sync budget consumption stats.');
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate statistics sync on mount
|
|
46
|
+
void fetchUsageAndConfig();
|
|
47
|
+
const interval = setInterval(fetchUsageAndConfig, 5000);
|
|
48
|
+
return () => clearInterval(interval);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
if (loading) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="page" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
54
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2.5" style={{ animation: 'spin 1s linear infinite', marginBottom: 12 }}>
|
|
55
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
|
56
|
+
</svg>
|
|
57
|
+
<div style={{ color: 'var(--text-secondary)' }}>Aggregating token expenditures...</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dailyCap = config?.budget?.budget?.daily_cap_usd ?? 5.0;
|
|
63
|
+
const weeklyCap = config?.budget?.budget?.weekly_cap_usd ?? 25.0;
|
|
64
|
+
const budgetEnabled = config?.budget?.budget?.enabled !== false;
|
|
65
|
+
|
|
66
|
+
const dailyPercent = Math.min(100, ((usage?.dailyUsd || 0) / dailyCap) * 100);
|
|
67
|
+
const weeklyPercent = Math.min(100, ((usage?.weeklyUsd || 0) / weeklyCap) * 100);
|
|
68
|
+
|
|
69
|
+
const getProgressColor = (percent: number) => {
|
|
70
|
+
if (percent > 85) return 'var(--error)';
|
|
71
|
+
if (percent > 60) return 'var(--warning)';
|
|
72
|
+
return 'var(--success)';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflowY: 'auto' }}>
|
|
77
|
+
<div className="page-header" style={{ marginBottom: 24 }}>
|
|
78
|
+
<h1>Token & Budget Statistics</h1>
|
|
79
|
+
<p>Monitor your token cost expenditure in real time against safety budget limits</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{error && (
|
|
83
|
+
<div className="badge badge-error" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #ef4444', color: '#f87171' }}>
|
|
84
|
+
⚠️ {error}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{/* Overview dollar cards */}
|
|
89
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(340px, 1fr))', gap: 24, marginBottom: 24 }}>
|
|
90
|
+
|
|
91
|
+
{/* Daily Cap progress card */}
|
|
92
|
+
<div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
93
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
94
|
+
<div>
|
|
95
|
+
<h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Daily Expenditure</h3>
|
|
96
|
+
<div style={{ fontSize: 28, fontWeight: 700, marginTop: 4, color: '#fff' }}>
|
|
97
|
+
${usage?.dailyUsd.toFixed(4) || '0.0000'}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<span className="badge" style={{ background: 'var(--bg-elevated)', color: 'var(--text-secondary)', border: '1px solid var(--border)', fontSize: 11 }}>
|
|
101
|
+
Cap: ${dailyCap.toFixed(2)}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
|
|
106
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--text-secondary)' }}>
|
|
107
|
+
<span>Consumption Ratio</span>
|
|
108
|
+
<span>{dailyPercent.toFixed(1)}%</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div style={{ height: 6, background: 'var(--bg-elevated)', borderRadius: 3, overflow: 'hidden' }}>
|
|
111
|
+
<div style={{
|
|
112
|
+
height: '100%',
|
|
113
|
+
width: `${dailyPercent}%`,
|
|
114
|
+
background: getProgressColor(dailyPercent),
|
|
115
|
+
boxShadow: `0 0 8px ${getProgressColor(dailyPercent)}`,
|
|
116
|
+
transition: 'width 0.4s ease'
|
|
117
|
+
}} />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', borderTop: '1px solid var(--border)', paddingTop: 10, marginTop: 4 }}>
|
|
122
|
+
Status: {budgetEnabled ? '🛡️ Budget supervision active' : '⚠️ Limits unmonitored (disabled)'}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Weekly Cap progress card */}
|
|
127
|
+
<div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
128
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
129
|
+
<div>
|
|
130
|
+
<h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Weekly Expenditure</h3>
|
|
131
|
+
<div style={{ fontSize: 28, fontWeight: 700, marginTop: 4, color: '#fff' }}>
|
|
132
|
+
${usage?.weeklyUsd.toFixed(4) || '0.0000'}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<span className="badge" style={{ background: 'var(--bg-elevated)', color: 'var(--text-secondary)', border: '1px solid var(--border)', fontSize: 11 }}>
|
|
136
|
+
Cap: ${weeklyCap.toFixed(2)}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
|
|
141
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--text-secondary)' }}>
|
|
142
|
+
<span>Consumption Ratio</span>
|
|
143
|
+
<span>{weeklyPercent.toFixed(1)}%</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div style={{ height: 6, background: 'var(--bg-elevated)', borderRadius: 3, overflow: 'hidden' }}>
|
|
146
|
+
<div style={{
|
|
147
|
+
height: '100%',
|
|
148
|
+
width: `${weeklyPercent}%`,
|
|
149
|
+
background: getProgressColor(weeklyPercent),
|
|
150
|
+
boxShadow: `0 0 8px ${getProgressColor(weeklyPercent)}`,
|
|
151
|
+
transition: 'width 0.4s ease'
|
|
152
|
+
}} />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', borderTop: '1px solid var(--border)', paddingTop: 10, marginTop: 4 }}>
|
|
157
|
+
Rolling window: Last 7 days in local system time
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Per Model breakdown list */}
|
|
164
|
+
<div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', border: '1px solid var(--border)' }}>
|
|
165
|
+
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, borderBottom: '1px solid var(--border)', paddingBottom: 10, marginBottom: 16 }}>
|
|
166
|
+
Model Engine Spend breakdown
|
|
167
|
+
</h3>
|
|
168
|
+
|
|
169
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 20 }}>
|
|
170
|
+
|
|
171
|
+
{/* Gemini */}
|
|
172
|
+
<div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
173
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
174
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
175
|
+
<span style={{ fontSize: 18 }}>♊</span>
|
|
176
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Gemini</span>
|
|
177
|
+
</div>
|
|
178
|
+
<span className="badge" style={{ background: 'rgba(59, 130, 246, 0.1)', color: '#60a5fa', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
|
|
179
|
+
Google API
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
|
|
184
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
185
|
+
<span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
|
|
186
|
+
<span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.gemini.dailyUsd.toFixed(4)}</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
189
|
+
<span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
|
|
190
|
+
<span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.gemini.weeklyUsd.toFixed(4)}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
|
|
193
|
+
<span>Rolling Tokens:</span>
|
|
194
|
+
<span>{usage?.breakdown.gemini.dailyTokens?.toLocaleString() || 0} (24h)</span>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Claude */}
|
|
200
|
+
<div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
201
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
202
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
203
|
+
<span style={{ fontSize: 18 }}>🍊</span>
|
|
204
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Claude</span>
|
|
205
|
+
</div>
|
|
206
|
+
<span className="badge" style={{ background: 'rgba(239, 107, 107, 0.1)', color: '#f87171', border: '1px solid rgba(239, 107, 107, 0.2)' }}>
|
|
207
|
+
Anthropic API
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
|
|
212
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
213
|
+
<span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
|
|
214
|
+
<span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.claude.dailyUsd.toFixed(4)}</span>
|
|
215
|
+
</div>
|
|
216
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
217
|
+
<span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
|
|
218
|
+
<span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.claude.weeklyUsd.toFixed(4)}</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
|
|
221
|
+
<span>Rolling Tokens:</span>
|
|
222
|
+
<span>(Ingested stats-cache)</span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Codex */}
|
|
228
|
+
<div style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', padding: 16, borderRadius: 10, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
229
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
230
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
231
|
+
<span style={{ fontSize: 18 }}>💻</span>
|
|
232
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: '#fff' }}>Codex</span>
|
|
233
|
+
</div>
|
|
234
|
+
<span className="badge" style={{ background: 'rgba(16, 185, 129, 0.1)', color: '#34d399', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
|
|
235
|
+
OpenAI API
|
|
236
|
+
</span>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12 }}>
|
|
240
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
241
|
+
<span style={{ color: 'var(--text-secondary)' }}>Daily Spend:</span>
|
|
242
|
+
<span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.codex.dailyUsd.toFixed(4)}</span>
|
|
243
|
+
</div>
|
|
244
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
245
|
+
<span style={{ color: 'var(--text-secondary)' }}>Weekly Spend:</span>
|
|
246
|
+
<span style={{ fontWeight: 600, color: '#fff' }}>${usage?.breakdown.codex.weeklyUsd.toFixed(4)}</span>
|
|
247
|
+
</div>
|
|
248
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border)', paddingTop: 6, marginTop: 4, fontSize: 11, color: 'var(--text-tertiary)' }}>
|
|
249
|
+
<span>Rolling Tokens:</span>
|
|
250
|
+
<span>{usage?.breakdown.codex.dailyTokens?.toLocaleString() || 0} (24h)</span>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|