/**
* 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}
)}
);
}