/** * PersonalityTuner.jsx — Personality dial controls. * * Reads current personality from /social/personality/state (PersonalityState). * Writes via /personality_node/set_parameters (rcl_interfaces/srv/SetParameters). * * SOUL.md params controlled: * sass_level (int, 0–10) * humor_level (int, 0–10) * verbosity (int, 0–10) — new param, add to SOUL.md + personality_node */ import { useEffect, useState } from 'react'; const PERSONALITY_DIALS = [ { key: 'sass_level', label: 'SASS', param: { name: 'sass_level', type: 'integer' }, min: 0, max: 10, leftLabel: 'Polite', rightLabel: 'Maximum Sass', color: 'accent-orange', barColor: '#f97316', description: '0 = pure politeness, 10 = maximum sass', }, { key: 'humor_level', label: 'HUMOR', param: { name: 'humor_level', type: 'integer' }, min: 0, max: 10, leftLabel: 'Deadpan', rightLabel: 'Comedian', color: 'accent-cyan', barColor: '#06b6d4', description: '0 = deadpan/serious, 10 = comedian', }, { key: 'verbosity', label: 'VERBOSITY', param: { name: 'verbosity', type: 'integer' }, min: 0, max: 10, leftLabel: 'Terse', rightLabel: 'Verbose', color: 'accent-purple', barColor: '#a855f7', description: '0 = brief responses, 10 = elaborate explanations', }, ]; function DialSlider({ dial, value, onChange }) { const pct = ((value - dial.min) / (dial.max - dial.min)) * 100; return (
{dial.label} {value}
onChange(Number(e.target.value))} className={`w-full h-1.5 rounded appearance-none cursor-pointer ${dial.color}`} style={{ background: `linear-gradient(to right, ${dial.barColor} ${pct}%, #1f2937 ${pct}%)`, }} />
{dial.leftLabel} {dial.rightLabel}
); } export function PersonalityTuner({ subscribe, setParam }) { const [values, setValues] = useState({ sass_level: 4, humor_level: 7, verbosity: 5 }); const [applied, setApplied] = useState(null); // last-applied snapshot const [saving, setSaving] = useState(false); const [saveResult, setSaveResult] = useState(null); const [personaInfo, setPersonaInfo] = useState(null); // Sync with live personality state useEffect(() => { const unsub = subscribe( '/social/personality/state', 'saltybot_social_msgs/PersonalityState', (msg) => { setPersonaInfo({ name: msg.persona_name, mood: msg.mood, person: msg.person_id, tier: msg.relationship_tier, greeting: msg.greeting_text, }); } ); return unsub; }, [subscribe]); const isDirty = JSON.stringify(values) !== JSON.stringify(applied); const handleApply = async () => { setSaving(true); setSaveResult(null); try { const params = PERSONALITY_DIALS.map((d) => ({ name: d.param.name, type: d.param.type, value: values[d.key], })); await setParam('personality_node', params); setApplied({ ...values }); setSaveResult({ ok: true, msg: 'Parameters applied to personality_node' }); } catch (e) { setSaveResult({ ok: false, msg: e?.message ?? 'Service call failed' }); } finally { setSaving(false); setTimeout(() => setSaveResult(null), 4000); } }; const handleReset = () => { setValues({ sass_level: 4, humor_level: 7, verbosity: 5 }); }; return (
{/* Current persona info */} {personaInfo && (
ACTIVE PERSONA
Name: {personaInfo.name || '—'}
Mood: {personaInfo.mood || '—'}
{personaInfo.person && ( <>
Talking to: {personaInfo.person}
Tier: {personaInfo.tier || '—'}
)} {personaInfo.greeting && (
"{personaInfo.greeting}"
)}
)} {/* Personality sliders */}
PERSONALITY DIALS
{PERSONALITY_DIALS.map((dial) => ( setValues((prev) => ({ ...prev, [dial.key]: v }))} /> ))} {/* Info */}
Changes call /personality_node/set_parameters. Hot-reload to SOUL.md happens within reload_interval (default 5s).
{/* Action buttons */}
{/* Save result */} {saveResult && (
{saveResult.msg}
)}
); }