@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
|
@@ -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
|
+
}
|