total-recall-brain 3.1.1 → 3.2.1
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 +806 -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/remember.mjs +1 -1
- package/src/core/research-queue.mjs +5 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"react": "^19.2.6",
|
|
14
|
+
"react-dom": "^19.2.6",
|
|
15
|
+
"react-router-dom": "^7.15.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@eslint/js": "^10.0.1",
|
|
19
|
+
"@types/node": "^24.12.3",
|
|
20
|
+
"@types/react": "^19.2.14",
|
|
21
|
+
"@types/react-dom": "^19.2.3",
|
|
22
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
23
|
+
"eslint": "^10.3.0",
|
|
24
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
25
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
26
|
+
"globals": "^17.6.0",
|
|
27
|
+
"typescript": "~6.0.2",
|
|
28
|
+
"typescript-eslint": "^8.59.2",
|
|
29
|
+
"vite": "^8.0.12"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { BrowserRouter, Routes, Route, NavLink, useLocation } from 'react-router-dom'
|
|
3
|
+
import './App.css'
|
|
4
|
+
import { getApiBase, setApiBase, checkSession, logout, registerUnauthedCallback, fetchHealth } from './api'
|
|
5
|
+
import type { HealthData } from './types'
|
|
6
|
+
import LoginPage from './pages/LoginPage'
|
|
7
|
+
import ChatPage from './pages/ChatPage'
|
|
8
|
+
import MemoryPage from './pages/MemoryPage'
|
|
9
|
+
import HealthPage from './pages/HealthPage'
|
|
10
|
+
import SandboxPage from './pages/SandboxPage'
|
|
11
|
+
import SettingsPage from './pages/SettingsPage'
|
|
12
|
+
import TasksPage from './pages/TasksPage'
|
|
13
|
+
import FilesPage from './pages/FilesPage'
|
|
14
|
+
import ApiKeysPage from './pages/ApiKeysPage'
|
|
15
|
+
import IntegrationsPage from './pages/IntegrationsPage'
|
|
16
|
+
import BrainSelector from './components/BrainSelector'
|
|
17
|
+
import AutomationsPage from './pages/AutomationsPage'
|
|
18
|
+
import DeploymentsPage from './pages/DeploymentsPage'
|
|
19
|
+
import SkillsPage from './pages/SkillsPage'
|
|
20
|
+
import UsagePage from './pages/UsagePage'
|
|
21
|
+
|
|
22
|
+
// ─── Auth state type ──────────────────────────────────────────────────────────
|
|
23
|
+
type AuthState = 'loading' | 'authed' | 'unauthed'
|
|
24
|
+
|
|
25
|
+
// ─── Sidebar ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
interface DotProps {
|
|
28
|
+
ok: boolean
|
|
29
|
+
label: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function StatusDot({ ok, label }: DotProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 5, color: 'var(--text-tertiary)' }}>
|
|
35
|
+
<span style={{
|
|
36
|
+
width: 7, height: 7, borderRadius: '50%',
|
|
37
|
+
background: ok ? '#22c55e' : '#ef4444',
|
|
38
|
+
boxShadow: ok ? '0 0 6px rgba(34,197,94,0.5)' : '0 0 6px rgba(239,68,68,0.5)',
|
|
39
|
+
display: 'inline-block',
|
|
40
|
+
animation: ok ? undefined : 'blink 1s ease-in-out infinite',
|
|
41
|
+
}} />
|
|
42
|
+
{label}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface SidebarProps {
|
|
48
|
+
onLogout: () => void
|
|
49
|
+
health: HealthData | null
|
|
50
|
+
activeBrainId: string
|
|
51
|
+
onBrainChange: (id: string) => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProps) {
|
|
55
|
+
return (
|
|
56
|
+
<aside className="sidebar">
|
|
57
|
+
<div className="sidebar-brand">
|
|
58
|
+
<h1>Total Recall</h1>
|
|
59
|
+
<span>Sovereign Brain v3.0</span>
|
|
60
|
+
</div>
|
|
61
|
+
<nav className="sidebar-nav">
|
|
62
|
+
<NavLink to="/" end className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-chat">
|
|
63
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
64
|
+
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
|
65
|
+
</svg>
|
|
66
|
+
Chat
|
|
67
|
+
</NavLink>
|
|
68
|
+
<NavLink to="/memory" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-memory">
|
|
69
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
70
|
+
<circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" />
|
|
71
|
+
</svg>
|
|
72
|
+
Memory
|
|
73
|
+
</NavLink>
|
|
74
|
+
<NavLink to="/tasks" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-tasks">
|
|
75
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
76
|
+
<path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
|
|
77
|
+
</svg>
|
|
78
|
+
Tasks
|
|
79
|
+
</NavLink>
|
|
80
|
+
<NavLink to="/automations" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-automations">
|
|
81
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
82
|
+
<circle cx="12" cy="12" r="10" />
|
|
83
|
+
<polyline points="12 6 12 12 16 14" />
|
|
84
|
+
</svg>
|
|
85
|
+
Automations
|
|
86
|
+
</NavLink>
|
|
87
|
+
<NavLink to="/files" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-files">
|
|
88
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
89
|
+
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
90
|
+
</svg>
|
|
91
|
+
Files
|
|
92
|
+
</NavLink>
|
|
93
|
+
<NavLink to="/sandbox" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-sandbox">
|
|
94
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
95
|
+
<polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" />
|
|
96
|
+
</svg>
|
|
97
|
+
Sandbox
|
|
98
|
+
</NavLink>
|
|
99
|
+
<NavLink to="/deployments" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-deployments">
|
|
100
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
101
|
+
<rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
|
|
102
|
+
<rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
|
|
103
|
+
<line x1="6" y1="6" x2="6.01" y2="6" />
|
|
104
|
+
<line x1="6" y1="18" x2="6.01" y2="18" />
|
|
105
|
+
</svg>
|
|
106
|
+
Deployments
|
|
107
|
+
</NavLink>
|
|
108
|
+
<NavLink to="/health" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-health">
|
|
109
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
110
|
+
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
111
|
+
</svg>
|
|
112
|
+
Health
|
|
113
|
+
</NavLink>
|
|
114
|
+
<NavLink to="/usage" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-usage">
|
|
115
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
116
|
+
<line x1="12" y1="1" x2="12" y2="23"></line>
|
|
117
|
+
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
|
118
|
+
</svg>
|
|
119
|
+
Usage & Costs
|
|
120
|
+
</NavLink>
|
|
121
|
+
<NavLink to="/settings" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-settings">
|
|
122
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
123
|
+
<circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
|
|
124
|
+
</svg>
|
|
125
|
+
Settings
|
|
126
|
+
</NavLink>
|
|
127
|
+
<NavLink to="/keys" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-keys">
|
|
128
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
129
|
+
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
|
130
|
+
</svg>
|
|
131
|
+
API Keys
|
|
132
|
+
</NavLink>
|
|
133
|
+
<NavLink to="/integrations" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-integrations">
|
|
134
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
135
|
+
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
|
|
136
|
+
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
|
|
137
|
+
</svg>
|
|
138
|
+
Integrations
|
|
139
|
+
</NavLink>
|
|
140
|
+
<NavLink to="/skills" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-skills">
|
|
141
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
142
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
143
|
+
</svg>
|
|
144
|
+
Skills Manager
|
|
145
|
+
</NavLink>
|
|
146
|
+
</nav>
|
|
147
|
+
<div className="sidebar-footer">
|
|
148
|
+
<div style={{ marginBottom: 8 }}>
|
|
149
|
+
<BrainSelector activeBrainId={activeBrainId} onBrainChange={onBrainChange} />
|
|
150
|
+
</div>
|
|
151
|
+
<div style={{ marginBottom: 12 }}>
|
|
152
|
+
<select
|
|
153
|
+
value={getApiBase()}
|
|
154
|
+
onChange={e => setApiBase(e.target.value)}
|
|
155
|
+
style={{
|
|
156
|
+
width: '100%',
|
|
157
|
+
background: 'var(--bg-secondary)',
|
|
158
|
+
color: 'var(--text-secondary)',
|
|
159
|
+
border: '1px solid var(--border)',
|
|
160
|
+
borderRadius: 6,
|
|
161
|
+
padding: '4px 8px',
|
|
162
|
+
fontSize: 12,
|
|
163
|
+
outline: 'none'
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<option value="">Localhost (Proxy)</option>
|
|
167
|
+
{import.meta.env.VITE_BRAIN_URL && (
|
|
168
|
+
<option value={import.meta.env.VITE_BRAIN_URL}>{new URL(import.meta.env.VITE_BRAIN_URL).host}</option>
|
|
169
|
+
)}
|
|
170
|
+
</select>
|
|
171
|
+
</div>
|
|
172
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
173
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11 }}>
|
|
174
|
+
{(() => {
|
|
175
|
+
const daemonOk = health?.daemon === 'running'
|
|
176
|
+
const serverOk = health && health.status !== 'unreachable'
|
|
177
|
+
const allGood = daemonOk && serverOk
|
|
178
|
+
|
|
179
|
+
if (!health) return <div style={{ color: 'var(--text-tertiary)' }}>Checking…</div>
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<style>{`@keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0.3;} }`}</style>
|
|
184
|
+
{allGood ? (
|
|
185
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 5, color: 'var(--text-tertiary)' }}>
|
|
186
|
+
<span className="pulse" /> All Systems Go
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<>
|
|
190
|
+
<StatusDot ok={!!serverOk} label={serverOk ? 'Server online' : 'Server offline'} />
|
|
191
|
+
<StatusDot ok={daemonOk} label={daemonOk ? 'Daemon' : 'Daemon ' + (health?.daemon ?? 'unknown')} />
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</>
|
|
195
|
+
)
|
|
196
|
+
})()}
|
|
197
|
+
</div>
|
|
198
|
+
{/* Logout button */}
|
|
199
|
+
<button
|
|
200
|
+
id="btn-logout"
|
|
201
|
+
onClick={onLogout}
|
|
202
|
+
title="Sign out"
|
|
203
|
+
style={{
|
|
204
|
+
background: 'none',
|
|
205
|
+
border: '1px solid var(--border)',
|
|
206
|
+
borderRadius: 6,
|
|
207
|
+
cursor: 'pointer',
|
|
208
|
+
color: 'var(--text-tertiary)',
|
|
209
|
+
display: 'flex',
|
|
210
|
+
alignItems: 'center',
|
|
211
|
+
gap: 5,
|
|
212
|
+
padding: '4px 8px',
|
|
213
|
+
fontSize: 11,
|
|
214
|
+
transition: 'color 0.15s, border-color 0.15s',
|
|
215
|
+
}}
|
|
216
|
+
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = '#ef4444'; (e.currentTarget as HTMLElement).style.borderColor = '#ef4444' }}
|
|
217
|
+
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'var(--text-tertiary)'; (e.currentTarget as HTMLElement).style.borderColor = 'var(--border)' }}
|
|
218
|
+
>
|
|
219
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
220
|
+
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
|
|
221
|
+
<polyline points="16 17 21 12 16 7"/>
|
|
222
|
+
<line x1="21" y1="12" x2="9" y2="12"/>
|
|
223
|
+
</svg>
|
|
224
|
+
Sign out
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</aside>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Main content (routes) ────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
function MainContent({ activeBrainId, onBrainChange }: { activeBrainId: string; onBrainChange: (id: string) => void }) {
|
|
235
|
+
const location = useLocation();
|
|
236
|
+
const isChat = location.pathname === '/';
|
|
237
|
+
const [floatingChat, setFloatingChat] = useState(false);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const handleToggle = () => setFloatingChat(v => !v);
|
|
241
|
+
window.addEventListener('toggle-floating-chat', handleToggle);
|
|
242
|
+
return () => window.removeEventListener('toggle-floating-chat', handleToggle);
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
const showChat = isChat || floatingChat;
|
|
246
|
+
|
|
247
|
+
const chatStyle: React.CSSProperties = floatingChat ? {
|
|
248
|
+
display: 'flex',
|
|
249
|
+
position: 'fixed',
|
|
250
|
+
bottom: 20,
|
|
251
|
+
right: 20,
|
|
252
|
+
width: 400,
|
|
253
|
+
height: 600,
|
|
254
|
+
zIndex: 9999,
|
|
255
|
+
backgroundColor: 'var(--bg-primary)',
|
|
256
|
+
border: '1px solid var(--border)',
|
|
257
|
+
borderRadius: 12,
|
|
258
|
+
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
|
|
259
|
+
overflow: 'hidden',
|
|
260
|
+
flexDirection: 'column'
|
|
261
|
+
} : {
|
|
262
|
+
display: showChat ? 'flex' : 'none',
|
|
263
|
+
flex: 1,
|
|
264
|
+
flexDirection: 'column',
|
|
265
|
+
height: '100%',
|
|
266
|
+
overflow: 'hidden'
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<main className="main-content" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', position: 'relative' }}>
|
|
271
|
+
<div style={chatStyle}>
|
|
272
|
+
{floatingChat && (
|
|
273
|
+
<div style={{ padding: '8px', background: 'var(--bg-secondary)', borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
274
|
+
<span style={{ fontSize: 12, fontWeight: 600 }}>Total Recall Agent</span>
|
|
275
|
+
<button onClick={() => setFloatingChat(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-tertiary)' }}>✖</button>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
<ChatPage activeBrainId={activeBrainId} onBrainChange={onBrainChange} />
|
|
279
|
+
</div>
|
|
280
|
+
{(!isChat || floatingChat) && (
|
|
281
|
+
<div style={{ flex: 1, overflow: 'auto' }}>
|
|
282
|
+
<Routes>
|
|
283
|
+
<Route path="/" element={<div/>} />
|
|
284
|
+
<Route path="/memory" element={<MemoryPage activeBrainId={activeBrainId} />} />
|
|
285
|
+
<Route path="/tasks" element={<TasksPage />} />
|
|
286
|
+
<Route path="/automations" element={<AutomationsPage />} />
|
|
287
|
+
<Route path="/files" element={<FilesPage />} />
|
|
288
|
+
<Route path="/sandbox" element={<SandboxPage />} />
|
|
289
|
+
<Route path="/deployments" element={<DeploymentsPage />} />
|
|
290
|
+
<Route path="/health" element={<HealthPage />} />
|
|
291
|
+
<Route path="/usage" element={<UsagePage />} />
|
|
292
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
293
|
+
<Route path="/keys" element={<ApiKeysPage />} />
|
|
294
|
+
<Route path="/integrations" element={<IntegrationsPage />} />
|
|
295
|
+
<Route path="/skills" element={<SkillsPage />} />
|
|
296
|
+
</Routes>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</main>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
// ─── Root App with auth gate ──────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
const BRAIN_STORAGE_KEY = 'total-recall-active-brain'
|
|
307
|
+
|
|
308
|
+
function App() {
|
|
309
|
+
const [authState, setAuthState] = useState<AuthState>('loading')
|
|
310
|
+
const [health, setHealth] = useState<HealthData | null>(null)
|
|
311
|
+
const [activeBrainId, setActiveBrainId] = useState(
|
|
312
|
+
() => localStorage.getItem(BRAIN_STORAGE_KEY) || 'global'
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
// Persist activeBrainId to localStorage on change
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
localStorage.setItem(BRAIN_STORAGE_KEY, activeBrainId)
|
|
318
|
+
}, [activeBrainId])
|
|
319
|
+
|
|
320
|
+
// Register 401 interceptor so any API call that gets a 401 flips us to unauthed
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
registerUnauthedCallback(() => setAuthState('unauthed'))
|
|
323
|
+
return () => {}
|
|
324
|
+
}, [])
|
|
325
|
+
|
|
326
|
+
// Session check on mount — probe /auth/me before rendering anything
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
checkSession().then(ok => setAuthState(ok ? 'authed' : 'unauthed'))
|
|
329
|
+
}, [])
|
|
330
|
+
|
|
331
|
+
// Health polling — shared by EmergencyAlertBanner and Sidebar
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
let active = true
|
|
334
|
+
const poll = async () => {
|
|
335
|
+
try {
|
|
336
|
+
const data = await fetchHealth()
|
|
337
|
+
if (active) setHealth(data)
|
|
338
|
+
} catch {
|
|
339
|
+
if (active) setHealth({ status: 'unreachable', version: '', uptime_seconds: 0, timestamp: '' })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
poll()
|
|
343
|
+
const interval = setInterval(poll, 10000)
|
|
344
|
+
return () => { active = false; clearInterval(interval) }
|
|
345
|
+
}, [])
|
|
346
|
+
|
|
347
|
+
async function handleLogout() {
|
|
348
|
+
await logout()
|
|
349
|
+
setAuthState('unauthed')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function handleAuthenticated() {
|
|
353
|
+
setAuthState('authed')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Loading state — blank dark screen, no flash of unprotected content
|
|
357
|
+
if (authState === 'loading') {
|
|
358
|
+
return (
|
|
359
|
+
<div style={{
|
|
360
|
+
minHeight: '100vh',
|
|
361
|
+
display: 'flex',
|
|
362
|
+
alignItems: 'center',
|
|
363
|
+
justifyContent: 'center',
|
|
364
|
+
background: 'var(--bg-primary)',
|
|
365
|
+
}}>
|
|
366
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2.5" strokeLinecap="round" style={{ animation: 'spin 0.8s linear infinite' }}>
|
|
367
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
|
368
|
+
</svg>
|
|
369
|
+
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
370
|
+
</div>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Unauthed — show login screen only
|
|
375
|
+
if (authState === 'unauthed') {
|
|
376
|
+
return <LoginPage onAuthenticated={handleAuthenticated} />
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Authed — render full dashboard
|
|
380
|
+
return (
|
|
381
|
+
<BrowserRouter>
|
|
382
|
+
<div className="app-layout">
|
|
383
|
+
<Sidebar onLogout={handleLogout} health={health} activeBrainId={activeBrainId} onBrainChange={setActiveBrainId} />
|
|
384
|
+
<MainContent activeBrainId={activeBrainId} onBrainChange={setActiveBrainId} />
|
|
385
|
+
</div>
|
|
386
|
+
</BrowserRouter>
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export default App
|