diff --git a/public/style.css b/public/style.css index d19123b..438d91d 100644 --- a/public/style.css +++ b/public/style.css @@ -778,3 +778,7 @@ body.drawer-open #scrim { opacity: 1; pointer-events: auto; } font-family:var(--font-ui);font-size:11px;letter-spacing:.12em;text-transform:uppercase;background:#0b0810;border-top:1px solid var(--border)} .dross-collapse:hover{color:var(--dross-glow)} .dross-collapse .grip{width:42px;height:4px;border-radius:3px;background:var(--border)} +.dross-pick{display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap} +.dross-avopt{display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 14px;border:1px solid var(--border);border-radius:12px;background:var(--panel);color:var(--muted);cursor:pointer;font-size:11px} +.dross-avopt.on{border-color:var(--dross);color:var(--dross-glow);background:var(--dross-soft)} +.dross-persona{width:100%;background:#0d0d13;border:1px solid var(--border);border-radius:8px;padding:10px;color:var(--text);font-family:var(--font-mono);font-size:12px;resize:vertical;margin:10px 0} diff --git a/public/views/settings.js b/public/views/settings.js index 7d64488..73b3d8b 100644 --- a/public/views/settings.js +++ b/public/views/settings.js @@ -3,6 +3,7 @@ import { el, mount } from '../dom.js'; import { api } from '../api.js'; import { iconSetsPanel } from './icon_sets_panel.js'; import { THEME_VARS, PRESETS, applyTheme, clearTheme, saveTheme, currentTheme, effectiveHex, toHex6 } from '../theme.js'; +import { drossAvatar } from '../components/dross_avatar.js'; // Theming — colour pickers for the palette, live-preview on input, presets + // reset. Persists to /api/theme (app_settings); applied app-wide on next boot. @@ -148,6 +149,44 @@ async function renderAgents(c) { } } +function drossBody() { + const out = el('span', { class: 'muted', style: { marginLeft: '8px' } }); + let cur = { avatar: 'soft-eye', accent: '#a86adf', persona: '', voiceMode: 'review' }; + const avatarRow = el('div', { class: 'dross-pick' }); + const accent = el('input', { type: 'color', value: cur.accent }); + const persona = el('textarea', { class: 'dross-persona', rows: 6, placeholder: "Dross's system prompt…" }); + const mode = el('select', { class: 'pm-input', style: { maxWidth: '200px' } }, + el('option', { value: 'review' }, 'Voice: review then send'), + el('option', { value: 'handsfree' }, 'Voice: hands-free (Phase 2)'), + el('option', { value: 'action' }, 'Voice: interpret to action (later)')); + + function paintAvatars() { + mount(avatarRow, ['soft-eye', 'wisp', 'motes'].map(v => { + const card = el('button', { class: 'dross-avopt' + (cur.avatar === v ? ' on' : ''), title: v }, + drossAvatar(v, 48), el('span', {}, v)); + card.style.setProperty('--dross', cur.accent); + card.onclick = () => { cur.avatar = v; paintAvatars(); }; + return card; + })); + } + (async () => { + try { cur = { ...cur, ...(await api.get('/api/dross/settings')) }; } catch {} + accent.value = cur.accent; persona.value = cur.persona; mode.value = cur.voiceMode; paintAvatars(); + })(); + accent.addEventListener('input', () => { cur.accent = accent.value; paintAvatars(); }); + + const save = el('button', { class: 'primary' }, 'Save'); + save.onclick = async () => { + try { + await api.put('/api/dross/settings', { avatar: cur.avatar, accent: accent.value, persona: persona.value, voiceMode: mode.value }); + window.dispatchEvent(new CustomEvent('dross-settings-changed')); + out.textContent = 'Saved.'; + } catch { out.textContent = 'Save failed'; } + }; + return el('div', { class: 'settings-body' }, avatarRow, el('label', { class: 'st-lbl' }, 'Accent', accent), + persona, el('div', { class: 'theme-actions' }, mode, save, out)); +} + export async function render(main) { const tokensBody = el('div', { class: 'settings-body' }); const agentsBody = el('div', { class: 'settings-body' }); @@ -172,6 +211,7 @@ export async function render(main) { mount(main, el('h1', { class: 'view-h1' }, '◆ Settings'), section('Theming', 'Recolour the interface. Pick a colour to preview it live, choose a preset, then Save to persist. Reset returns to the default Blackflame palette.', themingBody()), + section('Dross', "Your companion's look and voice. Avatar, accent colour, his personality (system prompt), and how voice clips behave.", drossBody()), section('API Tokens', 'Bearer tokens for agents (e.g. the MCP external-research agent). The secret is shown once at creation.', tokensBody), section('Agents', 'Seeded Cradle agents and what each is allowed to do.', agentsBody), section('Icon Sets', 'Upload or delete custom icon packs for device and service icons.', iconSetsWrap),