total-recall-brain 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/antigravity.mjs +19 -8
- package/bin/total-recall.mjs +3 -0
- package/extension/content-script.js +354 -11
- package/extension/popup/popup.html +5 -0
- package/extension/popup/popup.js +25 -0
- package/frontend/src/App.tsx +29 -0
- package/frontend/src/api.ts +40 -0
- package/frontend/src/pages/ChatPage.tsx +101 -8
- package/frontend/src/pages/CollabPage.tsx +470 -0
- package/frontend/src/pages/GraphPage.tsx +62 -0
- package/frontend/src/pages/HealthPage.tsx +185 -8
- package/frontend/src/pages/HelpPage.tsx +141 -0
- package/frontend/src/pages/SettingsPage.tsx +197 -2
- package/frontend/src/types.ts +16 -0
- package/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/research-queue.jsonl +1 -0
- package/src/cli/collab.mjs +49 -0
- package/src/cli/help.mjs +1 -1
- package/src/core/dream.mjs +41 -0
- package/src/core/runtime.mjs +30 -7
- package/src/core/schema.mjs +481 -0
- package/src/core/source-adapters.mjs +14 -6
- package/src/server/index.mjs +21 -1
- package/src/server/rest.mjs +158 -3
- package/src/server/route-manifest.json +12 -0
- package/src/server/routes/collab.mjs +286 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { listMemory, listResearch, fetchChatThreads } from '../api'
|
|
3
|
+
import type { MemoryNode, ResearchItem } from '../types'
|
|
4
|
+
import type { ChatThread } from '../api'
|
|
5
|
+
import Graph3D from '../components/Graph3D'
|
|
6
|
+
|
|
7
|
+
export default function GraphPage({ activeBrainId }: { activeBrainId?: string }) {
|
|
8
|
+
const [memoryNodes, setMemoryNodes] = useState<MemoryNode[]>([])
|
|
9
|
+
const [researchItems, setResearchItems] = useState<ResearchItem[]>([])
|
|
10
|
+
const [threads, setThreads] = useState<ChatThread[]>([])
|
|
11
|
+
const [loading, setLoading] = useState(true)
|
|
12
|
+
const [selectedGroundingNodes, setSelectedGroundingNodes] = useState<string[]>([])
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
async function loadData() {
|
|
16
|
+
setLoading(true)
|
|
17
|
+
try {
|
|
18
|
+
const [nodes, researchRes, threadsRes] = await Promise.all([
|
|
19
|
+
listMemory(activeBrainId),
|
|
20
|
+
listResearch(activeBrainId).catch(() => ({ items: [] })),
|
|
21
|
+
fetchChatThreads().catch(() => [])
|
|
22
|
+
])
|
|
23
|
+
setMemoryNodes(nodes)
|
|
24
|
+
setResearchItems(researchRes.items || [])
|
|
25
|
+
setThreads(threadsRes)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('Failed to load graph data', err)
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
loadData()
|
|
33
|
+
}, [activeBrainId])
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="page" style={{ padding: 0, height: 'calc(100vh - 1px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
37
|
+
<div style={{ padding: '16px 24px', background: 'rgba(18, 18, 26, 0.45)', backdropFilter: 'blur(16px)', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
|
|
38
|
+
<h1 style={{ fontSize: '18px', fontWeight: 700 }}>Sovereign Memory Network</h1>
|
|
39
|
+
<p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '2px' }}>Interactive 3D constellation layout of your knowledge vault, sessions, and active thoughts.</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
|
|
43
|
+
{loading ? (
|
|
44
|
+
<div className="skeleton" style={{ width: '100%', height: '100%' }} />
|
|
45
|
+
) : (
|
|
46
|
+
<Graph3D
|
|
47
|
+
threads={threads}
|
|
48
|
+
memoryNodes={memoryNodes}
|
|
49
|
+
researchItems={researchItems}
|
|
50
|
+
onOpenThread={() => {}}
|
|
51
|
+
onGroundMemoryNode={(slug) => {
|
|
52
|
+
setSelectedGroundingNodes(prev =>
|
|
53
|
+
prev.includes(slug) ? prev.filter(s => s !== slug) : [...prev, slug]
|
|
54
|
+
)
|
|
55
|
+
}}
|
|
56
|
+
selectedGroundingNodes={selectedGroundingNodes}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
|
-
import { fetchHealth } from '../api'
|
|
3
|
-
import type { HealthData } from '../types'
|
|
2
|
+
import { fetchHealth, checkUpdate, runUpdate } from '../api'
|
|
3
|
+
import type { HealthData, UpdateCheckResult } from '../types'
|
|
4
4
|
|
|
5
5
|
function formatUptime(seconds: number): string {
|
|
6
6
|
const d = Math.floor(seconds / 86400)
|
|
@@ -15,6 +15,12 @@ export default function HealthPage() {
|
|
|
15
15
|
const [health, setHealth] = useState<HealthData | null>(null)
|
|
16
16
|
const [error, setError] = useState('')
|
|
17
17
|
const [lastPoll, setLastPoll] = useState<Date | null>(null)
|
|
18
|
+
|
|
19
|
+
// Auto-Update States
|
|
20
|
+
const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null)
|
|
21
|
+
const [isUpdating, setIsUpdating] = useState(false)
|
|
22
|
+
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
|
23
|
+
const [updateError, setUpdateError] = useState('')
|
|
18
24
|
|
|
19
25
|
useEffect(() => {
|
|
20
26
|
let active = true
|
|
@@ -33,17 +39,171 @@ export default function HealthPage() {
|
|
|
33
39
|
|
|
34
40
|
poll()
|
|
35
41
|
const interval = setInterval(poll, 5000)
|
|
36
|
-
|
|
42
|
+
|
|
43
|
+
// Check for updates
|
|
44
|
+
const checkVersion = async () => {
|
|
45
|
+
try {
|
|
46
|
+
const data = await checkUpdate()
|
|
47
|
+
if (active) setUpdateInfo(data)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('Failed to check updates', e)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
checkVersion()
|
|
53
|
+
const updateInterval = setInterval(checkVersion, 3600000)
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
active = false;
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
clearInterval(updateInterval);
|
|
59
|
+
}
|
|
37
60
|
}, [])
|
|
38
61
|
|
|
62
|
+
const handleStartUpdate = async () => {
|
|
63
|
+
setShowConfirmModal(false)
|
|
64
|
+
setIsUpdating(true)
|
|
65
|
+
setUpdateError('')
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await runUpdate()
|
|
69
|
+
|
|
70
|
+
// Keep polling health to check when it goes down and recovers
|
|
71
|
+
let serverDown = false
|
|
72
|
+
const pollInterval = setInterval(async () => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetchHealth()
|
|
75
|
+
if (serverDown && res.status === 'healthy') {
|
|
76
|
+
clearInterval(pollInterval)
|
|
77
|
+
window.location.reload()
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Connection failed = server is offline / restarting
|
|
81
|
+
serverDown = true
|
|
82
|
+
}
|
|
83
|
+
}, 2000)
|
|
84
|
+
|
|
85
|
+
} catch (e) {
|
|
86
|
+
setUpdateError((e as Error).message)
|
|
87
|
+
setIsUpdating(false)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
39
91
|
return (
|
|
40
|
-
<div className="page">
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
92
|
+
<div className="page" style={{ position: 'relative' }}>
|
|
93
|
+
{isUpdating && (
|
|
94
|
+
<div style={{
|
|
95
|
+
position: 'fixed',
|
|
96
|
+
inset: 0,
|
|
97
|
+
background: 'rgba(13, 17, 23, 0.9)',
|
|
98
|
+
zIndex: 1000,
|
|
99
|
+
display: 'flex',
|
|
100
|
+
flexDirection: 'column',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
justifyContent: 'center',
|
|
103
|
+
backdropFilter: 'blur(8px)',
|
|
104
|
+
animation: 'fadeIn 0.3s ease'
|
|
105
|
+
}}>
|
|
106
|
+
<div className="spinner" style={{
|
|
107
|
+
width: 50,
|
|
108
|
+
height: 50,
|
|
109
|
+
border: '4px solid var(--border)',
|
|
110
|
+
borderTopColor: 'var(--accent)',
|
|
111
|
+
borderRadius: '50%',
|
|
112
|
+
animation: 'spin 1s linear infinite',
|
|
113
|
+
marginBottom: 20
|
|
114
|
+
}} />
|
|
115
|
+
<h2 style={{ color: 'var(--text-primary)', marginBottom: 8 }}>Updating Total Recall...</h2>
|
|
116
|
+
<p style={{ color: 'var(--text-muted)', fontSize: 14 }}>Applying git changes, installing npm modules, and restarting daemon.</p>
|
|
117
|
+
<p style={{ color: 'var(--accent)', fontSize: 13, marginTop: 10 }}>Please do not close this window.</p>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{showConfirmModal && (
|
|
122
|
+
<div style={{
|
|
123
|
+
position: 'fixed',
|
|
124
|
+
inset: 0,
|
|
125
|
+
background: 'rgba(0, 0, 0, 0.7)',
|
|
126
|
+
zIndex: 900,
|
|
127
|
+
display: 'flex',
|
|
128
|
+
alignItems: 'center',
|
|
129
|
+
justifyContent: 'center',
|
|
130
|
+
backdropFilter: 'blur(4px)',
|
|
131
|
+
padding: 20
|
|
132
|
+
}}>
|
|
133
|
+
<div className="card" style={{
|
|
134
|
+
maxWidth: 450,
|
|
135
|
+
width: '100%',
|
|
136
|
+
backgroundColor: 'var(--bg-surface)',
|
|
137
|
+
border: '1px solid var(--border)',
|
|
138
|
+
borderRadius: 12,
|
|
139
|
+
padding: 24,
|
|
140
|
+
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.5)'
|
|
141
|
+
}}>
|
|
142
|
+
<h3 style={{ marginBottom: 12, color: 'var(--text-primary)' }}>Confirm Core Self-Update</h3>
|
|
143
|
+
<p style={{ color: 'var(--text-muted)', marginBottom: 20, fontSize: 14, lineHeight: 1.5 }}>
|
|
144
|
+
This will pull the latest version of Total Recall ({updateInfo?.latestVersion}) from the git repository, reinstall dependencies, rebuild frontend bundles, and reboot the system kernel.
|
|
145
|
+
</p>
|
|
146
|
+
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
|
|
147
|
+
<button
|
|
148
|
+
className="btn btn-secondary"
|
|
149
|
+
onClick={() => setShowConfirmModal(false)}
|
|
150
|
+
style={{ padding: '8px 16px', borderRadius: 8 }}
|
|
151
|
+
>
|
|
152
|
+
Cancel
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
className="btn btn-primary"
|
|
156
|
+
onClick={handleStartUpdate}
|
|
157
|
+
style={{ padding: '8px 16px', borderRadius: 8, background: 'var(--accent)' }}
|
|
158
|
+
>
|
|
159
|
+
Update Now
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
167
|
+
<div>
|
|
168
|
+
<h1>System Health</h1>
|
|
169
|
+
<p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
|
|
170
|
+
</div>
|
|
171
|
+
{updateInfo?.updateAvailable && (
|
|
172
|
+
<div className="badge badge-success" style={{
|
|
173
|
+
display: 'flex',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
gap: 12,
|
|
176
|
+
padding: '12px 18px',
|
|
177
|
+
borderRadius: 12,
|
|
178
|
+
background: 'rgba(63, 185, 80, 0.1)',
|
|
179
|
+
border: '1px solid rgba(63, 185, 80, 0.3)',
|
|
180
|
+
animation: 'pulse 2s infinite'
|
|
181
|
+
}}>
|
|
182
|
+
<span style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 600 }}>
|
|
183
|
+
New version available: {updateInfo.latestVersion}
|
|
184
|
+
</span>
|
|
185
|
+
<button
|
|
186
|
+
className="btn btn-primary"
|
|
187
|
+
onClick={() => setShowConfirmModal(true)}
|
|
188
|
+
style={{
|
|
189
|
+
fontSize: 12,
|
|
190
|
+
padding: '6px 12px',
|
|
191
|
+
borderRadius: 6,
|
|
192
|
+
backgroundColor: '#3fb950',
|
|
193
|
+
color: '#fff',
|
|
194
|
+
border: 'none',
|
|
195
|
+
cursor: 'pointer',
|
|
196
|
+
fontWeight: 600
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
Update Now
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
44
203
|
</div>
|
|
45
204
|
|
|
46
205
|
{error && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ {error}</div>}
|
|
206
|
+
{updateError && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ Update failed: {updateError}</div>}
|
|
47
207
|
|
|
48
208
|
{health ? (
|
|
49
209
|
<>
|
|
@@ -55,7 +215,9 @@ export default function HealthPage() {
|
|
|
55
215
|
<div className="label">Status</div>
|
|
56
216
|
</div>
|
|
57
217
|
<div className="stat-card">
|
|
58
|
-
<div className="value" style={{ color: 'var(--accent)' }}>
|
|
218
|
+
<div className="value" style={{ color: 'var(--accent)' }}>
|
|
219
|
+
{health.version} {updateInfo && !updateInfo.updateAvailable && <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>(Latest)</span>}
|
|
220
|
+
</div>
|
|
59
221
|
<div className="label">Version</div>
|
|
60
222
|
</div>
|
|
61
223
|
<div className="stat-card">
|
|
@@ -76,6 +238,10 @@ export default function HealthPage() {
|
|
|
76
238
|
<tr><th>Version</th><td>{health.version}</td></tr>
|
|
77
239
|
<tr><th>Uptime</th><td>{formatUptime(health.uptime_seconds)}</td></tr>
|
|
78
240
|
<tr><th>Timestamp</th><td>{health.timestamp}</td></tr>
|
|
241
|
+
<tr><th>Daemon Status</th><td>{health.daemon || 'unknown'}</td></tr>
|
|
242
|
+
{health.cli_agents && (
|
|
243
|
+
<tr><th>CLI Agents</th><td>{health.cli_agents.join(', ')}</td></tr>
|
|
244
|
+
)}
|
|
79
245
|
<tr><th>Polling</th><td>Every 5 seconds</td></tr>
|
|
80
246
|
</tbody>
|
|
81
247
|
</table>
|
|
@@ -86,6 +252,17 @@ export default function HealthPage() {
|
|
|
86
252
|
{[1, 2, 3, 4].map(i => <div key={i} className="skeleton" style={{ height: 100 }} />)}
|
|
87
253
|
</div>
|
|
88
254
|
) : null}
|
|
255
|
+
|
|
256
|
+
<style>{`
|
|
257
|
+
@keyframes spin {
|
|
258
|
+
0% { transform: rotate(0deg); }
|
|
259
|
+
100% { transform: rotate(360deg); }
|
|
260
|
+
}
|
|
261
|
+
@keyframes fadeIn {
|
|
262
|
+
from { opacity: 0; }
|
|
263
|
+
to { opacity: 1; }
|
|
264
|
+
}
|
|
265
|
+
`}</style>
|
|
89
266
|
</div>
|
|
90
267
|
)
|
|
91
268
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { fetchHelpTopics, fetchHelpContent } from '../api'
|
|
3
|
+
import type { HelpTopic } from '../api'
|
|
4
|
+
|
|
5
|
+
function renderMarkdown(md: string) {
|
|
6
|
+
if (!md) return ''
|
|
7
|
+
let html = md
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
|
|
12
|
+
// Headings
|
|
13
|
+
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
|
14
|
+
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
15
|
+
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
16
|
+
|
|
17
|
+
// Bold & Italic
|
|
18
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
19
|
+
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
20
|
+
|
|
21
|
+
// Code blocks
|
|
22
|
+
html = html.replace(
|
|
23
|
+
/```([\s\S]*?)```/g,
|
|
24
|
+
'<pre style="background:rgba(0,0,0,0.3);padding:12px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);font-family:monospace;overflow-x:auto;margin:12px 0;"><code>$1</code></pre>'
|
|
25
|
+
)
|
|
26
|
+
html = html.replace(
|
|
27
|
+
/`(.*?)`/g,
|
|
28
|
+
'<code style="background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px;font-family:monospace;">$1</code>'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Blockquotes
|
|
32
|
+
html = html.replace(
|
|
33
|
+
/^> (.*$)/gim,
|
|
34
|
+
'<blockquote style="border-left:4px solid var(--purple-primary);padding-left:14px;color:var(--text-secondary);margin:16px 0;font-style:italic;">$1</blockquote>'
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// Lists
|
|
38
|
+
html = html.replace(/^- (.*$)/gim, '<li style="margin-left:20px;list-style-type:disc;">$1</li>')
|
|
39
|
+
|
|
40
|
+
// Linebreaks
|
|
41
|
+
html = html.replace(/\n/g, '<br />')
|
|
42
|
+
|
|
43
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} className="markdown-preview-block" />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function HelpPage() {
|
|
47
|
+
const [topics, setTopics] = useState<HelpTopic[]>([])
|
|
48
|
+
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
|
|
49
|
+
const [content, setContent] = useState<string>('')
|
|
50
|
+
const [loading, setLoading] = useState<boolean>(false)
|
|
51
|
+
const [error, setError] = useState<string>('')
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
async function loadTopics() {
|
|
55
|
+
try {
|
|
56
|
+
const data = await fetchHelpTopics()
|
|
57
|
+
setTopics(data.topics)
|
|
58
|
+
if (data.topics.length > 0) {
|
|
59
|
+
setSelectedTopic(data.topics[0].id)
|
|
60
|
+
}
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
setError((err as Error).message || 'Failed to load help topics')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
loadTopics()
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!selectedTopic) return
|
|
70
|
+
async function loadContent() {
|
|
71
|
+
setLoading(true)
|
|
72
|
+
try {
|
|
73
|
+
const data = await fetchHelpContent(selectedTopic!)
|
|
74
|
+
setContent(data.content)
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
setError((err as Error).message || 'Failed to load help details')
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
loadContent()
|
|
82
|
+
}, [selectedTopic])
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="page" style={{ padding: '24px', maxWidth: '1100px', margin: '0 auto' }}>
|
|
86
|
+
<div className="page-header" style={{ marginBottom: '24px' }}>
|
|
87
|
+
<h1>Sovereign Help System</h1>
|
|
88
|
+
<p>Offline interactive documentation, CLI command references, and SSSS VFS specifications.</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{error && <div className="badge badge-error" style={{ marginBottom: '16px' }}>⚠️ {error}</div>}
|
|
92
|
+
|
|
93
|
+
<div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', gap: '30px', minHeight: '500px' }}>
|
|
94
|
+
{/* Sidebar */}
|
|
95
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
96
|
+
{topics.map((t) => (
|
|
97
|
+
<button
|
|
98
|
+
key={t.id}
|
|
99
|
+
onClick={() => setSelectedTopic(t.id)}
|
|
100
|
+
style={{
|
|
101
|
+
textAlign: 'left',
|
|
102
|
+
padding: '12px 16px',
|
|
103
|
+
borderRadius: '8px',
|
|
104
|
+
background: selectedTopic === t.id ? 'var(--purple-primary)' : 'rgba(255, 255, 255, 0.03)',
|
|
105
|
+
color: '#fff',
|
|
106
|
+
border: selectedTopic === t.id ? '1px solid var(--purple-border)' : '1px solid transparent',
|
|
107
|
+
cursor: 'pointer',
|
|
108
|
+
transition: 'all 0.2s',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>{t.title}</div>
|
|
112
|
+
<div style={{ fontSize: '11px', color: selectedTopic === t.id ? 'rgba(255, 255, 255, 0.8)' : 'var(--text-secondary)', marginTop: '4px' }}>
|
|
113
|
+
{t.description}
|
|
114
|
+
</div>
|
|
115
|
+
</button>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Content Pane */}
|
|
120
|
+
<div
|
|
121
|
+
style={{
|
|
122
|
+
background: 'var(--surface-bg)',
|
|
123
|
+
border: '1px solid var(--border)',
|
|
124
|
+
borderRadius: '12px',
|
|
125
|
+
padding: '30px',
|
|
126
|
+
overflowY: 'auto',
|
|
127
|
+
maxHeight: '75vh',
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{loading ? (
|
|
131
|
+
<div className="skeleton" style={{ height: '400px' }} />
|
|
132
|
+
) : (
|
|
133
|
+
<div className="markdown-body" style={{ color: 'var(--text-primary)', lineHeight: '1.7', fontSize: '14px' }}>
|
|
134
|
+
{renderMarkdown(content)}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
@@ -35,6 +35,7 @@ export default function SettingsPage() {
|
|
|
35
35
|
if (!data.budget.budget) data.budget.budget = { daily_cap_usd: 5.0, weekly_cap_usd: 25.0, enabled: true };
|
|
36
36
|
if (!data.brain) data.brain = {};
|
|
37
37
|
if (!data.brain.preferred_agent) data.brain.preferred_agent = 'auto';
|
|
38
|
+
if (!data.secrets) data.secrets = {};
|
|
38
39
|
|
|
39
40
|
setConfigData(data);
|
|
40
41
|
} catch (err: unknown) {
|
|
@@ -152,6 +153,17 @@ export default function SettingsPage() {
|
|
|
152
153
|
});
|
|
153
154
|
};
|
|
154
155
|
|
|
156
|
+
const updateSecretsProp = (prop: string, value: string) => {
|
|
157
|
+
if (!configData) return;
|
|
158
|
+
setConfigData({
|
|
159
|
+
...configData,
|
|
160
|
+
secrets: {
|
|
161
|
+
...configData.secrets,
|
|
162
|
+
[prop]: value
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
155
167
|
return (
|
|
156
168
|
<div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
157
169
|
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
|
@@ -209,7 +221,7 @@ export default function SettingsPage() {
|
|
|
209
221
|
</div>
|
|
210
222
|
) : viewMode === 'visual' && configData ? (
|
|
211
223
|
/* ==================== VISUAL CONTROL PANEL UI ==================== */
|
|
212
|
-
<
|
|
224
|
+
<form onSubmit={(e) => { e.preventDefault(); handleSaveVisual(); }} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24, flex: 1, minHeight: 0, overflowY: 'auto', paddingBottom: 24 }}>
|
|
213
225
|
|
|
214
226
|
{/* Card 1: AI Reasoning & Autonomy */}
|
|
215
227
|
<div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
|
|
@@ -537,7 +549,190 @@ export default function SettingsPage() {
|
|
|
537
549
|
</div>
|
|
538
550
|
</div>
|
|
539
551
|
|
|
540
|
-
|
|
552
|
+
{/* Card 5: API Keys & Integrations */}
|
|
553
|
+
<div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
|
|
554
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
|
|
555
|
+
<span style={{ fontSize: 20 }}>🔑</span>
|
|
556
|
+
<div>
|
|
557
|
+
<h3 style={{ fontSize: 15, fontWeight: 600 }}>API Keys & Integrations</h3>
|
|
558
|
+
<p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Manage access keys and tokens for AI models and search providers</p>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
{/* Google Gemini API Key */}
|
|
563
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
564
|
+
<label htmlFor="google_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Google Gemini API Key</label>
|
|
565
|
+
<input
|
|
566
|
+
id="google_api_key"
|
|
567
|
+
type="password"
|
|
568
|
+
placeholder="AIzaSy..."
|
|
569
|
+
value={configData.secrets?.google_api_key || ''}
|
|
570
|
+
onChange={(e) => updateSecretsProp('google_api_key', e.target.value)}
|
|
571
|
+
style={{
|
|
572
|
+
background: 'var(--bg-tertiary)',
|
|
573
|
+
color: 'var(--text-primary)',
|
|
574
|
+
border: '1px solid var(--border)',
|
|
575
|
+
padding: '8px 12px',
|
|
576
|
+
borderRadius: 6,
|
|
577
|
+
outline: 'none',
|
|
578
|
+
fontSize: 13
|
|
579
|
+
}}
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{/* Anthropic API Key */}
|
|
584
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
585
|
+
<label htmlFor="anthropic_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Anthropic API Key (Claude Code)</label>
|
|
586
|
+
<input
|
|
587
|
+
id="anthropic_api_key"
|
|
588
|
+
type="password"
|
|
589
|
+
placeholder="sk-ant-..."
|
|
590
|
+
value={configData.secrets?.anthropic_api_key || ''}
|
|
591
|
+
onChange={(e) => updateSecretsProp('anthropic_api_key', e.target.value)}
|
|
592
|
+
style={{
|
|
593
|
+
background: 'var(--bg-tertiary)',
|
|
594
|
+
color: 'var(--text-primary)',
|
|
595
|
+
border: '1px solid var(--border)',
|
|
596
|
+
padding: '8px 12px',
|
|
597
|
+
borderRadius: 6,
|
|
598
|
+
outline: 'none',
|
|
599
|
+
fontSize: 13
|
|
600
|
+
}}
|
|
601
|
+
/>
|
|
602
|
+
<span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Configuring this allows Claude Code CLI to run without prompting for OAuth login.</span>
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
{/* OpenAI API Key */}
|
|
606
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
607
|
+
<label htmlFor="openai_api_key" style={{ fontSize: 13, fontWeight: 500 }}>OpenAI API Key (Codex)</label>
|
|
608
|
+
<input
|
|
609
|
+
id="openai_api_key"
|
|
610
|
+
type="password"
|
|
611
|
+
placeholder="sk-proj-..."
|
|
612
|
+
value={configData.secrets?.openai_api_key || ''}
|
|
613
|
+
onChange={(e) => updateSecretsProp('openai_api_key', e.target.value)}
|
|
614
|
+
style={{
|
|
615
|
+
background: 'var(--bg-tertiary)',
|
|
616
|
+
color: 'var(--text-primary)',
|
|
617
|
+
border: '1px solid var(--border)',
|
|
618
|
+
padding: '8px 12px',
|
|
619
|
+
borderRadius: 6,
|
|
620
|
+
outline: 'none',
|
|
621
|
+
fontSize: 13
|
|
622
|
+
}}
|
|
623
|
+
/>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{/* Tavily API Key */}
|
|
627
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
628
|
+
<label htmlFor="tavily_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Tavily Search API Key</label>
|
|
629
|
+
<input
|
|
630
|
+
id="tavily_api_key"
|
|
631
|
+
type="password"
|
|
632
|
+
placeholder="tvly-..."
|
|
633
|
+
value={configData.secrets?.tavily_api_key || ''}
|
|
634
|
+
onChange={(e) => updateSecretsProp('tavily_api_key', e.target.value)}
|
|
635
|
+
style={{
|
|
636
|
+
background: 'var(--bg-tertiary)',
|
|
637
|
+
color: 'var(--text-primary)',
|
|
638
|
+
border: '1px solid var(--border)',
|
|
639
|
+
padding: '8px 12px',
|
|
640
|
+
borderRadius: 6,
|
|
641
|
+
outline: 'none',
|
|
642
|
+
fontSize: 13
|
|
643
|
+
}}
|
|
644
|
+
/>
|
|
645
|
+
</div>
|
|
646
|
+
|
|
647
|
+
{/* Other Search Keys (Brave, Exa, Serper) */}
|
|
648
|
+
<details style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
|
649
|
+
<summary style={{ cursor: 'pointer', fontWeight: 500, outline: 'none', padding: '4px 0' }}>More Provider Keys (Brave, Exa, Serper, GitHub)</summary>
|
|
650
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12, paddingLeft: 8, borderLeft: '2px solid var(--border)' }}>
|
|
651
|
+
{/* Brave Search Key */}
|
|
652
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
653
|
+
<label htmlFor="brave_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Brave Search API Key</label>
|
|
654
|
+
<input
|
|
655
|
+
id="brave_api_key"
|
|
656
|
+
type="password"
|
|
657
|
+
value={configData.secrets?.brave_api_key || ''}
|
|
658
|
+
onChange={(e) => updateSecretsProp('brave_api_key', e.target.value)}
|
|
659
|
+
style={{
|
|
660
|
+
background: 'var(--bg-tertiary)',
|
|
661
|
+
color: 'var(--text-primary)',
|
|
662
|
+
border: '1px solid var(--border)',
|
|
663
|
+
padding: '8px 12px',
|
|
664
|
+
borderRadius: 6,
|
|
665
|
+
outline: 'none',
|
|
666
|
+
fontSize: 13
|
|
667
|
+
}}
|
|
668
|
+
/>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
671
|
+
{/* Exa Search Key */}
|
|
672
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
673
|
+
<label htmlFor="exa_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Exa API Key</label>
|
|
674
|
+
<input
|
|
675
|
+
id="exa_api_key"
|
|
676
|
+
type="password"
|
|
677
|
+
value={configData.secrets?.exa_api_key || ''}
|
|
678
|
+
onChange={(e) => updateSecretsProp('exa_api_key', e.target.value)}
|
|
679
|
+
style={{
|
|
680
|
+
background: 'var(--bg-tertiary)',
|
|
681
|
+
color: 'var(--text-primary)',
|
|
682
|
+
border: '1px solid var(--border)',
|
|
683
|
+
padding: '8px 12px',
|
|
684
|
+
borderRadius: 6,
|
|
685
|
+
outline: 'none',
|
|
686
|
+
fontSize: 13
|
|
687
|
+
}}
|
|
688
|
+
/>
|
|
689
|
+
</div>
|
|
690
|
+
|
|
691
|
+
{/* Serper API Key */}
|
|
692
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
693
|
+
<label htmlFor="serper_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Serper (Google Search API) Key</label>
|
|
694
|
+
<input
|
|
695
|
+
id="serper_api_key"
|
|
696
|
+
type="password"
|
|
697
|
+
value={configData.secrets?.serper_api_key || ''}
|
|
698
|
+
onChange={(e) => updateSecretsProp('serper_api_key', e.target.value)}
|
|
699
|
+
style={{
|
|
700
|
+
background: 'var(--bg-tertiary)',
|
|
701
|
+
color: 'var(--text-primary)',
|
|
702
|
+
border: '1px solid var(--border)',
|
|
703
|
+
padding: '8px 12px',
|
|
704
|
+
borderRadius: 6,
|
|
705
|
+
outline: 'none',
|
|
706
|
+
fontSize: 13
|
|
707
|
+
}}
|
|
708
|
+
/>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
{/* GitHub Token */}
|
|
712
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
713
|
+
<label htmlFor="github_token" style={{ fontSize: 12, fontWeight: 500 }}>GitHub Personal Access Token</label>
|
|
714
|
+
<input
|
|
715
|
+
id="github_token"
|
|
716
|
+
type="password"
|
|
717
|
+
placeholder="ghp_..."
|
|
718
|
+
value={configData.secrets?.github_token || ''}
|
|
719
|
+
onChange={(e) => updateSecretsProp('github_token', e.target.value)}
|
|
720
|
+
style={{
|
|
721
|
+
background: 'var(--bg-tertiary)',
|
|
722
|
+
color: 'var(--text-primary)',
|
|
723
|
+
border: '1px solid var(--border)',
|
|
724
|
+
padding: '8px 12px',
|
|
725
|
+
borderRadius: 6,
|
|
726
|
+
outline: 'none',
|
|
727
|
+
fontSize: 13
|
|
728
|
+
}}
|
|
729
|
+
/>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
</details>
|
|
733
|
+
</div>
|
|
734
|
+
|
|
735
|
+
</form>
|
|
541
736
|
) : (
|
|
542
737
|
/* ==================== RAW YAML EDITOR UI ==================== */
|
|
543
738
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
|