@auxiora/dashboard 1.0.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/LICENSE +191 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +69 -0
- package/dist/auth.js.map +1 -0
- package/dist/cloud-types.d.ts +71 -0
- package/dist/cloud-types.d.ts.map +1 -0
- package/dist/cloud-types.js +2 -0
- package/dist/cloud-types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +2250 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist-ui/assets/index-BfY0i5jw.css +1 -0
- package/dist-ui/assets/index-CXpk9mvw.js +60 -0
- package/dist-ui/icon.svg +59 -0
- package/dist-ui/index.html +20 -0
- package/package.json +32 -0
- package/src/auth.ts +83 -0
- package/src/cloud-types.ts +63 -0
- package/src/index.ts +5 -0
- package/src/router.ts +2494 -0
- package/src/types.ts +269 -0
- package/tests/auth.test.ts +51 -0
- package/tests/cloud-router.test.ts +249 -0
- package/tests/desktop-router.test.ts +151 -0
- package/tests/router.test.ts +388 -0
- package/tests/trust-router.test.ts +170 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/ui/index.html +19 -0
- package/ui/node_modules/.bin/browserslist +17 -0
- package/ui/node_modules/.bin/tsc +17 -0
- package/ui/node_modules/.bin/tsserver +17 -0
- package/ui/node_modules/.bin/vite +17 -0
- package/ui/package.json +23 -0
- package/ui/public/icon.svg +59 -0
- package/ui/src/App.tsx +63 -0
- package/ui/src/api.ts +238 -0
- package/ui/src/components/ActivityFeed.tsx +123 -0
- package/ui/src/components/BehaviorHealth.tsx +105 -0
- package/ui/src/components/DataTable.tsx +39 -0
- package/ui/src/components/Layout.tsx +160 -0
- package/ui/src/components/PasswordStrength.tsx +31 -0
- package/ui/src/components/SetupProgress.tsx +26 -0
- package/ui/src/components/StatusBadge.tsx +12 -0
- package/ui/src/components/ThemeSelector.tsx +39 -0
- package/ui/src/contexts/ThemeContext.tsx +58 -0
- package/ui/src/hooks/useApi.ts +19 -0
- package/ui/src/hooks/usePolling.ts +8 -0
- package/ui/src/main.tsx +16 -0
- package/ui/src/pages/AuditLog.tsx +36 -0
- package/ui/src/pages/Behaviors.tsx +426 -0
- package/ui/src/pages/Chat.tsx +688 -0
- package/ui/src/pages/Login.tsx +64 -0
- package/ui/src/pages/Overview.tsx +56 -0
- package/ui/src/pages/Sessions.tsx +26 -0
- package/ui/src/pages/SettingsAmbient.tsx +185 -0
- package/ui/src/pages/SettingsConnections.tsx +201 -0
- package/ui/src/pages/SettingsNotifications.tsx +241 -0
- package/ui/src/pages/SetupAppearance.tsx +45 -0
- package/ui/src/pages/SetupChannels.tsx +143 -0
- package/ui/src/pages/SetupComplete.tsx +31 -0
- package/ui/src/pages/SetupConnections.tsx +80 -0
- package/ui/src/pages/SetupDashboardPassword.tsx +50 -0
- package/ui/src/pages/SetupIdentity.tsx +68 -0
- package/ui/src/pages/SetupPersonality.tsx +78 -0
- package/ui/src/pages/SetupProvider.tsx +65 -0
- package/ui/src/pages/SetupVault.tsx +50 -0
- package/ui/src/pages/SetupWelcome.tsx +19 -0
- package/ui/src/pages/UnlockVault.tsx +56 -0
- package/ui/src/pages/Webhooks.tsx +158 -0
- package/ui/src/pages/settings/Appearance.tsx +63 -0
- package/ui/src/pages/settings/Channels.tsx +138 -0
- package/ui/src/pages/settings/Identity.tsx +61 -0
- package/ui/src/pages/settings/Personality.tsx +54 -0
- package/ui/src/pages/settings/PersonalityEditor.tsx +577 -0
- package/ui/src/pages/settings/Provider.tsx +537 -0
- package/ui/src/pages/settings/Security.tsx +111 -0
- package/ui/src/styles/global.css +2308 -0
- package/ui/src/styles/themes/index.css +7 -0
- package/ui/src/styles/themes/monolith.css +125 -0
- package/ui/src/styles/themes/nebula.css +90 -0
- package/ui/src/styles/themes/neon.css +149 -0
- package/ui/src/styles/themes/polar.css +151 -0
- package/ui/src/styles/themes/signal.css +163 -0
- package/ui/src/styles/themes/terra.css +146 -0
- package/ui/tsconfig.json +14 -0
- package/ui/vite.config.ts +20 -0
package/ui/src/api.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const BASE = '/api/v1/dashboard';
|
|
2
|
+
|
|
3
|
+
async function fetchApi<T>(path: string, options?: RequestInit): Promise<T> {
|
|
4
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
5
|
+
credentials: 'include',
|
|
6
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7
|
+
...options,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
if (res.status === 401) {
|
|
11
|
+
window.location.href = '/dashboard/login';
|
|
12
|
+
throw new Error('Unauthorized');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
const body = await res.json().catch(() => ({}));
|
|
17
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return res.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const api = {
|
|
24
|
+
checkAuth: () => fetchApi<{ authenticated: boolean }>('/auth/check'),
|
|
25
|
+
login: (pw: string) =>
|
|
26
|
+
fetchApi<{ success: boolean }>('/auth/login', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body: JSON.stringify({ password: pw }),
|
|
29
|
+
}),
|
|
30
|
+
logout: () => fetchApi<{ success: boolean }>('/auth/logout', { method: 'POST' }),
|
|
31
|
+
getBehaviors: () => fetchApi<{ data: any[] }>('/behaviors'),
|
|
32
|
+
patchBehavior: (id: string, updates: Record<string, unknown>) =>
|
|
33
|
+
fetchApi<{ data: any }>(`/behaviors/${id}`, {
|
|
34
|
+
method: 'PATCH',
|
|
35
|
+
body: JSON.stringify(updates),
|
|
36
|
+
}),
|
|
37
|
+
deleteBehavior: (id: string) =>
|
|
38
|
+
fetchApi<{ data: any }>(`/behaviors/${id}`, { method: 'DELETE' }),
|
|
39
|
+
createBehavior: (input: Record<string, unknown>) =>
|
|
40
|
+
fetchApi<{ data: any }>('/behaviors', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
body: JSON.stringify(input),
|
|
43
|
+
}),
|
|
44
|
+
getWebhooks: () => fetchApi<{ data: any[] }>('/webhooks'),
|
|
45
|
+
patchWebhook: (id: string, updates: Record<string, unknown>) =>
|
|
46
|
+
fetchApi<{ data: any }>(`/webhooks/${id}`, {
|
|
47
|
+
method: 'PATCH',
|
|
48
|
+
body: JSON.stringify(updates),
|
|
49
|
+
}),
|
|
50
|
+
deleteWebhook: (id: string) =>
|
|
51
|
+
fetchApi<{ data: any }>(`/webhooks/${id}`, { method: 'DELETE' }),
|
|
52
|
+
createWebhook: (input: Record<string, unknown>) =>
|
|
53
|
+
fetchApi<{ data: any }>('/webhooks', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
body: JSON.stringify(input),
|
|
56
|
+
}),
|
|
57
|
+
getSessions: () => fetchApi<{ data: any[] }>('/sessions'),
|
|
58
|
+
getAudit: (params?: { type?: string; limit?: number }) => {
|
|
59
|
+
const query = new URLSearchParams();
|
|
60
|
+
if (params?.type) query.set('type', params.type);
|
|
61
|
+
if (params?.limit) query.set('limit', String(params.limit));
|
|
62
|
+
const qs = query.toString();
|
|
63
|
+
return fetchApi<{ data: any[] }>(`/audit${qs ? `?${qs}` : ''}`);
|
|
64
|
+
},
|
|
65
|
+
getStatus: () => fetchApi<{ data: any }>('/status'),
|
|
66
|
+
getSetupStatus: () =>
|
|
67
|
+
fetchApi<{ needsSetup: boolean; completedSteps: string[]; vaultUnlocked: boolean; dashboardPasswordSet: boolean; agentName: string }>('/setup/status'),
|
|
68
|
+
setupVault: (password: string) =>
|
|
69
|
+
fetchApi<{ success: boolean }>('/setup/vault', { method: 'POST', body: JSON.stringify({ password }) }),
|
|
70
|
+
setupDashboardPassword: (password: string) =>
|
|
71
|
+
fetchApi<{ success: boolean }>('/setup/dashboard-password', { method: 'POST', body: JSON.stringify({ password }) }),
|
|
72
|
+
setupIdentity: (name: string, pronouns: string, vibe?: string) =>
|
|
73
|
+
fetchApi<{ success: boolean }>('/setup/identity', { method: 'POST', body: JSON.stringify({ name, pronouns, vibe }) }),
|
|
74
|
+
getSetupTemplates: () =>
|
|
75
|
+
fetchApi<{ data: Array<{ id: string; name: string; description: string; preview: string }> }>('/setup/templates'),
|
|
76
|
+
setupPersonality: (template: string) =>
|
|
77
|
+
fetchApi<{ success: boolean }>('/setup/personality', { method: 'POST', body: JSON.stringify({ template }) }),
|
|
78
|
+
setupProvider: (provider: string, apiKey?: string, endpoint?: string) =>
|
|
79
|
+
fetchApi<{ success: boolean }>('/setup/provider', { method: 'POST', body: JSON.stringify({ provider, apiKey, endpoint }) }),
|
|
80
|
+
setupChannels: (channels: Array<{ type: string; enabled: boolean; credentials?: Record<string, string> }>) =>
|
|
81
|
+
fetchApi<{ success: boolean }>('/setup/channels', { method: 'POST', body: JSON.stringify({ channels }) }),
|
|
82
|
+
completeSetup: () =>
|
|
83
|
+
fetchApi<{ success: boolean }>('/setup/complete', { method: 'POST' }),
|
|
84
|
+
|
|
85
|
+
// Settings API
|
|
86
|
+
getModels: () => fetchApi<{ providers: any[]; routing: any; cost: any }>('/models'),
|
|
87
|
+
getIdentity: () => fetchApi<{ data: { name: string; pronouns: string } }>('/identity'),
|
|
88
|
+
updateIdentity: (name: string, pronouns: string) =>
|
|
89
|
+
fetchApi<{ success: boolean }>('/identity', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: JSON.stringify({ name, pronouns }),
|
|
92
|
+
}),
|
|
93
|
+
getPersonality: () =>
|
|
94
|
+
fetchApi<{ data: { template: { id: string; name: string } | null } }>('/personality'),
|
|
95
|
+
getTemplates: () =>
|
|
96
|
+
fetchApi<{ data: Array<{ id: string; name: string; description: string; preview: string }> }>('/personality/templates'),
|
|
97
|
+
updatePersonality: (template: string) =>
|
|
98
|
+
fetchApi<{ success: boolean }>('/personality', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
body: JSON.stringify({ template }),
|
|
101
|
+
}),
|
|
102
|
+
getPersonalityFull: () =>
|
|
103
|
+
fetchApi<{ data: {
|
|
104
|
+
name: string; pronouns: string; avatar: string | null; vibe: string;
|
|
105
|
+
tone: { warmth: number; directness: number; humor: number; formality: number };
|
|
106
|
+
errorStyle: string; expertise: string[]; catchphrases: Record<string, string>;
|
|
107
|
+
boundaries: { neverJokeAbout: string[]; neverAdviseOn: string[] };
|
|
108
|
+
customInstructions: string; soulContent: string | null; activeTemplate: string | null;
|
|
109
|
+
} }>('/personality/full'),
|
|
110
|
+
updatePersonalityFull: (data: Record<string, unknown>) =>
|
|
111
|
+
fetchApi<{ success: boolean }>('/personality/full', {
|
|
112
|
+
method: 'PUT',
|
|
113
|
+
body: JSON.stringify(data),
|
|
114
|
+
}),
|
|
115
|
+
updateProvider: (provider: string, apiKey?: string, endpoint?: string) =>
|
|
116
|
+
fetchApi<{ success: boolean }>('/provider', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body: JSON.stringify({ provider, apiKey, endpoint }),
|
|
119
|
+
}),
|
|
120
|
+
configureProvider: (provider: string, apiKey?: string, endpoint?: string) =>
|
|
121
|
+
fetchApi<{ success: boolean }>('/provider/configure', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: JSON.stringify({ provider, apiKey, endpoint }),
|
|
124
|
+
}),
|
|
125
|
+
// Claude OAuth
|
|
126
|
+
startClaudeOAuth: () =>
|
|
127
|
+
fetchApi<{ authUrl: string }>('/provider/claude-oauth/start', { method: 'POST' }),
|
|
128
|
+
completeClaudeOAuth: (code: string) =>
|
|
129
|
+
fetchApi<{ success: boolean }>('/provider/claude-oauth/callback', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
body: JSON.stringify({ code }),
|
|
132
|
+
}),
|
|
133
|
+
disconnectClaudeOAuth: () =>
|
|
134
|
+
fetchApi<{ success: boolean }>('/provider/claude-oauth/disconnect', { method: 'POST' }),
|
|
135
|
+
getClaudeOAuthStatus: () =>
|
|
136
|
+
fetchApi<{ connected: boolean }>('/provider/claude-oauth/status'),
|
|
137
|
+
updateRouting: (primary: string, fallback?: string) =>
|
|
138
|
+
fetchApi<{ success: boolean }>('/provider/routing', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
body: JSON.stringify({ primary, fallback }),
|
|
141
|
+
}),
|
|
142
|
+
setProviderModel: (provider: string, model: string) =>
|
|
143
|
+
fetchApi<{ success: boolean }>('/provider/model', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: JSON.stringify({ provider, model }),
|
|
146
|
+
}),
|
|
147
|
+
getSessionMessages: () =>
|
|
148
|
+
fetchApi<{ data: Array<{ id: string; role: string; content: string; timestamp: number }> }>('/session/messages'),
|
|
149
|
+
getChannels: () => fetchApi<{ data: { connected: string[]; configured: Array<{ type: string; enabled: boolean }> } }>('/channels'),
|
|
150
|
+
updateChannels: (channels: Array<{ type: string; enabled: boolean; credentials?: Record<string, string> }>) =>
|
|
151
|
+
fetchApi<{ success: boolean }>('/channels', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
body: JSON.stringify({ channels }),
|
|
154
|
+
}),
|
|
155
|
+
changeDashboardPassword: (oldPassword: string, newPassword: string) =>
|
|
156
|
+
fetchApi<{ success: boolean }>('/security/dashboard-password', {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
body: JSON.stringify({ oldPassword, newPassword }),
|
|
159
|
+
}),
|
|
160
|
+
changeVaultPassword: (newPassword: string) =>
|
|
161
|
+
fetchApi<{ success: boolean }>('/security/vault-password', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
body: JSON.stringify({ newPassword }),
|
|
164
|
+
}),
|
|
165
|
+
|
|
166
|
+
// Connector OAuth API
|
|
167
|
+
getConnectorStatus: (connectorId: string) =>
|
|
168
|
+
fetchApi<{ data: { connectorId: string; hasCredentials: boolean; connected: boolean; expiresAt?: number } }>(
|
|
169
|
+
`/connectors/${connectorId}/status`,
|
|
170
|
+
),
|
|
171
|
+
saveConnectorCredentials: (connectorId: string, clientId: string, clientSecret: string) =>
|
|
172
|
+
fetchApi<{ success: boolean; oauthUrl?: string }>(`/connectors/${connectorId}/credentials`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: JSON.stringify({ clientId, clientSecret }),
|
|
175
|
+
}),
|
|
176
|
+
disconnectConnector: (connectorId: string) =>
|
|
177
|
+
fetchApi<{ success: boolean }>(`/connectors/${connectorId}/disconnect`, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
// Ambient config
|
|
182
|
+
getAmbientConfig: () =>
|
|
183
|
+
fetchApi<{ data: any }>('/ambient/config'),
|
|
184
|
+
updateAmbientConfig: (config: Record<string, unknown>) =>
|
|
185
|
+
fetchApi<{ success: boolean }>('/ambient/config', {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
body: JSON.stringify(config),
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
// Appearance
|
|
191
|
+
getAppearance: () =>
|
|
192
|
+
fetchApi<{ data: { theme: string } }>('/appearance'),
|
|
193
|
+
updateAppearance: (theme: string) =>
|
|
194
|
+
fetchApi<{ success: boolean }>('/appearance', {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
body: JSON.stringify({ theme }),
|
|
197
|
+
}),
|
|
198
|
+
|
|
199
|
+
// Chat management
|
|
200
|
+
getChats: (archived?: boolean) => {
|
|
201
|
+
const qs = archived ? '?archived=true' : '';
|
|
202
|
+
return fetchApi<{ data: any[]; total: number }>(`/chats${qs}`);
|
|
203
|
+
},
|
|
204
|
+
createNewChat: (title?: string) =>
|
|
205
|
+
fetchApi<{ data: any }>('/chats', {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
body: JSON.stringify({ title }),
|
|
208
|
+
}),
|
|
209
|
+
getChatMessages: (chatId: string) =>
|
|
210
|
+
fetchApi<{ data: Array<{ id: string; role: string; content: string; timestamp: number }> }>(`/chats/${chatId}/messages`),
|
|
211
|
+
renameChat: (chatId: string, title: string) =>
|
|
212
|
+
fetchApi<{ data: any }>(`/chats/${chatId}`, {
|
|
213
|
+
method: 'PATCH',
|
|
214
|
+
body: JSON.stringify({ title }),
|
|
215
|
+
}),
|
|
216
|
+
archiveChat: (chatId: string) =>
|
|
217
|
+
fetchApi<{ data: any }>(`/chats/${chatId}`, {
|
|
218
|
+
method: 'PATCH',
|
|
219
|
+
body: JSON.stringify({ archived: true }),
|
|
220
|
+
}),
|
|
221
|
+
deleteChatThread: (chatId: string) =>
|
|
222
|
+
fetchApi<{ data: any }>(`/chats/${chatId}`, { method: 'DELETE' }),
|
|
223
|
+
|
|
224
|
+
// Notifications
|
|
225
|
+
getNotifications: () =>
|
|
226
|
+
fetchApi<{ data: any[] }>('/notifications'),
|
|
227
|
+
dismissNotification: (id: string) =>
|
|
228
|
+
fetchApi<{ data: { dismissed: boolean } }>(`/notifications/${id}/dismiss`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
}),
|
|
231
|
+
getNotificationPreferences: () =>
|
|
232
|
+
fetchApi<{ data: any }>('/notifications/preferences'),
|
|
233
|
+
updateNotificationPreferences: (prefs: Record<string, unknown>) =>
|
|
234
|
+
fetchApi<{ success: boolean }>('/notifications/preferences', {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
body: JSON.stringify(prefs),
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { api } from '../api';
|
|
3
|
+
|
|
4
|
+
interface ActivityEvent {
|
|
5
|
+
timestamp: string;
|
|
6
|
+
sequence: number;
|
|
7
|
+
event: string;
|
|
8
|
+
details: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const EVENT_LABELS: Record<string, (d: Record<string, unknown>) => string> = {
|
|
12
|
+
'behavior.executed': (d) => d.success ? `Behavior ran successfully` : `Behavior failed: ${d.error ?? 'unknown'}`,
|
|
13
|
+
'behavior.created': () => 'Behavior created',
|
|
14
|
+
'behavior.updated': () => 'Behavior updated',
|
|
15
|
+
'behavior.deleted': () => 'Behavior deleted',
|
|
16
|
+
'behavior.paused': () => 'Behavior paused',
|
|
17
|
+
'behavior.failed': (d) => `Behavior failed: ${d.error ?? 'unknown'}`,
|
|
18
|
+
'message.received': (d) => `Message received on ${d.channelType ?? 'unknown'}`,
|
|
19
|
+
'message.sent': (d) => `Message sent to ${d.channelType ?? 'unknown'}`,
|
|
20
|
+
'message.filtered': () => 'Message filtered',
|
|
21
|
+
'channel.connected': (d) => `${d.channelType ?? 'Channel'} connected`,
|
|
22
|
+
'channel.disconnected': (d) => `${d.channelType ?? 'Channel'} disconnected`,
|
|
23
|
+
'channel.error': (d) => `${d.channelType ?? 'Channel'} error`,
|
|
24
|
+
'webhook.triggered': (d) => `Webhook triggered: ${d.path ?? ''}`,
|
|
25
|
+
'webhook.received': () => 'Webhook received',
|
|
26
|
+
'webhook.created': () => 'Webhook created',
|
|
27
|
+
'webhook.deleted': () => 'Webhook deleted',
|
|
28
|
+
'webhook.error': (d) => `Webhook error: ${d.error ?? 'unknown'}`,
|
|
29
|
+
'system.startup': () => 'System started',
|
|
30
|
+
'system.shutdown': () => 'System shut down',
|
|
31
|
+
'system.error': (d) => `System error: ${d.error ?? 'unknown'}`,
|
|
32
|
+
'auth.login': () => 'Login',
|
|
33
|
+
'auth.logout': () => 'Logout',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const CATEGORY_COLORS: Record<string, string> = {
|
|
37
|
+
behavior: 'var(--accent)',
|
|
38
|
+
message: '#3b82f6',
|
|
39
|
+
channel: 'var(--success)',
|
|
40
|
+
webhook: '#f97316',
|
|
41
|
+
system: 'var(--text-secondary)',
|
|
42
|
+
auth: 'var(--danger)',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function getCategory(event: string): string {
|
|
46
|
+
return event.split('.')[0];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getLabel(event: string, details: Record<string, unknown>): string {
|
|
50
|
+
const fn = EVENT_LABELS[event];
|
|
51
|
+
if (fn) return fn(details);
|
|
52
|
+
return event.replace(/\./g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function timeAgo(iso: string): string {
|
|
56
|
+
const diff = (Date.now() - new Date(iso).getTime()) / 1000;
|
|
57
|
+
if (diff < 60) return 'just now';
|
|
58
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
59
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
60
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MAX_EVENTS = 100;
|
|
64
|
+
|
|
65
|
+
export function ActivityFeed() {
|
|
66
|
+
const [events, setEvents] = useState<ActivityEvent[]>([]);
|
|
67
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
api.getAudit({ limit: 50 }).then((res) => {
|
|
71
|
+
if (res.data) {
|
|
72
|
+
const PREFIXES = ['behavior.', 'message.', 'channel.', 'webhook.', 'system.', 'auth.login', 'auth.logout'];
|
|
73
|
+
const filtered = res.data
|
|
74
|
+
.filter((e: any) => PREFIXES.some((p) => e.event.startsWith(p)))
|
|
75
|
+
.reverse();
|
|
76
|
+
setEvents(filtered);
|
|
77
|
+
}
|
|
78
|
+
}).catch(() => {});
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
83
|
+
const host = import.meta.env.DEV ? 'localhost:18800' : window.location.host;
|
|
84
|
+
const ws = new WebSocket(`${protocol}//${host}/`);
|
|
85
|
+
wsRef.current = ws;
|
|
86
|
+
|
|
87
|
+
ws.onmessage = (evt) => {
|
|
88
|
+
try {
|
|
89
|
+
const msg = JSON.parse(evt.data);
|
|
90
|
+
if (msg.type === 'activity' && msg.payload) {
|
|
91
|
+
setEvents((prev) => {
|
|
92
|
+
const next = [msg.payload, ...prev];
|
|
93
|
+
return next.length > MAX_EVENTS ? next.slice(0, MAX_EVENTS) : next;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch { /* ignore non-JSON */ }
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return () => { ws.close(); };
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="activity-feed">
|
|
104
|
+
<h3 className="mc-section-title">Live Activity</h3>
|
|
105
|
+
<div className="activity-feed-list">
|
|
106
|
+
{events.length === 0 && (
|
|
107
|
+
<div className="activity-empty">No recent activity</div>
|
|
108
|
+
)}
|
|
109
|
+
{events.map((e, i) => {
|
|
110
|
+
const cat = getCategory(e.event);
|
|
111
|
+
const color = CATEGORY_COLORS[cat] ?? 'var(--text-secondary)';
|
|
112
|
+
return (
|
|
113
|
+
<div key={`${e.sequence}-${i}`} className="activity-item">
|
|
114
|
+
<span className="activity-dot" style={{ background: color }} />
|
|
115
|
+
<span className="activity-label">{getLabel(e.event, e.details)}</span>
|
|
116
|
+
<span className="activity-time">{timeAgo(e.timestamp)}</span>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { api } from '../api';
|
|
3
|
+
|
|
4
|
+
interface Behavior {
|
|
5
|
+
id: string;
|
|
6
|
+
type: 'scheduled' | 'monitor' | 'one-shot';
|
|
7
|
+
status: 'active' | 'paused' | 'deleted' | 'missed';
|
|
8
|
+
action: string;
|
|
9
|
+
runCount: number;
|
|
10
|
+
failCount: number;
|
|
11
|
+
maxFailures: number;
|
|
12
|
+
lastRun?: string;
|
|
13
|
+
lastResult?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function timeAgo(iso: string): string {
|
|
17
|
+
const diff = (Date.now() - new Date(iso).getTime()) / 1000;
|
|
18
|
+
if (diff < 60) return 'just now';
|
|
19
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
20
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
21
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getHealthColor(b: Behavior): string {
|
|
25
|
+
if (b.status === 'paused' && b.failCount >= b.maxFailures) return 'var(--danger)';
|
|
26
|
+
if (b.status === 'paused') return 'var(--text-secondary)';
|
|
27
|
+
if (b.failCount > 0) return 'var(--warning)';
|
|
28
|
+
return 'var(--success)';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getHealthLabel(b: Behavior): string {
|
|
32
|
+
if (b.status === 'paused' && b.failCount >= b.maxFailures) return 'Auto-paused';
|
|
33
|
+
if (b.status === 'paused') return 'Paused';
|
|
34
|
+
if (b.failCount > 0) return `${b.failCount} failures`;
|
|
35
|
+
return 'Healthy';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
39
|
+
scheduled: 'Sched',
|
|
40
|
+
monitor: 'Monitor',
|
|
41
|
+
'one-shot': 'Once',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function BehaviorHealth() {
|
|
45
|
+
const [behaviors, setBehaviors] = useState<Behavior[]>([]);
|
|
46
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
api.getBehaviors().then((res) => {
|
|
50
|
+
if (res.data) {
|
|
51
|
+
setBehaviors(res.data.filter((b: any) => b.status !== 'deleted'));
|
|
52
|
+
}
|
|
53
|
+
}).catch(() => {});
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
58
|
+
const host = import.meta.env.DEV ? 'localhost:18800' : window.location.host;
|
|
59
|
+
const ws = new WebSocket(`${protocol}//${host}/`);
|
|
60
|
+
wsRef.current = ws;
|
|
61
|
+
|
|
62
|
+
ws.onmessage = (evt) => {
|
|
63
|
+
try {
|
|
64
|
+
const msg = JSON.parse(evt.data);
|
|
65
|
+
if (msg.type === 'activity' && msg.payload?.event?.startsWith('behavior.')) {
|
|
66
|
+
api.getBehaviors().then((res) => {
|
|
67
|
+
if (res.data) {
|
|
68
|
+
setBehaviors(res.data.filter((b: any) => b.status !== 'deleted'));
|
|
69
|
+
}
|
|
70
|
+
}).catch(() => {});
|
|
71
|
+
}
|
|
72
|
+
} catch { /* ignore */ }
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return () => { ws.close(); };
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="behavior-health">
|
|
80
|
+
<h3 className="mc-section-title">Behavior Health</h3>
|
|
81
|
+
{behaviors.length === 0 && (
|
|
82
|
+
<div className="bh-empty">No behaviors configured</div>
|
|
83
|
+
)}
|
|
84
|
+
<div className="bh-list">
|
|
85
|
+
{behaviors.map((b) => (
|
|
86
|
+
<div key={b.id} className="bh-card">
|
|
87
|
+
<div className="bh-card-header">
|
|
88
|
+
<span className="bh-dot" style={{ background: getHealthColor(b) }} />
|
|
89
|
+
<span className="bh-action">{b.action.length > 60 ? b.action.slice(0, 57) + '...' : b.action}</span>
|
|
90
|
+
<span className="bh-type-badge">{TYPE_LABELS[b.type] ?? b.type}</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="bh-card-meta">
|
|
93
|
+
<span className="bh-health-label" style={{ color: getHealthColor(b) }}>{getHealthLabel(b)}</span>
|
|
94
|
+
<span className="bh-stat">{b.runCount} runs</span>
|
|
95
|
+
{b.lastRun && <span className="bh-stat">{timeAgo(b.lastRun)}</span>}
|
|
96
|
+
</div>
|
|
97
|
+
{b.lastResult && (
|
|
98
|
+
<div className="bh-result">{b.lastResult.length > 120 ? b.lastResult.slice(0, 117) + '...' : b.lastResult}</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface Column<T> {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
render?: (row: T) => React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface DataTableProps<T> {
|
|
8
|
+
columns: Column<T>[];
|
|
9
|
+
rows: T[];
|
|
10
|
+
keyField: string;
|
|
11
|
+
actions?: (row: T) => React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DataTable<T extends Record<string, any>>({ columns, rows, keyField, actions }: DataTableProps<T>) {
|
|
15
|
+
return (
|
|
16
|
+
<table className="data-table">
|
|
17
|
+
<thead>
|
|
18
|
+
<tr>
|
|
19
|
+
{columns.map((col) => <th key={col.key}>{col.label}</th>)}
|
|
20
|
+
{actions && <th>Actions</th>}
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
{rows.length === 0 ? (
|
|
25
|
+
<tr><td colSpan={columns.length + (actions ? 1 : 0)} className="empty-row">No data</td></tr>
|
|
26
|
+
) : (
|
|
27
|
+
rows.map((row) => (
|
|
28
|
+
<tr key={row[keyField]}>
|
|
29
|
+
{columns.map((col) => (
|
|
30
|
+
<td key={col.key}>{col.render ? col.render(row) : String(row[col.key] ?? '')}</td>
|
|
31
|
+
))}
|
|
32
|
+
{actions && <td className="actions-cell">{actions(row)}</td>}
|
|
33
|
+
</tr>
|
|
34
|
+
))
|
|
35
|
+
)}
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { useApi } from '../hooks/useApi';
|
|
4
|
+
import { usePolling } from '../hooks/usePolling';
|
|
5
|
+
import { api } from '../api';
|
|
6
|
+
|
|
7
|
+
const CHANNEL_TYPES = ['webchat', 'discord', 'telegram', 'slack', 'twilio', 'matrix', 'signal', 'teams', 'whatsapp', 'email'] as const;
|
|
8
|
+
|
|
9
|
+
export function Layout() {
|
|
10
|
+
const [checking, setChecking] = useState(true);
|
|
11
|
+
const [ready, setReady] = useState(false);
|
|
12
|
+
const [agentName, setAgentName] = useState('Auxiora');
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
api.getSetupStatus()
|
|
17
|
+
.then(status => {
|
|
18
|
+
if (status.agentName) setAgentName(status.agentName);
|
|
19
|
+
if (status.needsSetup) {
|
|
20
|
+
navigate('/setup', { replace: true });
|
|
21
|
+
} else if (!status.vaultUnlocked) {
|
|
22
|
+
navigate('/unlock', { replace: true });
|
|
23
|
+
} else {
|
|
24
|
+
setReady(true);
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.catch(() => {})
|
|
28
|
+
.finally(() => setChecking(false));
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
// Only make authenticated calls once vault/setup check passes
|
|
32
|
+
const { data: status, refresh } = useApi(() => ready ? api.getStatus() : Promise.resolve(null), [ready]);
|
|
33
|
+
const { data: sessions, refresh: refreshSessions } = useApi(() => ready ? api.getSessions() : Promise.resolve(null), [ready]);
|
|
34
|
+
usePolling(() => { if (ready) { refresh(); refreshSessions(); } });
|
|
35
|
+
|
|
36
|
+
if (checking) return null;
|
|
37
|
+
|
|
38
|
+
// Derive connected channel types from active sessions
|
|
39
|
+
const connectedChannels = new Set<string>();
|
|
40
|
+
if (sessions?.data) {
|
|
41
|
+
for (const s of sessions.data) {
|
|
42
|
+
if (s.channelType) connectedChannels.add(s.channelType);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Only show channels that have connections or are commonly configured
|
|
47
|
+
const visibleChannels = CHANNEL_TYPES.filter(ch =>
|
|
48
|
+
connectedChannels.has(ch) || ch === 'webchat'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="layout">
|
|
53
|
+
<nav className="sidebar">
|
|
54
|
+
<div className="sidebar-header">
|
|
55
|
+
<h1>{agentName}</h1>
|
|
56
|
+
</div>
|
|
57
|
+
<ul className="nav-list">
|
|
58
|
+
{/* MAIN */}
|
|
59
|
+
<div className="nav-group">
|
|
60
|
+
<div className="nav-group-label">Main</div>
|
|
61
|
+
<li>
|
|
62
|
+
<NavLink to="/chat" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
63
|
+
Chat
|
|
64
|
+
</NavLink>
|
|
65
|
+
</li>
|
|
66
|
+
<li>
|
|
67
|
+
<NavLink to="/" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'} end>
|
|
68
|
+
Mission Control
|
|
69
|
+
</NavLink>
|
|
70
|
+
</li>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* CHANNELS */}
|
|
74
|
+
<div className="nav-group">
|
|
75
|
+
<div className="nav-group-label">Channels</div>
|
|
76
|
+
{visibleChannels.map(ch => (
|
|
77
|
+
<li key={ch}>
|
|
78
|
+
<NavLink to="/settings/channels" className="nav-link">
|
|
79
|
+
{ch.charAt(0).toUpperCase() + ch.slice(1)}
|
|
80
|
+
<span className={`channel-dot ${connectedChannels.has(ch) ? 'connected' : 'disconnected'}`} />
|
|
81
|
+
</NavLink>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* MANAGEMENT */}
|
|
87
|
+
<div className="nav-group">
|
|
88
|
+
<div className="nav-group-label">Management</div>
|
|
89
|
+
<li>
|
|
90
|
+
<NavLink to="/behaviors" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
91
|
+
Behaviors
|
|
92
|
+
</NavLink>
|
|
93
|
+
</li>
|
|
94
|
+
<li>
|
|
95
|
+
<NavLink to="/webhooks" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
96
|
+
Webhooks
|
|
97
|
+
</NavLink>
|
|
98
|
+
</li>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* SETTINGS */}
|
|
102
|
+
<div className="nav-group">
|
|
103
|
+
<div className="nav-group-label">Settings</div>
|
|
104
|
+
<li>
|
|
105
|
+
<NavLink to="/settings/personality" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
106
|
+
Personality
|
|
107
|
+
</NavLink>
|
|
108
|
+
</li>
|
|
109
|
+
<li>
|
|
110
|
+
<NavLink to="/settings/provider" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
111
|
+
Provider
|
|
112
|
+
</NavLink>
|
|
113
|
+
</li>
|
|
114
|
+
<li>
|
|
115
|
+
<NavLink to="/settings/channels" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
116
|
+
Channels
|
|
117
|
+
</NavLink>
|
|
118
|
+
</li>
|
|
119
|
+
<li>
|
|
120
|
+
<NavLink to="/settings/connections" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
121
|
+
Connections
|
|
122
|
+
</NavLink>
|
|
123
|
+
</li>
|
|
124
|
+
<li>
|
|
125
|
+
<NavLink to="/settings/ambient" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
126
|
+
Ambient
|
|
127
|
+
</NavLink>
|
|
128
|
+
</li>
|
|
129
|
+
<li>
|
|
130
|
+
<NavLink to="/settings/appearance" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
131
|
+
Appearance
|
|
132
|
+
</NavLink>
|
|
133
|
+
</li>
|
|
134
|
+
<li>
|
|
135
|
+
<NavLink to="/settings/notifications" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
136
|
+
Notifications
|
|
137
|
+
</NavLink>
|
|
138
|
+
</li>
|
|
139
|
+
<li>
|
|
140
|
+
<NavLink to="/settings/security" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
141
|
+
Security
|
|
142
|
+
</NavLink>
|
|
143
|
+
</li>
|
|
144
|
+
<li>
|
|
145
|
+
<NavLink to="/settings/audit" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
|
|
146
|
+
Audit Log
|
|
147
|
+
</NavLink>
|
|
148
|
+
</li>
|
|
149
|
+
</div>
|
|
150
|
+
</ul>
|
|
151
|
+
<button className="logout-btn" onClick={() => api.logout().then(() => { window.location.href = '/dashboard/login'; })}>
|
|
152
|
+
Logout
|
|
153
|
+
</button>
|
|
154
|
+
</nav>
|
|
155
|
+
<main className="content">
|
|
156
|
+
<Outlet />
|
|
157
|
+
</main>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|