Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
155 lines
5.8 KiB
JavaScript
155 lines
5.8 KiB
JavaScript
/**
|
|
* PersonasSection — user persona list + create/edit
|
|
*/
|
|
const { html } = window;
|
|
const { useState, useEffect, useCallback } = hooks;
|
|
|
|
function esc(s) { return s == null ? '' : String(s); }
|
|
|
|
export function PersonasSection() {
|
|
const [personas, setPersonas] = useState(null);
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editId, setEditId] = useState(null);
|
|
const [form, setForm] = useState({ name: '', description: '', system_prompt: '' });
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
const resp = await sw.api.personas.list();
|
|
setPersonas(resp?.data || resp || []);
|
|
} catch (e) {
|
|
setPersonas([]);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
const openAdd = useCallback(() => {
|
|
setEditId(null);
|
|
setForm({ name: '', description: '', system_prompt: '' });
|
|
setShowForm(true);
|
|
}, []);
|
|
|
|
const openEdit = useCallback((p) => {
|
|
setEditId(p.id);
|
|
setForm({ name: p.name || '', description: p.description || '', system_prompt: p.system_prompt || '' });
|
|
setShowForm(true);
|
|
}, []);
|
|
|
|
const cancel = useCallback(() => {
|
|
setShowForm(false);
|
|
setEditId(null);
|
|
}, []);
|
|
|
|
const save = useCallback(async () => {
|
|
if (!form.name.trim()) {
|
|
sw.emit('toast', { message: 'Name is required', variant: 'error' });
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
if (editId) {
|
|
await sw.api.personas.update(editId, form);
|
|
sw.emit('toast', { message: 'Persona updated', variant: 'success' });
|
|
} else {
|
|
await sw.api.personas.create(form);
|
|
sw.emit('toast', { message: 'Persona created', variant: 'success' });
|
|
}
|
|
setShowForm(false);
|
|
setEditId(null);
|
|
load();
|
|
} catch (e) {
|
|
sw.emit('toast', { message: e.message, variant: 'error' });
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [form, editId, load]);
|
|
|
|
const del = useCallback(async (p) => {
|
|
const ok = await sw.confirm(`Delete persona "${p.name}"?`);
|
|
if (!ok) return;
|
|
try {
|
|
await sw.api.personas.del(p.id);
|
|
sw.emit('toast', { message: 'Persona deleted', variant: 'success' });
|
|
load();
|
|
} catch (e) {
|
|
sw.emit('toast', { message: e.message, variant: 'error' });
|
|
}
|
|
}, [load]);
|
|
|
|
const setField = useCallback((key, val) => {
|
|
setForm(f => ({ ...f, [key]: val }));
|
|
}, []);
|
|
|
|
if (personas === null) {
|
|
return html`<div class="settings-placeholder">Loading personas\u2026</div>`;
|
|
}
|
|
|
|
return html`
|
|
<div style="margin-bottom:12px;">
|
|
<button class="btn-md btn-primary" onClick=${openAdd}>+ New Persona</button>
|
|
</div>
|
|
|
|
${showForm && html`
|
|
<div class="settings-section" style="margin-bottom:16px;">
|
|
<h3>${editId ? 'Edit Persona' : 'New Persona'}</h3>
|
|
<div class="form-group">
|
|
<label>Name</label>
|
|
<input type="text" value=${form.name}
|
|
onInput=${e => setField('name', e.target.value)} />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Description</label>
|
|
<input type="text" value=${form.description}
|
|
onInput=${e => setField('description', e.target.value)} />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>System Prompt</label>
|
|
<textarea rows="4" value=${form.system_prompt}
|
|
onInput=${e => setField('system_prompt', e.target.value)}
|
|
style="max-width:100%;" />
|
|
</div>
|
|
<div style="display:flex;gap:8px;margin-top:12px;">
|
|
<button class="btn-md btn-primary" disabled=${saving}
|
|
onClick=${save}>
|
|
${saving ? 'Saving\u2026' : editId ? 'Update' : 'Create'}
|
|
</button>
|
|
<button class="btn-md btn-secondary" onClick=${cancel}>Cancel</button>
|
|
</div>
|
|
</div>
|
|
`}
|
|
|
|
${!personas.length && !showForm && html`
|
|
<div class="empty-hint">No personas configured.</div>
|
|
`}
|
|
|
|
${personas.length > 0 && html`
|
|
<div>
|
|
${personas.map(p => html`
|
|
<div class="settings-section" style="margin-bottom:12px;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
<div>
|
|
<strong style="font-size:14px;">${esc(p.name)}</strong>
|
|
${p.description && html`
|
|
<div style="font-size:12px;color:var(--text-2);margin-top:2px;">
|
|
${esc(p.description)}
|
|
</div>
|
|
`}
|
|
</div>
|
|
<div style="display:flex;gap:4px;">
|
|
<button class="icon-btn" title="Edit" onClick=${() => openEdit(p)}>
|
|
\u{270F}\u{FE0F}
|
|
</button>
|
|
<button class="icon-btn icon-btn-danger" title="Delete"
|
|
onClick=${() => del(p)}>
|
|
\u{1F5D1}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
`}
|
|
`;
|
|
}
|