← back to Dw Boardroom Governance
frontend/src/components/AutonomyControl.tsx
99 lines
import { useState, useEffect } from 'react';
import { api } from '../api';
export default function AutonomyControl() {
const [config, setConfig] = useState<Record<string, any>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
api.getConfig().then(c => { setConfig(c); setLoading(false); }).catch(() => setLoading(false));
}, []);
async function updateSetting(key: string, value: any) {
const previous = config;
const updated = { ...config, [key]: value };
setConfig(updated);
try {
await api.updateConfig({ [key]: value });
} catch (err: any) {
console.error('[AutonomyControl] updateConfig failed:', err.message);
setConfig(previous); // revert optimistic update on failure
}
}
const mode = (typeof config.governance_mode === 'string' ? config.governance_mode : 'supervised').replace(/"/g, '');
const autonomyDefault = Number(config.autonomy_default) || 1;
const threshold = Number(config.escalation_threshold_hours) || 12;
if (loading) return <div style={styles.card}><div style={styles.empty}>Loading config...</div></div>;
return (
<div style={styles.card}>
<h3 style={styles.cardTitle}>Governance Controls</h3>
{/* Mode toggle */}
<div style={styles.setting}>
<div style={styles.settingLabel}>Governance Mode</div>
<div style={styles.toggleRow}>
<button
style={{ ...styles.modeBtn, ...(mode === 'supervised' ? styles.modeBtnActive : {}) }}
onClick={() => updateSetting('governance_mode', 'supervised')}
>
Supervised
</button>
<button
style={{ ...styles.modeBtn, ...(mode === 'autonomous' ? { ...styles.modeBtnActive, background: '#22c55e33', borderColor: '#22c55e', color: '#22c55e' } : {}) }}
onClick={() => updateSetting('governance_mode', 'autonomous')}
>
Autonomous
</button>
</div>
<div style={styles.hint}>
{mode === 'supervised' ? 'All decisions require human approval.' : 'Type A decisions auto-execute at autonomy level 4+.'}
</div>
</div>
{/* Default autonomy */}
<div style={styles.setting}>
<div style={styles.settingLabel}>Default Autonomy Level: {autonomyDefault}</div>
<input type="range" min={1} max={5} value={autonomyDefault}
onChange={e => updateSetting('autonomy_default', Number(e.target.value))}
style={{ width: '100%' }}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#7a7f96' }}>
<span>1 — Full oversight</span>
<span>5 — Full autonomy</span>
</div>
</div>
{/* Escalation threshold */}
<div style={styles.setting}>
<div style={styles.settingLabel}>Escalation Threshold: {threshold}h</div>
<input type="range" min={1} max={48} value={threshold}
onChange={e => updateSetting('escalation_threshold_hours', Number(e.target.value))}
style={{ width: '100%' }}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#7a7f96' }}>
<span>1 hour</span>
<span>48 hours</span>
</div>
<div style={styles.hint}>
Pending decisions older than {threshold}h are auto-escalated to Type C (human-required).
</div>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
card: { background: '#1a1a2e', borderRadius: 12, padding: 20, border: '1px solid #252540' },
cardTitle: { margin: '0 0 20px', fontSize: 15, fontWeight: 700 },
setting: { marginBottom: 24 },
settingLabel: { fontSize: 12, fontWeight: 700, marginBottom: 8, color: '#e8eaf0' },
toggleRow: { display: 'flex', gap: 8 },
modeBtn: { padding: '8px 20px', borderRadius: 8, border: '1px solid #252540', background: 'transparent', color: '#7a7f96', fontWeight: 600, fontSize: 12, cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.2s' },
modeBtnActive: { background: '#8b5cf633', borderColor: '#8b5cf6', color: '#8b5cf6' },
hint: { fontSize: 11, color: '#7a7f96', marginTop: 6, fontStyle: 'italic' },
empty: { color: '#7a7f96', fontSize: 13, textAlign: 'center' as const, padding: 20 },
};