/** * HeartbeatConfig - Configuration UI for periodic proactive checks * * Allows users to configure: * - Heartbeat interval (default 30 minutes) * - Enable/disable built-in check items * - Quiet hours (no notifications during sleep time) * - Proactivity level (silent/light/standard/autonomous) * * Part of ZCLAW L4 Self-Evolution capability. */ import { useState, useCallback, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Heart, Settings, Clock, Moon, Sun, Volume2, VolumeX, AlertTriangle, CheckCircle, Info, RefreshCw, } from 'lucide-react'; import { intelligenceClient, type HeartbeatConfig as HeartbeatConfigType, type HeartbeatResult, type HeartbeatAlert, } from '../lib/intelligence-client'; import { createLogger } from '../lib/logger'; const log = createLogger('HeartbeatConfig'); // === Default Config === const DEFAULT_HEARTBEAT_CONFIG: HeartbeatConfigType = { enabled: true, interval_minutes: 30, quiet_hours_start: null, quiet_hours_end: null, notify_channel: 'ui', proactivity_level: 'standard', max_alerts_per_tick: 5, }; // === Types === interface HeartbeatConfigProps { className?: string; onConfigChange?: (config: HeartbeatConfigType) => void; } type ProactivityLevel = 'silent' | 'light' | 'standard' | 'autonomous'; // === Proactivity Level Config === const PROACTIVITY_CONFIG: Record = { silent: { label: '静默', description: '从不主动推送,仅被动响应', icon: VolumeX, }, light: { label: '轻度', description: '仅紧急事项推送(如定时任务完成)', icon: Volume2, }, standard: { label: '标准', description: '定期巡检 + 任务通知 + 建议推送', icon: AlertTriangle, }, autonomous: { label: '自主', description: 'Agent 自行判断何时推送', icon: Heart, }, }; // === Check Item Config === interface CheckItemConfig { id: string; name: string; description: string; enabled: boolean; } const BUILT_IN_CHECKS: CheckItemConfig[] = [ { id: 'pending-tasks', name: '待办任务检查', description: '检查是否有未完成的任务需要跟进', enabled: true, }, { id: 'memory-health', name: '记忆健康检查', description: '检查记忆存储是否过大需要清理', enabled: true, }, { id: 'idle-greeting', name: '空闲问候', description: '长时间未使用时发送简短问候', enabled: false, }, ]; // === Components === function ProactivityLevelSelector({ value, onChange, }: { value: ProactivityLevel; onChange: (level: ProactivityLevel) => void; }) { return (
{(Object.keys(PROACTIVITY_CONFIG) as ProactivityLevel[]).map((level) => { const config = PROACTIVITY_CONFIG[level]; const Icon = config.icon; const isSelected = value === level; return ( ); })}
); } function QuietHoursConfig({ start, end, onStartChange, onEndChange, enabled, onToggle, }: { start?: string; end?: string; onStartChange: (time: string) => void; onEndChange: (time: string) => void; enabled: boolean; onToggle: (enabled: boolean) => void; }) { return (
免打扰时段
{enabled && (
onEndChange(e.target.value)} className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" />
onStartChange(e.target.value)} className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" />
)}
); } function CheckItemToggle({ item, onToggle, }: { item: CheckItemConfig; onToggle: (enabled: boolean) => void; }) { return (
{item.name}
{item.description}
); } // === Main Component === export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatConfigProps) { const [config, setConfig] = useState(DEFAULT_HEARTBEAT_CONFIG); const [checkItems, setCheckItems] = useState(BUILT_IN_CHECKS); const [lastResult, setLastResult] = useState(null); const [isTesting, setIsTesting] = useState(false); const [hasChanges, setHasChanges] = useState(false); // Load saved config useEffect(() => { const saved = localStorage.getItem('zclaw-heartbeat-config'); if (saved) { try { const parsed = JSON.parse(saved); setConfig({ ...DEFAULT_HEARTBEAT_CONFIG, ...parsed }); } catch { // Use defaults } } const savedChecks = localStorage.getItem('zclaw-heartbeat-checks'); if (savedChecks) { try { setCheckItems(JSON.parse(savedChecks)); } catch { // Use defaults } } }, []); const updateConfig = useCallback( (updates: Partial) => { setConfig((prev) => { const next = { ...prev, ...updates }; setHasChanges(true); onConfigChange?.(next); return next; }); }, [onConfigChange] ); const toggleCheckItem = useCallback((id: string, enabled: boolean) => { setCheckItems((prev) => { const next = prev.map((item) => item.id === id ? { ...item, enabled } : item ); setHasChanges(true); return next; }); }, []); const handleSave = useCallback(async () => { localStorage.setItem('zclaw-heartbeat-config', JSON.stringify(config)); localStorage.setItem('zclaw-heartbeat-checks', JSON.stringify(checkItems)); // Sync to Rust backend (non-blocking — UI updates immediately) try { await intelligenceClient.heartbeat.updateConfig('zclaw-main', config); } catch (err) { log.warn('[HeartbeatConfig] Backend sync failed:', err); } setHasChanges(false); }, [config, checkItems]); const handleTestHeartbeat = useCallback(async () => { setIsTesting(true); try { await intelligenceClient.heartbeat.init('zclaw-main', config); const result = await intelligenceClient.heartbeat.tick('zclaw-main'); setLastResult(result); } catch (error) { console.error('[HeartbeatConfig] Test failed:', error); } finally { setIsTesting(false); } }, [config]); return (
{/* Header */}

心跳配置

{/* Content */}
{/* Enable Toggle */}
启用主动巡检
Agent 将定期检查并主动推送通知
{config.enabled && ( {/* Interval */}
巡检间隔
updateConfig({ interval_minutes: parseInt(e.target.value) })} className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-pink-500" /> {config.interval_minutes} 分钟
{/* Proactivity Level */}
主动性级别
updateConfig({ proactivity_level: level })} />
{/* Quiet Hours */}
updateConfig({ quiet_hours_start: time })} onEndChange={(time) => updateConfig({ quiet_hours_end: time })} onToggle={(enabled) => updateConfig({ quiet_hours_start: enabled ? '22:00' : null, quiet_hours_end: enabled ? '08:00' : null, }) } />
{/* Check Items */}
检查项目
{checkItems.map((item) => ( toggleCheckItem(item.id, enabled)} /> ))}
{/* Last Result */} {lastResult && (
{lastResult.status === 'ok' ? ( ) : ( )} 上次测试结果
检查了 {lastResult.checked_items} 项 {lastResult.alerts.length > 0 && ` · ${lastResult.alerts.length} 个提醒`}
{lastResult.alerts.length > 0 && (
{lastResult.alerts.map((alert: HeartbeatAlert, i: number) => (
{alert.title}: {alert.content}
))}
)}
)}
)}
{/* Info */}

心跳机制让 Agent 具备主动意识,能够定期检查任务状态、记忆健康度等,并根据主动性级别推送通知。 在"自主"模式下,Agent 将自行判断是否需要通知你。

); } export default HeartbeatConfig;