@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.
Files changed (97) hide show
  1. package/LICENSE +191 -0
  2. package/dist/auth.d.ts +13 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +69 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/cloud-types.d.ts +71 -0
  7. package/dist/cloud-types.d.ts.map +1 -0
  8. package/dist/cloud-types.js +2 -0
  9. package/dist/cloud-types.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/router.d.ts +13 -0
  15. package/dist/router.d.ts.map +1 -0
  16. package/dist/router.js +2250 -0
  17. package/dist/router.js.map +1 -0
  18. package/dist/types.d.ts +314 -0
  19. package/dist/types.d.ts.map +1 -0
  20. package/dist/types.js +7 -0
  21. package/dist/types.js.map +1 -0
  22. package/dist-ui/assets/index-BfY0i5jw.css +1 -0
  23. package/dist-ui/assets/index-CXpk9mvw.js +60 -0
  24. package/dist-ui/icon.svg +59 -0
  25. package/dist-ui/index.html +20 -0
  26. package/package.json +32 -0
  27. package/src/auth.ts +83 -0
  28. package/src/cloud-types.ts +63 -0
  29. package/src/index.ts +5 -0
  30. package/src/router.ts +2494 -0
  31. package/src/types.ts +269 -0
  32. package/tests/auth.test.ts +51 -0
  33. package/tests/cloud-router.test.ts +249 -0
  34. package/tests/desktop-router.test.ts +151 -0
  35. package/tests/router.test.ts +388 -0
  36. package/tests/trust-router.test.ts +170 -0
  37. package/tsconfig.json +12 -0
  38. package/tsconfig.tsbuildinfo +1 -0
  39. package/ui/index.html +19 -0
  40. package/ui/node_modules/.bin/browserslist +17 -0
  41. package/ui/node_modules/.bin/tsc +17 -0
  42. package/ui/node_modules/.bin/tsserver +17 -0
  43. package/ui/node_modules/.bin/vite +17 -0
  44. package/ui/package.json +23 -0
  45. package/ui/public/icon.svg +59 -0
  46. package/ui/src/App.tsx +63 -0
  47. package/ui/src/api.ts +238 -0
  48. package/ui/src/components/ActivityFeed.tsx +123 -0
  49. package/ui/src/components/BehaviorHealth.tsx +105 -0
  50. package/ui/src/components/DataTable.tsx +39 -0
  51. package/ui/src/components/Layout.tsx +160 -0
  52. package/ui/src/components/PasswordStrength.tsx +31 -0
  53. package/ui/src/components/SetupProgress.tsx +26 -0
  54. package/ui/src/components/StatusBadge.tsx +12 -0
  55. package/ui/src/components/ThemeSelector.tsx +39 -0
  56. package/ui/src/contexts/ThemeContext.tsx +58 -0
  57. package/ui/src/hooks/useApi.ts +19 -0
  58. package/ui/src/hooks/usePolling.ts +8 -0
  59. package/ui/src/main.tsx +16 -0
  60. package/ui/src/pages/AuditLog.tsx +36 -0
  61. package/ui/src/pages/Behaviors.tsx +426 -0
  62. package/ui/src/pages/Chat.tsx +688 -0
  63. package/ui/src/pages/Login.tsx +64 -0
  64. package/ui/src/pages/Overview.tsx +56 -0
  65. package/ui/src/pages/Sessions.tsx +26 -0
  66. package/ui/src/pages/SettingsAmbient.tsx +185 -0
  67. package/ui/src/pages/SettingsConnections.tsx +201 -0
  68. package/ui/src/pages/SettingsNotifications.tsx +241 -0
  69. package/ui/src/pages/SetupAppearance.tsx +45 -0
  70. package/ui/src/pages/SetupChannels.tsx +143 -0
  71. package/ui/src/pages/SetupComplete.tsx +31 -0
  72. package/ui/src/pages/SetupConnections.tsx +80 -0
  73. package/ui/src/pages/SetupDashboardPassword.tsx +50 -0
  74. package/ui/src/pages/SetupIdentity.tsx +68 -0
  75. package/ui/src/pages/SetupPersonality.tsx +78 -0
  76. package/ui/src/pages/SetupProvider.tsx +65 -0
  77. package/ui/src/pages/SetupVault.tsx +50 -0
  78. package/ui/src/pages/SetupWelcome.tsx +19 -0
  79. package/ui/src/pages/UnlockVault.tsx +56 -0
  80. package/ui/src/pages/Webhooks.tsx +158 -0
  81. package/ui/src/pages/settings/Appearance.tsx +63 -0
  82. package/ui/src/pages/settings/Channels.tsx +138 -0
  83. package/ui/src/pages/settings/Identity.tsx +61 -0
  84. package/ui/src/pages/settings/Personality.tsx +54 -0
  85. package/ui/src/pages/settings/PersonalityEditor.tsx +577 -0
  86. package/ui/src/pages/settings/Provider.tsx +537 -0
  87. package/ui/src/pages/settings/Security.tsx +111 -0
  88. package/ui/src/styles/global.css +2308 -0
  89. package/ui/src/styles/themes/index.css +7 -0
  90. package/ui/src/styles/themes/monolith.css +125 -0
  91. package/ui/src/styles/themes/nebula.css +90 -0
  92. package/ui/src/styles/themes/neon.css +149 -0
  93. package/ui/src/styles/themes/polar.css +151 -0
  94. package/ui/src/styles/themes/signal.css +163 -0
  95. package/ui/src/styles/themes/terra.css +146 -0
  96. package/ui/tsconfig.json +14 -0
  97. package/ui/vite.config.ts +20 -0
