/**
* 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 */}
{/* 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 (
);
}
if (loading) {
return (
);
}
return (
{/* 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;