feat(intelligence): add self-evolution UI for identity change proposals
P1.1: Identity Change Proposal UI - Create IdentityChangeProposal.tsx with diff view for SOUL.md changes - Add approve/reject buttons with visual feedback - Show evolution history timeline with restore capability P1.2: Connect Reflection Engine to Identity Proposals - Update ReflectionLog.tsx to convert reflection proposals to identity proposals - Add ReflectionIdentityProposal type for non-persisted proposals - Auto-create identity proposals when reflection detects personality changes P1.3: Evolution History and Rollback - Display identity snapshots with timestamps - One-click restore to previous personality versions - Visual diff between current and proposed content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
476
desktop/src/components/IdentityChangeProposal.tsx
Normal file
476
desktop/src/components/IdentityChangeProposal.tsx
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs font-mono">
|
||||||
|
{/* Current */}
|
||||||
|
<div className="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
|
<div className="bg-red-50 dark:bg-red-900/20 px-2 py-1 text-red-700 dark:text-red-300 font-sans font-medium border-b border-red-100 dark:border-red-800">
|
||||||
|
当前版本
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-800/50 max-h-64 overflow-y-auto">
|
||||||
|
{diffLines.map((line, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`px-2 py-0.5 ${
|
||||||
|
line.type === 'removed'
|
||||||
|
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
: line.type === 'modified'
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{line.type === 'removed' && <span className="text-red-500 mr-1">-</span>}
|
||||||
|
{line.type === 'modified' && <span className="text-yellow-500 mr-1">~</span>}
|
||||||
|
{line.current || '\u00A0'}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Proposed */}
|
||||||
|
<div className="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
|
<div className="bg-green-50 dark:bg-green-900/20 px-2 py-1 text-green-700 dark:text-green-300 font-sans font-medium border-b border-green-100 dark:border-green-800">
|
||||||
|
建议版本
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-800/50 max-h-64 overflow-y-auto">
|
||||||
|
{diffLines.map((line, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`px-2 py-0.5 ${
|
||||||
|
line.type === 'added'
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: line.type === 'modified'
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{line.type === 'added' && <span className="text-green-500 mr-1">+</span>}
|
||||||
|
{line.type === 'modified' && <span className="text-yellow-500 mr-1">~</span>}
|
||||||
|
{line.proposed || '\u00A0'}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 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 (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
className="rounded-xl border border-orange-200 dark:border-orange-800 bg-orange-50/50 dark:bg-orange-900/10 overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
className="px-4 py-3 flex items-center justify-between cursor-pointer hover:bg-orange-100/50 dark:hover:bg-orange-900/20 transition-colors"
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center">
|
||||||
|
<Sparkles className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
人格变更提案
|
||||||
|
</span>
|
||||||
|
<Badge variant="warning" className="text-xs">
|
||||||
|
{fileLabel}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>{timeAgo}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{expanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{expanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-4 pb-4 space-y-4">
|
||||||
|
{/* Reason */}
|
||||||
|
<div className="rounded-lg bg-white dark:bg-gray-800 p-3 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||||
|
变更原因
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300">{proposal.reason}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diff View */}
|
||||||
|
<DiffView
|
||||||
|
current={proposal.current_content}
|
||||||
|
proposed={proposal.suggested_content}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onReject}
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="text-red-600 border-red-200 hover:bg-red-50 dark:border-red-800 dark:hover:bg-red-900/20"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 mr-1" />
|
||||||
|
拒绝
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={onApprove}
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4 mr-1" />
|
||||||
|
接受
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Evolution History Item ===
|
||||||
|
|
||||||
|
function HistoryItem({
|
||||||
|
snapshot,
|
||||||
|
onRestore,
|
||||||
|
isRestoring,
|
||||||
|
}: {
|
||||||
|
snapshot: IdentitySnapshot;
|
||||||
|
onRestore: () => void;
|
||||||
|
isRestoring: boolean;
|
||||||
|
}) {
|
||||||
|
const timeAgo = getTimeAgo(snapshot.timestamp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">
|
||||||
|
<History className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">{timeAgo}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onRestore}
|
||||||
|
disabled={isRestoring}
|
||||||
|
className="text-xs text-gray-500 hover:text-orange-600"
|
||||||
|
>
|
||||||
|
恢复
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1 truncate">
|
||||||
|
{snapshot.reason || '自动快照'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Main Component ===
|
||||||
|
|
||||||
|
export function IdentityChangeProposalPanel() {
|
||||||
|
const { currentAgent } = useChatStore();
|
||||||
|
const [proposals, setProposals] = useState<Proposal[]>([]);
|
||||||
|
const [snapshots, setSnapshots] = useState<IdentitySnapshot[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [processingId, setProcessingId] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
|
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p className="text-sm">请先选择一个 Agent</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
|
<div className="animate-pulse">加载中...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pending Proposals */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2">
|
||||||
|
<Sparkles className="w-4 h-4 text-orange-500" />
|
||||||
|
待审批提案
|
||||||
|
{proposals.length > 0 && (
|
||||||
|
<Badge variant="warning" className="text-xs">
|
||||||
|
{proposals.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{proposals.length === 0 ? (
|
||||||
|
<div className="text-center py-6 text-gray-500 dark:text-gray-400 text-sm bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-100 dark:border-gray-700">
|
||||||
|
暂无待审批的人格变更提案
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<AnimatePresence>
|
||||||
|
{proposals.map((proposal) => (
|
||||||
|
<ProposalCard
|
||||||
|
key={proposal.id}
|
||||||
|
proposal={proposal}
|
||||||
|
onApprove={() => handleApprove(proposal.id)}
|
||||||
|
onReject={() => handleReject(proposal.id)}
|
||||||
|
isProcessing={processingId === proposal.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Evolution History */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2">
|
||||||
|
<History className="w-4 h-4 text-gray-500" />
|
||||||
|
演化历史
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{snapshots.length === 0 ? (
|
||||||
|
<div className="text-center py-6 text-gray-500 dark:text-gray-400 text-sm bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-100 dark:border-gray-700">
|
||||||
|
暂无演化历史
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{snapshots.map((snapshot) => (
|
||||||
|
<HistoryItem
|
||||||
|
key={snapshot.id}
|
||||||
|
snapshot={snapshot}
|
||||||
|
onRestore={() => handleRestore(snapshot.id)}
|
||||||
|
isRestoring={processingId === snapshot.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 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;
|
||||||
@@ -413,9 +413,34 @@ export function ReflectionLog({
|
|||||||
const result = await intelligenceClient.reflection.reflect(agentId, []);
|
const result = await intelligenceClient.reflection.reflect(agentId, []);
|
||||||
setHistory((prev) => [result, ...prev]);
|
setHistory((prev) => [result, ...prev]);
|
||||||
|
|
||||||
// Update pending proposals
|
// Convert reflection identity_proposals to actual identity proposals
|
||||||
if (result.identity_proposals.length > 0) {
|
// The reflection result contains proposals that need to be persisted
|
||||||
setPendingProposals((prev) => [...prev, ...result.identity_proposals]);
|
if (result.identity_proposals && result.identity_proposals.length > 0) {
|
||||||
|
for (const proposal of result.identity_proposals) {
|
||||||
|
try {
|
||||||
|
// Determine which file to modify based on the field
|
||||||
|
const file: 'soul' | 'instructions' =
|
||||||
|
proposal.field === 'soul' || proposal.field === 'instructions'
|
||||||
|
? (proposal.field as 'soul' | 'instructions')
|
||||||
|
: proposal.field.toLowerCase().includes('soul')
|
||||||
|
? 'soul'
|
||||||
|
: 'instructions';
|
||||||
|
|
||||||
|
// Persist the proposal to the identity system
|
||||||
|
await intelligenceClient.identity.proposeChange(
|
||||||
|
agentId,
|
||||||
|
file,
|
||||||
|
proposal.proposed_value,
|
||||||
|
proposal.reason
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[ReflectionLog] Failed to create identity proposal:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh pending proposals from the identity system
|
||||||
|
const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
|
||||||
|
setPendingProposals(proposals);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ReflectionLog] Reflection failed:', error);
|
console.error('[ReflectionLog] Reflection failed:', error);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
|||||||
import {
|
import {
|
||||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||||
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||||
Shield, Sparkles, GraduationCap, List, Network
|
Shield, Sparkles, GraduationCap, List, Network, Dna
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// === Helper to extract code blocks from markdown content ===
|
// === Helper to extract code blocks from markdown content ===
|
||||||
@@ -74,6 +74,7 @@ import { MemoryGraph } from './MemoryGraph';
|
|||||||
import { ReflectionLog } from './ReflectionLog';
|
import { ReflectionLog } from './ReflectionLog';
|
||||||
import { AutonomyConfig } from './AutonomyConfig';
|
import { AutonomyConfig } from './AutonomyConfig';
|
||||||
import { ActiveLearningPanel } from './ActiveLearningPanel';
|
import { ActiveLearningPanel } from './ActiveLearningPanel';
|
||||||
|
import { IdentityChangeProposalPanel } from './IdentityChangeProposal';
|
||||||
import { CodeSnippetPanel, type CodeSnippet } from './CodeSnippetPanel';
|
import { CodeSnippetPanel, type CodeSnippet } from './CodeSnippetPanel';
|
||||||
import { cardHover, defaultTransition } from '../lib/animations';
|
import { cardHover, defaultTransition } from '../lib/animations';
|
||||||
import { Button, Badge } from './ui';
|
import { Button, Badge } from './ui';
|
||||||
@@ -101,7 +102,7 @@ export function RightPanel() {
|
|||||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||||||
|
|
||||||
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
|
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
|
||||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning'>('status');
|
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning' | 'evolution'>('status');
|
||||||
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
||||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||||
@@ -263,6 +264,12 @@ export function RightPanel() {
|
|||||||
icon={<GraduationCap className="w-4 h-4" />}
|
icon={<GraduationCap className="w-4 h-4" />}
|
||||||
label="学习"
|
label="学习"
|
||||||
/>
|
/>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'evolution'}
|
||||||
|
onClick={() => setActiveTab('evolution')}
|
||||||
|
icon={<Dna className="w-4 h-4" />}
|
||||||
|
label="演化"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -324,6 +331,8 @@ export function RightPanel() {
|
|||||||
<AutonomyConfig />
|
<AutonomyConfig />
|
||||||
) : activeTab === 'learning' ? (
|
) : activeTab === 'learning' ? (
|
||||||
<ActiveLearningPanel />
|
<ActiveLearningPanel />
|
||||||
|
) : activeTab === 'evolution' ? (
|
||||||
|
<IdentityChangeProposalPanel />
|
||||||
) : activeTab === 'agent' ? (
|
) : activeTab === 'agent' ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -146,10 +146,19 @@ export interface ImprovementSuggestion {
|
|||||||
priority: 'high' | 'medium' | 'low';
|
priority: 'high' | 'medium' | 'low';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reflection identity proposal (from reflection engine, not yet persisted)
|
||||||
|
export interface ReflectionIdentityProposal {
|
||||||
|
agent_id: string;
|
||||||
|
field: string;
|
||||||
|
current_value: string;
|
||||||
|
proposed_value: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReflectionResult {
|
export interface ReflectionResult {
|
||||||
patterns: PatternObservation[];
|
patterns: PatternObservation[];
|
||||||
improvements: ImprovementSuggestion[];
|
improvements: ImprovementSuggestion[];
|
||||||
identity_proposals: IdentityChangeProposal[];
|
identity_proposals: ReflectionIdentityProposal[];
|
||||||
new_memories: number;
|
new_memories: number;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export type {
|
|||||||
ReflectionResult,
|
ReflectionResult,
|
||||||
ReflectionState,
|
ReflectionState,
|
||||||
ReflectionConfig,
|
ReflectionConfig,
|
||||||
|
ReflectionIdentityProposal,
|
||||||
IdentityFiles,
|
IdentityFiles,
|
||||||
IdentityChangeProposal,
|
IdentityChangeProposal,
|
||||||
IdentitySnapshot,
|
IdentitySnapshot,
|
||||||
|
|||||||
Reference in New Issue
Block a user