/** * Identity Change Proposal Component * * Displays pending personality change proposals with: * - Side-by-side diff view * - Accept/Reject buttons * - Reason explanation * * Part of ZCLAW L4 Self-Evolution capability. */ import { useState, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Check, X, FileText, Clock, AlertCircle, ChevronDown, ChevronUp, Sparkles, History, } from 'lucide-react'; import { intelligenceClient, type IdentityChangeProposal as Proposal, type IdentitySnapshot, } from '../lib/intelligence-client'; import { useChatStore } from '../store/chatStore'; import { Button, Badge } from './ui'; // === Diff View Component === function DiffView({ current, proposed, }: { current: string; proposed: string; }) { const currentLines = useMemo(() => current.split('\n'), [current]); const proposedLines = useMemo(() => proposed.split('\n'), [proposed]); // Simple line-by-line diff const maxLines = Math.max(currentLines.length, proposedLines.length); const diffLines: Array<{ type: 'unchanged' | 'added' | 'removed' | 'modified'; current?: string; proposed?: string; lineNum: number; }> = []; // Build a simple diff - for a production system, use a proper diff algorithm // Note: currentSet/proposedSet could be used for advanced diff algorithms // const currentSet = new Set(currentLines); // const proposedSet = new Set(proposedLines); for (let i = 0; i < maxLines; i++) { const currLine = currentLines[i]; const propLine = proposedLines[i]; if (currLine === propLine) { diffLines.push({ type: 'unchanged', current: currLine, proposed: propLine, lineNum: i + 1 }); } else if (currLine === undefined) { diffLines.push({ type: 'added', proposed: propLine, lineNum: i + 1 }); } else if (propLine === undefined) { diffLines.push({ type: 'removed', current: currLine, lineNum: i + 1 }); } else { diffLines.push({ type: 'modified', current: currLine, proposed: propLine, lineNum: i + 1 }); } } return (
{/* Current */}
当前版本
{diffLines.map((line, idx) => (
{line.type === 'removed' && -} {line.type === 'modified' && ~} {line.current || '\u00A0'}
))}
{/* Proposed */}
建议版本
{diffLines.map((line, idx) => (
{line.type === 'added' && +} {line.type === 'modified' && ~} {line.proposed || '\u00A0'}
))}
); } // === Single Proposal Card === function ProposalCard({ proposal, onApprove, onReject, isProcessing, }: { proposal: Proposal; onApprove: () => void; onReject: () => void; isProcessing: boolean; }) { const [expanded, setExpanded] = useState(true); const fileLabel = proposal.file === 'soul' ? 'SOUL.md' : 'Instructions'; const timeAgo = getTimeAgo(proposal.created_at); return ( {/* Header */}
setExpanded(!expanded)} >
人格变更提案 {fileLabel}
{timeAgo}
{expanded ? ( ) : ( )}
{/* Content */} {expanded && (
{/* Reason */}
变更原因

{proposal.reason}

{/* Diff View */} {/* Actions */}
)}
); } // === Evolution History Item === function HistoryItem({ snapshot, onRestore, isRestoring, }: { snapshot: IdentitySnapshot; onRestore: () => void; isRestoring: boolean; }) { const timeAgo = getTimeAgo(snapshot.timestamp); return (
{timeAgo}

{snapshot.reason || '自动快照'}

); } // === Main Component === export function IdentityChangeProposalPanel() { const { currentAgent } = useChatStore(); const [proposals, setProposals] = useState([]); const [snapshots, setSnapshots] = useState([]); const [loading, setLoading] = useState(true); const [processingId, setProcessingId] = useState(null); const [error, setError] = useState(null); const agentId = currentAgent?.id; // Load data useEffect(() => { if (!agentId) return; const loadData = async () => { setLoading(true); setError(null); try { const [pendingProposals, agentSnapshots] = await Promise.all([ intelligenceClient.identity.getPendingProposals(agentId), intelligenceClient.identity.getSnapshots(agentId, 10), ]); setProposals(pendingProposals); setSnapshots(agentSnapshots); } catch (err) { console.error('[IdentityChangeProposal] Failed to load data:', err); setError('加载失败'); } finally { setLoading(false); } }; loadData(); }, [agentId]); const handleApprove = async (proposalId: string) => { if (!agentId) return; setProcessingId(proposalId); setError(null); try { await intelligenceClient.identity.approveProposal(proposalId); // Refresh data const [pendingProposals, agentSnapshots] = await Promise.all([ intelligenceClient.identity.getPendingProposals(agentId), intelligenceClient.identity.getSnapshots(agentId, 10), ]); setProposals(pendingProposals); setSnapshots(agentSnapshots); } catch (err) { console.error('[IdentityChangeProposal] Failed to approve:', err); setError('审批失败'); } finally { setProcessingId(null); } }; const handleReject = async (proposalId: string) => { if (!agentId) return; setProcessingId(proposalId); setError(null); try { await intelligenceClient.identity.rejectProposal(proposalId); // Refresh proposals const pendingProposals = await intelligenceClient.identity.getPendingProposals(agentId); setProposals(pendingProposals); } catch (err) { console.error('[IdentityChangeProposal] Failed to reject:', err); setError('拒绝失败'); } finally { setProcessingId(null); } }; const handleRestore = async (snapshotId: string) => { if (!agentId) return; setProcessingId(snapshotId); setError(null); try { await intelligenceClient.identity.restoreSnapshot(agentId, snapshotId); // Refresh snapshots const agentSnapshots = await intelligenceClient.identity.getSnapshots(agentId, 10); setSnapshots(agentSnapshots); } catch (err) { console.error('[IdentityChangeProposal] Failed to restore:', err); setError('恢复失败'); } finally { setProcessingId(null); } }; if (!agentId) { return (

请先选择一个 Agent

); } if (loading) { return (
加载中...
); } return (
{/* Error */} {error && (
{error}
)} {/* Pending Proposals */}

待审批提案 {proposals.length > 0 && ( {proposals.length} )}

{proposals.length === 0 ? (
暂无待审批的人格变更提案
) : (
{proposals.map((proposal) => ( handleApprove(proposal.id)} onReject={() => handleReject(proposal.id)} isProcessing={processingId === proposal.id} /> ))}
)}
{/* Evolution History */}

演化历史

{snapshots.length === 0 ? (
暂无演化历史
) : (
{snapshots.map((snapshot) => ( handleRestore(snapshot.id)} isRestoring={processingId === snapshot.id} /> ))}
)}
); } // === Helper === function getTimeAgo(timestamp: string): string { const now = Date.now(); const then = new Date(timestamp).getTime(); const diff = now - then; if (diff < 60000) return '刚刚'; if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`; if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`; if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`; return new Date(timestamp).toLocaleDateString('zh-CN'); } export default IdentityChangeProposalPanel;