/* ============================================================ page-bot-behavior.jsx — Bot Behavior configuration panel BC4: categorized accordion + search with match highlighting BC6: presets (Strict / Standard / Relaxed + custom) BC7: diff banner with undo after save v2: full visual polish — icons, colors, hierarchy, sticky rail ============================================================ */ /* Re-bind shared primitives. NOTE: the shared ToastCtx exposes `push({msg, kind})`, but every call site in this file uses `toast({message, kind})`. _useToast adapts between the two so all toasts in this panel actually fire (previously `toast` was undefined). */ const _useToast = () => { const ctx = (typeof window.useToast === 'function' ? window.useToast() : null) || {}; const push = typeof ctx.push === 'function' ? ctx.push : (() => {}); return { toast: (o) => push({ kind: (o && o.kind) || 'info', msg: (o && (o.message != null ? o.message : o.msg)) || '', }), }; }; const _StatusPill = window.StatusPill || (({ children, kind }) => React.createElement('span', { className: `pill pill-${kind}` }, children)); /* ── Category meta ─────────────────────────────────────────── */ const CATEGORY_META = { 'Sign-in': { emoji: '📥', color: '#059669', bg: 'rgba(5,150,105,.12)', subtitle: 'How sign-ins are accepted, parsed, and acknowledged.' }, 'Strikes': { emoji: '⚡', color: '#e11d48', bg: 'rgba(225,29,72,.12)', subtitle: 'Enforcement thresholds — when warnings escalate to admin reports.' }, 'PTO': { emoji: '🏖', color: '#0284c7', bg: 'rgba(2,132,199,.12)', subtitle: 'Time-off detection, sick-day handling, and PTO confirmation flow.' }, 'Hours': { emoji: '⏰', color: '#d97706', bg: 'rgba(217,119,6,.12)', subtitle: 'Workday length, late grace windows, burnout caps, and short-day flags.' }, 'Reports': { emoji: '📊', color: '#7c3aed', bg: 'rgba(124,58,237,.12)', subtitle: 'What goes into admin reports and how often they post.' }, 'Voice': { emoji: '🎙', color: '#4f46e5', bg: 'rgba(79,70,229,.12)', subtitle: 'Voice listening windows, wake phrases, and follow-up timing.' }, 'AI Prompts': { emoji: '✨', color: '#db2777', bg: 'rgba(219,39,119,.12)', subtitle: 'System prompts, tone, and persona that shape the AI bot\'s replies.' }, 'Templates': { emoji: '📄', color: '#475569', bg: 'rgba(71,85,105,.12)', subtitle: 'Canned message templates the bot uses for acks, nudges, and DMs.' }, 'Timing': { emoji: '⏰', color: '#d97706', bg: 'rgba(217,119,6,.12)', subtitle: 'When the bot checks in, pings, or follows up.' }, 'Enforcement': { emoji: '⚖️', color: '#e11d48', bg: 'rgba(225,29,72,.12)', subtitle: 'How strict the bot is when format or sign-in rules are missed.' }, 'Formatting': { emoji: '📝', color: '#0284c7', bg: 'rgba(2,132,199,.12)', subtitle: 'How messages are rendered — bullets, headers, emoji density.' }, 'Tone': { emoji: '💬', color: '#db2777', bg: 'rgba(219,39,119,.12)', subtitle: 'How friendly, formal, or terse the bot sounds.' }, 'Escalation': { emoji: '🚨', color: '#e11d48', bg: 'rgba(225,29,72,.12)', subtitle: 'When and how the bot escalates to admins or on-call.' }, }; function getCatMeta(cat) { return CATEGORY_META[cat] || { emoji: '⚙', color: 'var(--accent)', bg: 'color-mix(in srgb, var(--accent) 12%, transparent)', subtitle: 'Settings in this category.', }; } /* ── Helpers ─────────────────────────────────────────────── */ function highlight(text, query) { if (!query || !text) return text; const idx = text.toLowerCase().indexOf(query.toLowerCase()); if (idx === -1) return text; return ( {text.slice(0, idx)} {text.slice(idx, idx + query.length)} {text.slice(idx + query.length)} ); } function matchesSetting(entry, q) { if (!q) return true; const lower = q.toLowerCase(); return ( entry.key.toLowerCase().includes(lower) || (entry.label || '').toLowerCase().includes(lower) || (entry.help || '').toLowerCase().includes(lower) || (entry.category || '').toLowerCase().includes(lower) ); } function coerceValue(value, type) { if (type === 'bool') return Boolean(value); if (type === 'int') return parseInt(value, 10) || 0; if (type === 'float') return parseFloat(value) || 0; if (type === 'string_list') { if (Array.isArray(value)) return value; return value ? value.split('\n').map(s => s.trim()).filter(Boolean) : []; } return value; } function displayValue(value, type) { if (type === 'string_list') { if (Array.isArray(value)) return value.join('\n'); return value || ''; } if (value === null || value === undefined) return ''; return String(value); } /* ── Tag editor for string_list ────────────────────────────── */ function TagEditor({ value, onChange, disabled }) { const items = Array.isArray(value) ? value : (value ? String(value).split('\n').filter(Boolean) : []); const [draft, setDraft] = React.useState(''); function addTag() { const t = draft.trim(); if (!t || items.includes(t)) { setDraft(''); return; } onChange([...items, t]); setDraft(''); } function removeTag(i) { onChange(items.filter((_, idx) => idx !== i)); } return (