@@ -0,0 +1,78 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { api } from '../api';
4
+ import { useApi } from '../hooks/useApi';
5
+ import { SetupProgress } from '../components/SetupProgress';
6
+
7
+ const VIBE_KEYWORDS: Record<string, string[]> = {
8
+ chill: ['chill', 'relaxed', 'laid-back', 'easygoing', 'calm'],
9
+ professional: ['professional', 'formal', 'precise', 'corporate', 'business'],
10
+ friendly: ['friendly', 'warm', 'kind', 'caring', 'supportive', 'best friend'],
11
+ creative: ['creative', 'imaginative', 'artistic', 'playful', 'quirky'],
12
+ mentor: ['mentor', 'teacher', 'guide', 'coach', 'wise'],
13
+ technical: ['technical', 'code', 'developer', 'engineer', 'hacker'],
14
+ };
15
+
16
+ function matchVibeToTemplate(vibe: string): string | null {
17
+ const lower = vibe.toLowerCase();
18
+ for (const [templateId, keywords] of Object.entries(VIBE_KEYWORDS)) {
19
+ if (keywords.some((kw) => lower.includes(kw))) return templateId;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export function SetupPersonality() {
25
+ const { data: templates, loading: templatesLoading, error: templatesError } = useApi(() => api.getSetupTemplates(), []);
26
+ const [selected, setSelected] = useState('');
27
+ const [error, setError] = useState('');
28
+ const [loading, setLoading] = useState(false);
29
+ const navigate = useNavigate();
30
+ const recommendedId = useMemo(() => {
31
+ const vibe = localStorage.getItem('auxiora_setup_vibe');
32
+ if (!vibe) return null;
33
+ localStorage.removeItem('auxiora_setup_vibe');
34
+ return matchVibeToTemplate(vibe);
35
+ }, []);
36
+
37
+ const handleSelect = async (templateId: string) => {
38
+ setSelected(templateId);
39
+ setLoading(true);
40
+ setError('');
41
+ try {
42
+ await api.setupPersonality(templateId);
43
+ navigate('/setup/appearance');
44
+ } catch (err: unknown) {
45
+ setError(err instanceof Error ? err.message : 'Failed to set personality');
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <div className="setup-page">
52
+ <SetupProgress currentStep={4} />
53
+ <div className="setup-card" style={{ maxWidth: 680 }}>
54
+ <h1>Personality</h1>
55
+ <p className="subtitle">Choose a personality template for your assistant.</p>
56
+ {templatesLoading && <p style={{ color: 'var(--text-secondary)' }}>Loading templates...</p>}
57
+ {templatesError && <p className="error">{templatesError}</p>}
58
+ {templates && (
59
+ <div className="template-grid">
60
+ {templates.data.map((t) => (
61
+ <div
62
+ key={t.id}
63
+ className={`template-card${selected === t.id ? ' selected' : ''}${recommendedId === t.id ? ' recommended' : ''}`}
64
+ onClick={() => !loading && handleSelect(t.id)}
65
+ >
66
+ <h3>{t.name}{recommendedId === t.id && <span className="badge badge-primary" style={{ marginLeft: '0.5rem', fontSize: '0.6rem' }}>Recommended</span>}</h3>
67
+ <p>{t.description}</p>
68
+ {t.preview && <p style={{ marginTop: '0.5rem', fontStyle: 'italic', fontSize: '0.75rem' }}>{t.preview}</p>}
69
+ </div>
70
+ ))}
71
+ </div>
72
+ )}
73
+ {error && <p className="error">{error}</p>}
74
+ {loading && <p style={{ color: 'var(--text-secondary)', textAlign: 'center' }}>Saving...</p>}
75
+ </div>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,65 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { api } from '../api';
4
+ import { SetupProgress } from '../components/SetupProgress';
5
+
6
+ export function SetupProvider() {
7
+ const [provider, setProvider] = useState('anthropic');
8
+ const [apiKey, setApiKey] = useState('');
9
+ const [endpoint, setEndpoint] = useState('http://localhost:11434');
10
+ const [error, setError] = useState('');
11
+ const [loading, setLoading] = useState(false);
12
+ const navigate = useNavigate();
13
+
14
+ const handleSubmit = async (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ setLoading(true);
17
+ setError('');
18
+ try {
19
+ await api.setupProvider(
20
+ provider,
21
+ provider !== 'ollama' ? apiKey : undefined,
22
+ provider === 'ollama' ? endpoint : undefined,
23
+ );
24
+ navigate('/setup/channels');
25
+ } catch (err: unknown) {
26
+ setError(err instanceof Error ? err.message : 'Failed to configure provider');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="setup-page">
34
+ <SetupProgress currentStep={6} />
35
+ <div className="setup-card">
36
+ <h1>AI Provider</h1>
37
+ <p className="subtitle">Choose which AI model provider to use.</p>
38
+ <form onSubmit={handleSubmit}>
39
+ <label>Provider</label>
40
+ <select value={provider} onChange={(e) => setProvider(e.target.value)}>
41
+ <option value="anthropic">Anthropic Claude</option>
42
+ <option value="openai">OpenAI</option>
43
+ <option value="ollama">Ollama (Local)</option>
44
+ </select>
45
+ {provider !== 'ollama' && (
46
+ <>
47
+ <label>API Key</label>
48
+ <input type="password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="sk-..." />
49
+ </>
50
+ )}
51
+ {provider === 'ollama' && (
52
+ <>
53
+ <label>Endpoint URL</label>
54
+ <input type="text" value={endpoint} onChange={(e) => setEndpoint(e.target.value)} />
55
+ </>
56
+ )}
57
+ <button type="submit" className="setup-btn-primary" disabled={loading}>
58
+ {loading ? 'Saving...' : 'Continue'}
59
+ </button>
60
+ {error && <p className="error">{error}</p>}
61
+ </form>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,50 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { api } from '../api';
4
+ import { SetupProgress } from '../components/SetupProgress';
5
+ import { PasswordStrength } from '../components/PasswordStrength';
6
+
7
+ export function SetupVault() {
8
+ const [password, setPassword] = useState('');
9
+ const [confirm, setConfirm] = useState('');
10
+ const [error, setError] = useState('');
11
+ const [loading, setLoading] = useState(false);
12
+ const navigate = useNavigate();
13
+
14
+ const handleSubmit = async (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ if (password.length < 8) { setError('Password must be at least 8 characters'); return; }
17
+ if (password !== confirm) { setError('Passwords do not match'); return; }
18
+ setLoading(true);
19
+ setError('');
20
+ try {
21
+ await api.setupVault(password);
22
+ navigate('/setup/dashboard-password');
23
+ } catch (err: unknown) {
24
+ setError(err instanceof Error ? err.message : 'Failed to create vault');
25
+ } finally {
26
+ setLoading(false);
27
+ }
28
+ };
29
+
30
+ return (
31
+ <div className="setup-page">
32
+ <SetupProgress currentStep={1} />
33
+ <div className="setup-card">
34
+ <h1>Create Vault Password</h1>
35
+ <p className="subtitle">This password encrypts your secrets. Store it somewhere safe — it cannot be recovered.</p>
36
+ <form onSubmit={handleSubmit}>
37
+ <label>Vault password</label>
38
+ <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoFocus />
39
+ <PasswordStrength password={password} />
40
+ <label>Confirm password</label>
41
+ <input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} />
42
+ <button type="submit" className="setup-btn-primary" disabled={loading}>
43
+ {loading ? 'Creating...' : 'Create Vault'}
44
+ </button>
45
+ {error && <p className="error">{error}</p>}
46
+ </form>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,19 @@
1
+ import { useNavigate } from 'react-router-dom';
2
+
3
+ export function SetupWelcome() {
4
+ const navigate = useNavigate();
5
+
6
+ return (
7
+ <div className="setup-page">
8
+ <div className="setup-card" style={{ textAlign: 'center' }}>
9
+ <h1>Welcome to Auxiora</h1>
10
+ <p className="subtitle">
11
+ Your personal AI assistant. Let's get you set up — it only takes a minute.
12
+ </p>
13
+ <button className="setup-btn-primary" onClick={() => navigate('/setup/vault')}>
14
+ Get Started
15
+ </button>
16
+ </div>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,56 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { api } from '../api';
4
+
5
+ export function UnlockVault() {
6
+ const [password, setPassword] = useState('');
7
+ const [error, setError] = useState('');
8
+ const [loading, setLoading] = useState(false);
9
+ const [agentName, setAgentName] = useState('Auxiora');
10
+ const navigate = useNavigate();
11
+
12
+ useEffect(() => {
13
+ api.getSetupStatus()
14
+ .then(status => { if (status.agentName) setAgentName(status.agentName); })
15
+ .catch(() => {});
16
+ }, []);
17
+
18
+ const handleSubmit = async (e: React.FormEvent) => {
19
+ e.preventDefault();
20
+ if (!password) return;
21
+ setLoading(true);
22
+ setError('');
23
+ try {
24
+ await api.setupVault(password);
25
+ navigate('/login', { replace: true });
26
+ } catch (err: unknown) {
27
+ setError(err instanceof Error ? err.message : 'Wrong password');
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+
33
+ return (
34
+ <div className="login-page">
35
+ <div className="login-card">
36
+ <h1>Unlock Vault</h1>
37
+ <p style={{ color: 'var(--text-secondary)', marginBottom: '1.5rem', fontSize: '0.9rem' }}>
38
+ Enter your vault password to start {agentName}.
39
+ </p>
40
+ <form onSubmit={handleSubmit}>
41
+ <input
42
+ type="password"
43
+ value={password}
44
+ onChange={(e) => setPassword(e.target.value)}
45
+ placeholder="Vault password"
46
+ autoFocus
47
+ />
48
+ <button type="submit" disabled={loading || !password}>
49
+ {loading ? 'Unlocking...' : 'Unlock'}
50
+ </button>
51
+ {error && <p className="error">{error}</p>}
52
+ </form>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,158 @@
1
+ import { useState } from 'react';
2
+ import { useApi } from '../hooks/useApi';
3
+ import { usePolling } from '../hooks/usePolling';
4
+ import { api } from '../api';
5
+ import { DataTable } from '../components/DataTable';
6
+ import { StatusBadge } from '../components/StatusBadge';
7
+
8
+ export function Webhooks() {
9
+ const { data, refresh } = useApi(() => api.getWebhooks(), []);
10
+ const { data: behaviorsData } = useApi(() => api.getBehaviors(), []);
11
+ usePolling(refresh);
12
+
13
+ const webhooks = data?.data ?? [];
14
+ const behaviors = behaviorsData?.data ?? [];
15
+
16
+ const [showForm, setShowForm] = useState(false);
17
+ const [name, setName] = useState('');
18
+ const [secret, setSecret] = useState('');
19
+ const [behaviorId, setBehaviorId] = useState('');
20
+ const [creating, setCreating] = useState(false);
21
+ const [error, setError] = useState('');
22
+ const [createdUrl, setCreatedUrl] = useState('');
23
+
24
+ const columns = [
25
+ { key: 'name', label: 'Name' },
26
+ { key: 'type', label: 'Type' },
27
+ { key: 'enabled', label: 'Status', render: (w: any) => <StatusBadge status={w.enabled ? 'enabled' : 'disabled'} /> },
28
+ { key: 'behaviorId', label: 'Behavior', render: (w: any) => w.behaviorId || '-' },
29
+ { key: 'createdAt', label: 'Created', render: (w: any) => new Date(w.createdAt).toLocaleDateString() },
30
+ ];
31
+
32
+ const handleToggle = async (w: any) => {
33
+ try {
34
+ await api.patchWebhook(w.id, { enabled: !w.enabled });
35
+ refresh();
36
+ } catch (err: any) {
37
+ alert(err.message || 'Failed to update webhook');
38
+ }
39
+ };
40
+
41
+ const handleDelete = async (w: any) => {
42
+ if (!confirm(`Delete webhook "${w.name}"?`)) return;
43
+ try {
44
+ await api.deleteWebhook(w.id);
45
+ refresh();
46
+ } catch (err: any) {
47
+ alert(err.message || 'Failed to delete webhook');
48
+ }
49
+ };
50
+
51
+ const resetForm = () => {
52
+ setName('');
53
+ setSecret('');
54
+ setBehaviorId('');
55
+ setError('');
56
+ setCreatedUrl('');
57
+ };
58
+
59
+ const handleCreate = async (e: React.FormEvent) => {
60
+ e.preventDefault();
61
+ setError('');
62
+ setCreatedUrl('');
63
+ setCreating(true);
64
+
65
+ try {
66
+ const input: Record<string, unknown> = { name, secret };
67
+ if (behaviorId) input.behaviorId = behaviorId;
68
+
69
+ await api.createWebhook(input);
70
+ const webhookUrl = `${window.location.origin}/api/v1/webhooks/custom/${name}`;
71
+ setCreatedUrl(webhookUrl);
72
+ resetForm();
73
+ setShowForm(false);
74
+ refresh();
75
+ } catch (err: any) {
76
+ setError(err.message || 'Failed to create webhook');
77
+ } finally {
78
+ setCreating(false);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <div className="page">
84
+ <h2>Webhooks</h2>
85
+
86
+ <div className="create-form-toggle">
87
+ <button
88
+ className="btn-sm"
89
+ onClick={() => { setShowForm(!showForm); if (showForm) resetForm(); }}
90
+ >
91
+ {showForm ? 'Cancel' : 'New Webhook'}
92
+ </button>
93
+ </div>
94
+
95
+ {createdUrl && (
96
+ <div className="create-form-success">
97
+ Webhook created. URL: <code>{createdUrl}</code>
98
+ </div>
99
+ )}
100
+
101
+ {showForm && (
102
+ <div className="create-form">
103
+ <form onSubmit={handleCreate}>
104
+ <label>Name</label>
105
+ <input
106
+ type="text"
107
+ value={name}
108
+ onChange={e => setName(e.target.value)}
109
+ placeholder="my-webhook"
110
+ pattern="[a-zA-Z0-9_-]+"
111
+ title="URL-safe: letters, numbers, hyphens, underscores"
112
+ required
113
+ />
114
+
115
+ <label>Secret</label>
116
+ <input
117
+ type="password"
118
+ value={secret}
119
+ onChange={e => setSecret(e.target.value)}
120
+ placeholder="HMAC signing key"
121
+ required
122
+ />
123
+
124
+ <label>Behavior (optional)</label>
125
+ <select value={behaviorId} onChange={e => setBehaviorId(e.target.value)}>
126
+ <option value="">None</option>
127
+ {behaviors.map((b: any) => (
128
+ <option key={b.id} value={b.id}>
129
+ {b.action?.slice(0, 50) || b.id}
130
+ </option>
131
+ ))}
132
+ </select>
133
+
134
+ {error && <div className="error">{error}</div>}
135
+
136
+ <button type="submit" className="settings-btn" disabled={creating || !name || !secret}>
137
+ {creating ? 'Creating...' : 'Create Webhook'}
138
+ </button>
139
+ </form>
140
+ </div>
141
+ )}
142
+
143
+ <DataTable
144
+ columns={columns}
145
+ rows={webhooks}
146
+ keyField="id"
147
+ actions={(w: any) => (
148
+ <>
149
+ <button className="btn-sm" onClick={() => handleToggle(w)}>
150
+ {w.enabled ? 'Disable' : 'Enable'}
151
+ </button>
152
+ <button className="btn-sm btn-danger" onClick={() => handleDelete(w)}>Delete</button>
153
+ </>
154
+ )}
155
+ />
156
+ </div>
157
+ );
158
+ }
@@ -0,0 +1,63 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { ThemeSelector } from '../../components/ThemeSelector';
3
+ import { useTheme, type ThemeId } from '../../contexts/ThemeContext';
4
+ import { api } from '../../api';
5
+
6
+ export function SettingsAppearance() {
7
+ const { theme, setTheme } = useTheme();
8
+ const [saving, setSaving] = useState(false);
9
+ const [success, setSuccess] = useState('');
10
+ const [error, setError] = useState('');
11
+ const [serverTheme, setServerTheme] = useState<ThemeId | null>(null);
12
+
13
+ useEffect(() => {
14
+ api.getAppearance()
15
+ .then(res => {
16
+ const t = res.data.theme as ThemeId;
17
+ setServerTheme(t);
18
+ })
19
+ .catch(() => {
20
+ // If server has no saved theme, treat current theme as the baseline
21
+ setServerTheme(theme);
22
+ });
23
+ }, []);
24
+
25
+ const hasChanges = serverTheme !== null && theme !== serverTheme;
26
+
27
+ const handleSave = async () => {
28
+ setSaving(true);
29
+ setError('');
30
+ setSuccess('');
31
+ try {
32
+ await api.updateAppearance(theme);
33
+ setServerTheme(theme);
34
+ setSuccess('Theme updated successfully');
35
+ } catch (err: unknown) {
36
+ setError(err instanceof Error ? err.message : 'Failed to save theme');
37
+ } finally {
38
+ setSaving(false);
39
+ }
40
+ };
41
+
42
+ return (
43
+ <div className="page">
44
+ <h2>Appearance</h2>
45
+ <p style={{ color: 'var(--text-secondary)', marginBottom: '1.5rem' }}>
46
+ Choose your theme. Changes preview instantly.
47
+ </p>
48
+ <ThemeSelector />
49
+ {hasChanges && (
50
+ <button
51
+ className="btn-primary"
52
+ onClick={handleSave}
53
+ disabled={saving}
54
+ style={{ marginTop: '1.5rem' }}
55
+ >
56
+ {saving ? 'Saving...' : 'Save Theme'}
57
+ </button>
58
+ )}
59
+ {success && <p className="success" style={{ marginTop: '0.75rem' }}>{success}</p>}
60
+ {error && <p className="error" style={{ marginTop: '0.75rem' }}>{error}</p>}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,138 @@
1
+ import { useState } from 'react';
2
+ import { useApi } from '../../hooks/useApi';
3
+ import { api } from '../../api';
4
+
5
+ const AVAILABLE_CHANNELS = [
6
+ { type: 'discord', label: 'Discord', fields: [{ key: 'botToken', label: 'Bot Token', secret: true }] },
7
+ { type: 'telegram', label: 'Telegram', fields: [{ key: 'botToken', label: 'Bot Token', secret: true }] },
8
+ { type: 'slack', label: 'Slack', fields: [{ key: 'botToken', label: 'Bot Token', secret: true }, { key: 'appToken', label: 'App Token', secret: true }] },
9
+ { type: 'twilio', label: 'Twilio', fields: [{ key: 'accountSid', label: 'Account SID', secret: false }, { key: 'authToken', label: 'Auth Token', secret: true }] },
10
+ { type: 'matrix', label: 'Matrix', fields: [{ key: 'accessToken', label: 'Access Token', secret: true }] },
11
+ {
12
+ type: 'email',
13
+ label: 'Email',
14
+ fields: [
15
+ { key: 'imapHost', label: 'IMAP Host', secret: false },
16
+ { key: 'imapPort', label: 'IMAP Port', secret: false },
17
+ { key: 'smtpHost', label: 'SMTP Host', secret: false },
18
+ { key: 'smtpPort', label: 'SMTP Port', secret: false },
19
+ { key: 'email', label: 'Email Address', secret: false },
20
+ { key: 'password', label: 'Password (App Password for Gmail)', secret: true },
21
+ ],
22
+ },
23
+ ];
24
+
25
+ interface ChannelState {
26
+ enabled: boolean;
27
+ credentials: Record<string, string>;
28
+ }
29
+
30
+ export function SettingsChannels() {
31
+ const { data } = useApi(() => api.getChannels(), []);
32
+ const connected = data?.data?.connected ?? [];
33
+ const configured = data?.data?.configured ?? [];
34
+
35
+ const [channelStates, setChannelStates] = useState<Record<string, ChannelState>>({});
36
+ const [saving, setSaving] = useState(false);
37
+ const [success, setSuccess] = useState('');
38
+ const [error, setError] = useState('');
39
+
40
+ const getState = (type: string): ChannelState => {
41
+ if (channelStates[type]) return channelStates[type];
42
+ const conf = configured.find(c => c.type === type);
43
+ return { enabled: conf?.enabled ?? false, credentials: {} };
44
+ };
45
+
46
+ const toggleChannel = (type: string) => {
47
+ const current = getState(type);
48
+ setChannelStates(prev => ({
49
+ ...prev,
50
+ [type]: { ...current, enabled: !current.enabled },
51
+ }));
52
+ };
53
+
54
+ const updateCredential = (type: string, key: string, value: string) => {
55
+ const current = getState(type);
56
+ setChannelStates(prev => ({
57
+ ...prev,
58
+ [type]: { ...current, credentials: { ...current.credentials, [key]: value } },
59
+ }));
60
+ };
61
+
62
+ const handleSave = async () => {
63
+ setSaving(true);
64
+ setError('');
65
+ setSuccess('');
66
+ try {
67
+ const channels = AVAILABLE_CHANNELS
68
+ .filter(ch => getState(ch.type).enabled || Object.values(getState(ch.type).credentials).some(v => v))
69
+ .map(ch => {
70
+ const state = getState(ch.type);
71
+ const creds: Record<string, string> = {};
72
+ for (const [k, v] of Object.entries(state.credentials)) {
73
+ if (v) creds[k] = v;
74
+ }
75
+ return {
76
+ type: ch.type,
77
+ enabled: state.enabled,
78
+ ...(Object.keys(creds).length > 0 ? { credentials: creds } : {}),
79
+ };
80
+ });
81
+ await api.updateChannels(channels);
82
+ setSuccess('Channels updated successfully');
83
+ } catch (err: any) {
84
+ setError(err.message);
85
+ } finally {
86
+ setSaving(false);
87
+ }
88
+ };
89
+
90
+ return (
91
+ <div className="page">
92
+ <h2>Channels</h2>
93
+ <div className="channel-grid">
94
+ {AVAILABLE_CHANNELS.map(ch => {
95
+ const state = getState(ch.type);
96
+ const isConnected = connected.includes(ch.type);
97
+ return (
98
+ <div key={ch.type} className="channel-card">
99
+ <div className="channel-card-header">
100
+ <h3>{ch.label}</h3>
101
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
102
+ {isConnected
103
+ ? <span className="badge badge-green">Connected</span>
104
+ : state.enabled && <span className="badge badge-yellow">Configured</span>
105
+ }
106
+ <div
107
+ className={`toggle${state.enabled ? ' active' : ''}`}
108
+ onClick={() => toggleChannel(ch.type)}
109
+ />
110
+ </div>
111
+ </div>
112
+ {state.enabled && (
113
+ <div className="channel-card-fields">
114
+ {ch.fields.map(f => (
115
+ <div key={f.key}>
116
+ <label>{f.label}</label>
117
+ <input
118
+ type={f.secret ? 'password' : 'text'}
119
+ value={state.credentials[f.key] ?? ''}
120
+ onChange={(e) => updateCredential(ch.type, f.key, e.target.value)}
121
+ placeholder={isConnected ? '(configured)' : `Enter ${f.label.toLowerCase()}`}
122
+ />
123
+ </div>
124
+ ))}
125
+ </div>
126
+ )}
127
+ </div>
128
+ );
129
+ })}
130
+ </div>
131
+ <button className="settings-btn" onClick={handleSave} disabled={saving}>
132
+ {saving ? 'Saving...' : 'Save Changes'}
133
+ </button>
134
+ {success && <div className="settings-success">{success}</div>}
135
+ {error && <div className="error">{error}</div>}
136
+ </div>
137
+ );
138
+ }