diff --git a/@applications/web/src/features/settings/SettingsPanel.tsx b/@applications/web/src/features/settings/SettingsPanel.tsx index bed15eb..7be8ab5 100644 --- a/@applications/web/src/features/settings/SettingsPanel.tsx +++ b/@applications/web/src/features/settings/SettingsPanel.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import styled from '@lilith/ui-styled-components'; import { Tooltip } from '@lilith/ui-feedback'; @@ -6,6 +6,12 @@ import { AnimatePresence, motion } from '@lilith/ui-motion'; import type { VoiceSessionState } from '../voice/VoiceSession'; import type { Settings, SettingsActions } from './useSettings'; +interface PersonaOption { + id: string; + slug: string; + name: string; +} + export interface SettingsPanelProps { open: boolean; onClose: () => void; @@ -13,6 +19,9 @@ export interface SettingsPanelProps { connectionState: VoiceSessionState | 'disconnected'; settings: Settings; onSettings: SettingsActions; + apiBaseUrl: string; + currentPersonaId: string; + onPersonaChange: (personaId: string) => void; } const Overlay = styled(motion.div)` @@ -203,6 +212,41 @@ const VolumeLabel = styled.span` text-align: right; `; +const PersonaGrid = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 20px 12px; +`; + +const PersonaButton = styled.button<{ $active: boolean }>` + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-radius: 10px; + border: 1px solid ${({ $active }) => ($active ? '#553c9a' : '#2d3748')}; + background: ${({ $active }) => ($active ? 'rgba(85,60,154,0.15)' : 'transparent')}; + color: ${({ $active }) => ($active ? '#e2e8f0' : '#a0aec0')}; + font-size: 14px; + cursor: pointer; + text-align: left; + width: 100%; + transition: border-color 150ms ease, background 150ms ease; + + &:active { + background: rgba(85,60,154,0.25); + } +`; + +const PersonaActiveDot = styled.span<{ $active: boolean }>` + width: 8px; + height: 8px; + border-radius: 50%; + background: ${({ $active }) => ($active ? '#553c9a' : '#2d3748')}; + flex-shrink: 0; +`; + function Toggle({ on, onChange, label }: { on: boolean; onChange: (v: boolean) => void; label: string }): ReactElement { return ( ([]); + + useEffect(() => { + if (!open) return; + fetch(`${apiBaseUrl}/personalities`) + .then((r) => r.json() as Promise) + .then(setPersonas) + .catch(() => {/* non-fatal */}); + }, [open, apiBaseUrl]); + + const handlePersonaChange = useCallback((id: string) => { + if (id === currentPersonaId) return; + onPersonaChange(id); + onClose(); + }, [currentPersonaId, onPersonaChange, onClose]); + const handleCopySession = useCallback(() => { void navigator.clipboard.writeText(sessionId); }, [sessionId]); @@ -253,6 +316,27 @@ export function SettingsPanel({ > + {personas.length > 0 && ( +
+ Personality + + {personas.map((p) => { + const isActive = p.id === currentPersonaId || p.slug === currentPersonaId; + return ( + handlePersonaChange(p.id)} + > + + {p.name} + + ); + })} + +
+ )} +
Voice Output (TTS)