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, []);
|
||||
setHistory((prev) => [result, ...prev]);
|
||||
|
||||
// Update pending proposals
|
||||
if (result.identity_proposals.length > 0) {
|
||||
setPendingProposals((prev) => [...prev, ...result.identity_proposals]);
|
||||
// Convert reflection identity_proposals to actual identity proposals
|
||||
// The reflection result contains proposals that need to be persisted
|
||||
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) {
|
||||
console.error('[ReflectionLog] Reflection failed:', error);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
||||
import {
|
||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||
Shield, Sparkles, GraduationCap, List, Network
|
||||
Shield, Sparkles, GraduationCap, List, Network, Dna
|
||||
} from 'lucide-react';
|
||||
|
||||
// === Helper to extract code blocks from markdown content ===
|
||||
@@ -74,6 +74,7 @@ import { MemoryGraph } from './MemoryGraph';
|
||||
import { ReflectionLog } from './ReflectionLog';
|
||||
import { AutonomyConfig } from './AutonomyConfig';
|
||||
import { ActiveLearningPanel } from './ActiveLearningPanel';
|
||||
import { IdentityChangeProposalPanel } from './IdentityChangeProposal';
|
||||
import { CodeSnippetPanel, type CodeSnippet } from './CodeSnippetPanel';
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
import { Button, Badge } from './ui';
|
||||
@@ -101,7 +102,7 @@ export function RightPanel() {
|
||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||||
|
||||
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 [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||
@@ -263,6 +264,12 @@ export function RightPanel() {
|
||||
icon={<GraduationCap className="w-4 h-4" />}
|
||||
label="学习"
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'evolution'}
|
||||
onClick={() => setActiveTab('evolution')}
|
||||
icon={<Dna className="w-4 h-4" />}
|
||||
label="演化"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -324,6 +331,8 @@ export function RightPanel() {
|
||||
<AutonomyConfig />
|
||||
) : activeTab === 'learning' ? (
|
||||
<ActiveLearningPanel />
|
||||
) : activeTab === 'evolution' ? (
|
||||
<IdentityChangeProposalPanel />
|
||||
) : activeTab === 'agent' ? (
|
||||
<div className="space-y-4">
|
||||
<motion.div
|
||||
|
||||
@@ -146,10 +146,19 @@ export interface ImprovementSuggestion {
|
||||
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 {
|
||||
patterns: PatternObservation[];
|
||||
improvements: ImprovementSuggestion[];
|
||||
identity_proposals: IdentityChangeProposal[];
|
||||
identity_proposals: ReflectionIdentityProposal[];
|
||||
new_memories: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ export type {
|
||||
ReflectionResult,
|
||||
ReflectionState,
|
||||
ReflectionConfig,
|
||||
ReflectionIdentityProposal,
|
||||
IdentityFiles,
|
||||
IdentityChangeProposal,
|
||||
IdentitySnapshot,
|
||||
|
||||
Reference in New Issue
Block a user