@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,61 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { api } from '../../api';
3
+
4
+ export function SettingsIdentity() {
5
+ const [name, setName] = useState('');
6
+ const [pronouns, setPronouns] = useState('');
7
+ const [saving, setSaving] = useState(false);
8
+ const [success, setSuccess] = useState('');
9
+ const [error, setError] = useState('');
10
+
11
+ useEffect(() => {
12
+ api.getIdentity()
13
+ .then(res => {
14
+ setName(res.data.name);
15
+ setPronouns(res.data.pronouns);
16
+ })
17
+ .catch(err => setError(err.message));
18
+ }, []);
19
+
20
+ const handleSave = async (e: React.FormEvent) => {
21
+ e.preventDefault();
22
+ setSaving(true);
23
+ setError('');
24
+ setSuccess('');
25
+ try {
26
+ await api.updateIdentity(name, pronouns);
27
+ setSuccess('Identity updated successfully');
28
+ } catch (err: any) {
29
+ setError(err.message);
30
+ } finally {
31
+ setSaving(false);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="page">
37
+ <h2>Identity</h2>
38
+ <form className="settings-form" onSubmit={handleSave}>
39
+ <label>Agent Name</label>
40
+ <input
41
+ type="text"
42
+ value={name}
43
+ onChange={(e) => setName(e.target.value)}
44
+ placeholder="Auxiora"
45
+ />
46
+ <label>Pronouns</label>
47
+ <input
48
+ type="text"
49
+ value={pronouns}
50
+ onChange={(e) => setPronouns(e.target.value)}
51
+ placeholder="they/them"
52
+ />
53
+ <button className="settings-btn" type="submit" disabled={saving || !name}>
54
+ {saving ? 'Saving...' : 'Save'}
55
+ </button>
56
+ {success && <div className="settings-success">{success}</div>}
57
+ {error && <div className="error">{error}</div>}
58
+ </form>
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,54 @@
1
+ import { useState } from 'react';
2
+ import { useApi } from '../../hooks/useApi';
3
+ import { api } from '../../api';
4
+
5
+ export function SettingsPersonality() {
6
+ const { data, loading } = useApi(() => api.getTemplates(), []);
7
+ const [selected, setSelected] = useState('');
8
+ const [saving, setSaving] = useState(false);
9
+ const [success, setSuccess] = useState('');
10
+ const [error, setError] = useState('');
11
+
12
+ const templates = data?.data ?? [];
13
+
14
+ const handleSave = async () => {
15
+ if (!selected) return;
16
+ setSaving(true);
17
+ setError('');
18
+ setSuccess('');
19
+ try {
20
+ await api.updatePersonality(selected);
21
+ setSuccess('Personality template applied');
22
+ } catch (err: any) {
23
+ setError(err.message);
24
+ } finally {
25
+ setSaving(false);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <div className="page">
31
+ <h2>Personality</h2>
32
+ {loading && <p>Loading templates...</p>}
33
+ <div className="template-grid">
34
+ {templates.map((t: any) => (
35
+ <div
36
+ key={t.id}
37
+ className={`template-card${selected === t.id ? ' selected' : ''}`}
38
+ onClick={() => setSelected(t.id)}
39
+ >
40
+ <h3>{t.name}</h3>
41
+ <p>{t.description}</p>
42
+ </div>
43
+ ))}
44
+ </div>
45
+ {templates.length > 0 && (
46
+ <button className="settings-btn" onClick={handleSave} disabled={saving || !selected}>
47
+ {saving ? 'Applying...' : 'Apply Template'}
48
+ </button>
49
+ )}
50
+ {success && <div className="settings-success">{success}</div>}
51
+ {error && <div className="error">{error}</div>}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,577 @@
1
+ import { useState, useEffect, useCallback, type KeyboardEvent } from 'react';
2
+ import { api } from '../../api.js';
3
+ import { useApi } from '../../hooks/useApi.js';
4
+
5
+ /* ---------- Types ---------- */
6
+
7
+ interface ToneValues {
8
+ warmth: number;
9
+ directness: number;
10
+ humor: number;
11
+ formality: number;
12
+ }
13
+
14
+ interface Boundaries {
15
+ neverJokeAbout: string[];
16
+ neverAdviseOn: string[];
17
+ }
18
+
19
+ interface PersonalityData {
20
+ name: string;
21
+ pronouns: string;
22
+ avatar: string | null;
23
+ vibe: string;
24
+ tone: ToneValues;
25
+ errorStyle: string;
26
+ expertise: string[];
27
+ catchphrases: Record<string, string>;
28
+ boundaries: Boundaries;
29
+ customInstructions: string;
30
+ soulContent: string | null;
31
+ activeTemplate: string | null;
32
+ }
33
+
34
+ const ERROR_STYLES = [
35
+ 'professional',
36
+ 'apologetic',
37
+ 'matter_of_fact',
38
+ 'self_deprecating',
39
+ 'gentle',
40
+ 'detailed',
41
+ 'encouraging',
42
+ 'terse',
43
+ 'educational',
44
+ ] as const;
45
+
46
+ const PRONOUN_OPTIONS = ['she/her', 'he/him', 'they/them', 'it/its'] as const;
47
+
48
+ const CATCHPHRASE_KEYS = ['greeting', 'farewell', 'thinking', 'success', 'error'] as const;
49
+
50
+ const TONE_CONFIG: Array<{
51
+ key: keyof ToneValues;
52
+ label: string;
53
+ low: string;
54
+ high: string;
55
+ }> = [
56
+ { key: 'warmth', label: 'Warmth', low: 'Reserved', high: 'Warm' },
57
+ { key: 'directness', label: 'Directness', low: 'Gentle', high: 'Direct' },
58
+ { key: 'humor', label: 'Humor', low: 'Serious', high: 'Playful' },
59
+ { key: 'formality', label: 'Formality', low: 'Casual', high: 'Formal' },
60
+ ];
61
+
62
+ /* ---------- Inline helper components ---------- */
63
+
64
+ function TagInput({
65
+ tags,
66
+ onChange,
67
+ placeholder,
68
+ }: {
69
+ tags: string[];
70
+ onChange: (tags: string[]) => void;
71
+ placeholder?: string;
72
+ }) {
73
+ const [input, setInput] = useState('');
74
+
75
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
76
+ if (e.key === 'Enter') {
77
+ e.preventDefault();
78
+ const value = input.trim();
79
+ if (value && !tags.includes(value)) {
80
+ onChange([...tags, value]);
81
+ }
82
+ setInput('');
83
+ } else if (e.key === 'Backspace' && input === '' && tags.length > 0) {
84
+ onChange(tags.slice(0, -1));
85
+ }
86
+ };
87
+
88
+ const removeTag = (index: number) => {
89
+ onChange(tags.filter((_, i) => i !== index));
90
+ };
91
+
92
+ return (
93
+ <div className="tag-input-container">
94
+ <div className="tag-list">
95
+ {tags.map((tag, i) => (
96
+ <span key={i} className="tag">
97
+ {tag}
98
+ <button
99
+ type="button"
100
+ className="tag-remove"
101
+ onClick={() => removeTag(i)}
102
+ aria-label={`Remove ${tag}`}
103
+ >
104
+ x
105
+ </button>
106
+ </span>
107
+ ))}
108
+ </div>
109
+ <input
110
+ className="tag-input"
111
+ type="text"
112
+ value={input}
113
+ onChange={(e) => setInput(e.target.value)}
114
+ onKeyDown={handleKeyDown}
115
+ placeholder={placeholder ?? 'Type and press Enter'}
116
+ />
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function ToneSlider({
122
+ label,
123
+ low,
124
+ high,
125
+ value,
126
+ onChange,
127
+ }: {
128
+ label: string;
129
+ low: string;
130
+ high: string;
131
+ value: number;
132
+ onChange: (v: number) => void;
133
+ }) {
134
+ const pct = Math.round(value * 100);
135
+
136
+ return (
137
+ <div className="tone-slider">
138
+ <div className="tone-slider-header">
139
+ <span className="tone-label">{label}</span>
140
+ <span className="tone-value">{pct}%</span>
141
+ </div>
142
+ <div className="tone-slider-row">
143
+ <span className="tone-extreme">{low}</span>
144
+ <input
145
+ className="slider"
146
+ type="range"
147
+ min={0}
148
+ max={100}
149
+ value={pct}
150
+ onChange={(e) => onChange(Number(e.target.value) / 100)}
151
+ />
152
+ <span className="tone-extreme">{high}</span>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ /* ---------- Main component ---------- */
159
+
160
+ const DEFAULT_DATA: PersonalityData = {
161
+ name: '',
162
+ pronouns: 'they/them',
163
+ avatar: null,
164
+ vibe: '',
165
+ tone: { warmth: 0.5, directness: 0.5, humor: 0.5, formality: 0.5 },
166
+ errorStyle: 'professional',
167
+ expertise: [],
168
+ catchphrases: { greeting: '', farewell: '', thinking: '', success: '', error: '' },
169
+ boundaries: { neverJokeAbout: [], neverAdviseOn: [] },
170
+ customInstructions: '',
171
+ soulContent: null,
172
+ activeTemplate: null,
173
+ };
174
+
175
+ export function PersonalityEditor() {
176
+ /* --- Server data (snapshot of last saved state) --- */
177
+ const [serverData, setServerData] = useState<PersonalityData | null>(null);
178
+
179
+ /* --- Editable form state --- */
180
+ const [form, setForm] = useState<PersonalityData>(DEFAULT_DATA);
181
+ const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
182
+ const [showSoul, setShowSoul] = useState(false);
183
+
184
+ /* --- UI state --- */
185
+ const [saving, setSaving] = useState(false);
186
+ const [success, setSuccess] = useState('');
187
+ const [error, setError] = useState('');
188
+ const [loadError, setLoadError] = useState('');
189
+
190
+ /* --- Load personality data --- */
191
+ useEffect(() => {
192
+ api.getPersonalityFull()
193
+ .then((res) => {
194
+ const d = res.data;
195
+ const loaded: PersonalityData = {
196
+ name: d.name ?? '',
197
+ pronouns: d.pronouns ?? 'they/them',
198
+ avatar: d.avatar ?? null,
199
+ vibe: d.vibe ?? '',
200
+ tone: d.tone ?? { warmth: 0.5, directness: 0.5, humor: 0.5, formality: 0.5 },
201
+ errorStyle: d.errorStyle ?? 'professional',
202
+ expertise: d.expertise ?? [],
203
+ catchphrases: {
204
+ greeting: d.catchphrases?.greeting ?? '',
205
+ farewell: d.catchphrases?.farewell ?? '',
206
+ thinking: d.catchphrases?.thinking ?? '',
207
+ success: d.catchphrases?.success ?? '',
208
+ error: d.catchphrases?.error ?? '',
209
+ },
210
+ boundaries: {
211
+ neverJokeAbout: d.boundaries?.neverJokeAbout ?? [],
212
+ neverAdviseOn: d.boundaries?.neverAdviseOn ?? [],
213
+ },
214
+ customInstructions: d.customInstructions ?? '',
215
+ soulContent: d.soulContent ?? null,
216
+ activeTemplate: d.activeTemplate ?? null,
217
+ };
218
+ setForm(loaded);
219
+ setServerData(loaded);
220
+ setSelectedTemplate(loaded.activeTemplate);
221
+ })
222
+ .catch((err) => setLoadError(err.message));
223
+ }, []);
224
+
225
+ /* --- Templates --- */
226
+ const { data: templatesRes, loading: templatesLoading } = useApi(
227
+ () => api.getTemplates(),
228
+ [],
229
+ );
230
+ const templates = templatesRes?.data ?? [];
231
+
232
+ /* --- Dirty detection --- */
233
+ const isDirty = useCallback((): boolean => {
234
+ if (!serverData) return false;
235
+ if (selectedTemplate !== serverData.activeTemplate) return true;
236
+ return JSON.stringify(form) !== JSON.stringify(serverData);
237
+ }, [form, serverData, selectedTemplate]);
238
+
239
+ const dirty = isDirty();
240
+
241
+ /* --- Field updaters --- */
242
+ const updateField = <K extends keyof PersonalityData>(key: K, value: PersonalityData[K]) => {
243
+ setForm((prev) => ({ ...prev, [key]: value }));
244
+ };
245
+
246
+ const updateTone = (key: keyof ToneValues, value: number) => {
247
+ setForm((prev) => ({ ...prev, tone: { ...prev.tone, [key]: value } }));
248
+ };
249
+
250
+ const updateCatchphrase = (key: string, value: string) => {
251
+ setForm((prev) => ({
252
+ ...prev,
253
+ catchphrases: { ...prev.catchphrases, [key]: value },
254
+ }));
255
+ };
256
+
257
+ const updateBoundary = (key: keyof Boundaries, value: string[]) => {
258
+ setForm((prev) => ({
259
+ ...prev,
260
+ boundaries: { ...prev.boundaries, [key]: value },
261
+ }));
262
+ };
263
+
264
+ /* --- Save handler --- */
265
+ const handleSave = async () => {
266
+ setSaving(true);
267
+ setError('');
268
+ setSuccess('');
269
+ try {
270
+ const payload: Record<string, unknown> = {
271
+ name: form.name,
272
+ pronouns: form.pronouns,
273
+ avatar: form.avatar,
274
+ vibe: form.vibe,
275
+ tone: form.tone,
276
+ errorStyle: form.errorStyle,
277
+ expertise: form.expertise,
278
+ catchphrases: form.catchphrases,
279
+ boundaries: form.boundaries,
280
+ customInstructions: form.customInstructions,
281
+ soulContent: form.soulContent,
282
+ };
283
+ if (selectedTemplate !== serverData?.activeTemplate) {
284
+ payload.activeTemplate = selectedTemplate;
285
+ }
286
+ await api.updatePersonalityFull(payload);
287
+ const saved: PersonalityData = {
288
+ ...form,
289
+ activeTemplate: selectedTemplate,
290
+ };
291
+ setServerData(saved);
292
+ setSuccess('Personality settings saved successfully');
293
+ } catch (err: unknown) {
294
+ setError(err instanceof Error ? err.message : 'Failed to save');
295
+ } finally {
296
+ setSaving(false);
297
+ }
298
+ };
299
+
300
+ /* --- Avatar preview validity --- */
301
+ const [avatarValid, setAvatarValid] = useState(false);
302
+ useEffect(() => {
303
+ if (!form.avatar) {
304
+ setAvatarValid(false);
305
+ return;
306
+ }
307
+ try {
308
+ new URL(form.avatar);
309
+ setAvatarValid(true);
310
+ } catch {
311
+ setAvatarValid(false);
312
+ }
313
+ }, [form.avatar]);
314
+
315
+ /* --- Render --- */
316
+
317
+ if (loadError) {
318
+ return (
319
+ <div className="page">
320
+ <div className="error">Failed to load personality data: {loadError}</div>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ if (!serverData) {
326
+ return (
327
+ <div className="page">
328
+ <p>Loading personality settings...</p>
329
+ </div>
330
+ );
331
+ }
332
+
333
+ const saveButton = (
334
+ <button
335
+ className="settings-btn"
336
+ onClick={handleSave}
337
+ disabled={saving || !dirty}
338
+ >
339
+ {saving ? 'Saving...' : 'Save Changes'}
340
+ </button>
341
+ );
342
+
343
+ return (
344
+ <div className="page">
345
+ {/* Header */}
346
+ <div className="page-header">
347
+ <h2>Personality Editor</h2>
348
+ {dirty && saveButton}
349
+ </div>
350
+
351
+ {success && <div className="settings-success">{success}</div>}
352
+ {error && <div className="error">{error}</div>}
353
+
354
+ <div className="settings-form">
355
+ {/* Section 1: Identity */}
356
+ <div className="editor-section">
357
+ <h3>Identity</h3>
358
+ <div className="form-grid">
359
+ <div className="form-field">
360
+ <label>Name</label>
361
+ <input
362
+ type="text"
363
+ value={form.name}
364
+ onChange={(e) => updateField('name', e.target.value)}
365
+ placeholder="Agent name"
366
+ />
367
+ </div>
368
+
369
+ <div className="form-field">
370
+ <label>Pronouns</label>
371
+ <select
372
+ value={form.pronouns}
373
+ onChange={(e) => updateField('pronouns', e.target.value)}
374
+ >
375
+ {PRONOUN_OPTIONS.map((p) => (
376
+ <option key={p} value={p}>{p}</option>
377
+ ))}
378
+ </select>
379
+ </div>
380
+
381
+ <div className="form-field full-width">
382
+ <label>Vibe</label>
383
+ <input
384
+ type="text"
385
+ value={form.vibe}
386
+ onChange={(e) => {
387
+ if (e.target.value.length <= 200) {
388
+ updateField('vibe', e.target.value);
389
+ }
390
+ }}
391
+ placeholder="A short description of the assistant's personality vibe"
392
+ maxLength={200}
393
+ />
394
+ <span className="field-hint">{form.vibe.length}/200 characters</span>
395
+ </div>
396
+
397
+ <div className="form-field full-width">
398
+ <label>Avatar URL</label>
399
+ <input
400
+ type="text"
401
+ value={form.avatar ?? ''}
402
+ onChange={(e) => updateField('avatar', e.target.value || null)}
403
+ placeholder="https://example.com/avatar.png"
404
+ />
405
+ {avatarValid && form.avatar && (
406
+ <img
407
+ src={form.avatar}
408
+ alt="Avatar preview"
409
+ style={{ width: 64, height: 64, borderRadius: 8, marginTop: 8, objectFit: 'cover' }}
410
+ onError={() => setAvatarValid(false)}
411
+ />
412
+ )}
413
+ </div>
414
+ </div>
415
+ </div>
416
+
417
+ {/* Section 2: Tone */}
418
+ <div className="editor-section">
419
+ <h3>Tone</h3>
420
+ <div className="tone-sliders">
421
+ {TONE_CONFIG.map((tc) => (
422
+ <ToneSlider
423
+ key={tc.key}
424
+ label={tc.label}
425
+ low={tc.low}
426
+ high={tc.high}
427
+ value={form.tone[tc.key]}
428
+ onChange={(v) => updateTone(tc.key, v)}
429
+ />
430
+ ))}
431
+ </div>
432
+ </div>
433
+
434
+ {/* Section 3: Quick Start Templates */}
435
+ <div className="editor-section">
436
+ <h3>Quick Start Templates</h3>
437
+ {templatesLoading && <p>Loading templates...</p>}
438
+ <div className="template-grid">
439
+ {templates.map((t) => (
440
+ <div
441
+ key={t.id}
442
+ className={`template-card${selectedTemplate === t.id ? ' selected' : ''}`}
443
+ onClick={() => setSelectedTemplate(t.id)}
444
+ >
445
+ <h4>{t.name}</h4>
446
+ <p>{t.description}</p>
447
+ </div>
448
+ ))}
449
+ </div>
450
+ </div>
451
+
452
+ {/* Section 4: Behavior */}
453
+ <div className="editor-section">
454
+ <h3>Behavior</h3>
455
+ <div className="form-grid">
456
+ <div className="form-field">
457
+ <label>Error Style</label>
458
+ <select
459
+ value={form.errorStyle}
460
+ onChange={(e) => updateField('errorStyle', e.target.value)}
461
+ >
462
+ {ERROR_STYLES.map((s) => (
463
+ <option key={s} value={s}>
464
+ {s.replace(/_/g, ' ')}
465
+ </option>
466
+ ))}
467
+ </select>
468
+ </div>
469
+
470
+ <div className="form-field full-width">
471
+ <label>Expertise Areas</label>
472
+ <TagInput
473
+ tags={form.expertise}
474
+ onChange={(tags) => updateField('expertise', tags)}
475
+ placeholder="Add expertise area and press Enter"
476
+ />
477
+ </div>
478
+
479
+ <div className="form-field full-width">
480
+ <label>Catchphrases</label>
481
+ <div className="catchphrase-grid">
482
+ {CATCHPHRASE_KEYS.map((key) => (
483
+ <div key={key} className="catchphrase-row">
484
+ <span className="catchphrase-label">{key}</span>
485
+ <input
486
+ type="text"
487
+ value={form.catchphrases[key] ?? ''}
488
+ onChange={(e) => updateCatchphrase(key, e.target.value)}
489
+ placeholder={`${key} phrase`}
490
+ />
491
+ </div>
492
+ ))}
493
+ </div>
494
+ </div>
495
+
496
+ <div className="form-field full-width">
497
+ <label>Never Joke About</label>
498
+ <TagInput
499
+ tags={form.boundaries.neverJokeAbout}
500
+ onChange={(tags) => updateBoundary('neverJokeAbout', tags)}
501
+ placeholder="Add topic and press Enter"
502
+ />
503
+ </div>
504
+
505
+ <div className="form-field full-width">
506
+ <label>Never Advise On</label>
507
+ <TagInput
508
+ tags={form.boundaries.neverAdviseOn}
509
+ onChange={(tags) => updateBoundary('neverAdviseOn', tags)}
510
+ placeholder="Add topic and press Enter"
511
+ />
512
+ </div>
513
+ </div>
514
+ </div>
515
+
516
+ {/* Section 5: Custom Instructions */}
517
+ <div className="editor-section">
518
+ <h3>Custom Instructions</h3>
519
+ <div className="form-field full-width">
520
+ <textarea
521
+ className="custom-instructions-textarea"
522
+ rows={6}
523
+ value={form.customInstructions}
524
+ onChange={(e) => {
525
+ if (e.target.value.length <= 4000) {
526
+ updateField('customInstructions', e.target.value);
527
+ }
528
+ }}
529
+ maxLength={4000}
530
+ placeholder="Add any custom instructions for the assistant..."
531
+ />
532
+ <span className="field-hint">
533
+ {form.customInstructions.length}/4000 characters. These instructions are always
534
+ included in the system prompt and guide the assistant's overall behavior.
535
+ </span>
536
+ </div>
537
+ </div>
538
+
539
+ {/* Section 6: Advanced — Raw SOUL.md */}
540
+ <div className="editor-section">
541
+ <h3>
542
+ Advanced
543
+ <button
544
+ type="button"
545
+ className="section-toggle"
546
+ onClick={() => setShowSoul(!showSoul)}
547
+ >
548
+ {showSoul ? 'Hide' : 'Show'} Raw SOUL.md
549
+ </button>
550
+ </h3>
551
+ {showSoul && (
552
+ <>
553
+ <p className="field-hint" style={{ marginBottom: '0.5rem' }}>
554
+ Warning: selecting a template will overwrite manual edits to this content.
555
+ </p>
556
+ <textarea
557
+ className="soul-editor-textarea"
558
+ rows={16}
559
+ value={form.soulContent ?? ''}
560
+ onChange={(e) => updateField('soulContent', e.target.value || null)}
561
+ placeholder="Raw SOUL.md content..."
562
+ style={{ fontFamily: 'monospace' }}
563
+ />
564
+ </>
565
+ )}
566
+ </div>
567
+
568
+ {/* Sticky footer */}
569
+ {dirty && (
570
+ <div className="editor-footer">
571
+ {saveButton}
572
+ </div>
573
+ )}
574
+ </div>{/* end settings-form */}
575
+ </div>
576
+ );
577
+ }