feat(l4): add Phase 1 UI components for self-evolution capability
SwarmDashboard (多 Agent 协作面板): - Task list with real-time status updates - Subtask visualization with results - Communication style indicators (Sequential/Parallel/Debate) - Task creation form with manual triggers SkillMarket (技能市场): - Browse 12 built-in skills by category - Keyword/capability search - Skill details with triggers and capabilities - Install/uninstall with L4 autonomy hooks HeartbeatConfig (心跳配置): - Enable/disable periodic proactive checks - Interval slider (5-120 minutes) - Proactivity level selector (Silent/Light/Standard/Autonomous) - Quiet hours configuration - Built-in check item toggles ReflectionLog (反思日志): - Reflection history with pattern analysis - Improvement suggestions by priority - Identity change proposal approval workflow - Manual reflection trigger - Config panel for trigger settings Part of ZCLAW L4 Self-Evolution capability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
608
desktop/src/components/ReflectionLog.tsx
Normal file
608
desktop/src/components/ReflectionLog.tsx
Normal file
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* ReflectionLog - Self-reflection history and identity change approval UI
|
||||
*
|
||||
* Displays:
|
||||
* - Reflection history (patterns, improvements)
|
||||
* - Pending identity change proposals
|
||||
* - Approve/reject identity modifications
|
||||
* - Manual reflection trigger
|
||||
*
|
||||
* Part of ZCLAW L4 Self-Evolution capability.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Brain,
|
||||
Sparkles,
|
||||
Check,
|
||||
X,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
AlertTriangle,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Minus,
|
||||
FileText,
|
||||
History,
|
||||
Play,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
ReflectionEngine,
|
||||
type ReflectionResult,
|
||||
type PatternObservation,
|
||||
type ImprovementSuggestion,
|
||||
type ReflectionConfig,
|
||||
DEFAULT_REFLECTION_CONFIG,
|
||||
} from '../lib/reflection-engine';
|
||||
import { getAgentIdentityManager, type IdentityChangeProposal } from '../lib/agent-identity';
|
||||
|
||||
// === Types ===
|
||||
|
||||
interface ReflectionLogProps {
|
||||
className?: string;
|
||||
agentId?: string;
|
||||
onProposalApprove?: (proposal: IdentityChangeProposal) => void;
|
||||
onProposalReject?: (proposal: IdentityChangeProposal) => void;
|
||||
}
|
||||
|
||||
// === Sentiment Config ===
|
||||
|
||||
const SENTIMENT_CONFIG: Record<string, { icon: typeof TrendingUp; color: string; bgColor: string }> = {
|
||||
positive: {
|
||||
icon: TrendingUp,
|
||||
color: 'text-green-600 dark:text-green-400',
|
||||
bgColor: 'bg-green-100 dark:bg-green-900/30',
|
||||
},
|
||||
negative: {
|
||||
icon: TrendingDown,
|
||||
color: 'text-red-600 dark:text-red-400',
|
||||
bgColor: 'bg-red-100 dark:bg-red-900/30',
|
||||
},
|
||||
neutral: {
|
||||
icon: Minus,
|
||||
color: 'text-gray-600 dark:text-gray-400',
|
||||
bgColor: 'bg-gray-100 dark:bg-gray-800',
|
||||
},
|
||||
};
|
||||
|
||||
// === Priority Config ===
|
||||
|
||||
const PRIORITY_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
||||
high: {
|
||||
color: 'text-red-600 dark:text-red-400',
|
||||
bgColor: 'bg-red-100 dark:bg-red-900/30',
|
||||
},
|
||||
medium: {
|
||||
color: 'text-yellow-600 dark:text-yellow-400',
|
||||
bgColor: 'bg-yellow-100 dark:bg-yellow-900/30',
|
||||
},
|
||||
low: {
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
bgColor: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
},
|
||||
};
|
||||
|
||||
// === Components ===
|
||||
|
||||
function SentimentBadge({ sentiment }: { sentiment: string }) {
|
||||
const config = SENTIMENT_CONFIG[sentiment] || SENTIMENT_CONFIG.neutral;
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color}`}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
{sentiment === 'positive' ? '积极' : sentiment === 'negative' ? '消极' : '中性'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function PriorityBadge({ priority }: { priority: string }) {
|
||||
const config = PRIORITY_CONFIG[priority] || PRIORITY_CONFIG.medium;
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bgColor} ${config.color}`}
|
||||
>
|
||||
{priority === 'high' ? '高' : priority === 'medium' ? '中' : '低'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function PatternCard({ pattern }: { pattern: PatternObservation }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-start gap-3 p-3 hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors text-left"
|
||||
>
|
||||
<SentimentBadge sentiment={pattern.sentiment} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-gray-900 dark:text-gray-100">{pattern.observation}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
出现频率: {pattern.frequency} 次
|
||||
</p>
|
||||
</div>
|
||||
{expanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{expanded && pattern.evidence.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-gray-200 dark:border-gray-700 p-3 bg-gray-50 dark:bg-gray-800/30"
|
||||
>
|
||||
<h5 className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">证据</h5>
|
||||
<ul className="space-y-1">
|
||||
{pattern.evidence.map((ev, i) => (
|
||||
<li key={i} className="text-xs text-gray-600 dark:text-gray-300 pl-2 border-l-2 border-gray-300 dark:border-gray-600">
|
||||
{ev}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ImprovementCard({ improvement }: { improvement: ImprovementSuggestion }) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{improvement.area}
|
||||
</span>
|
||||
<PriorityBadge priority={improvement.priority} />
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{improvement.suggestion}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProposalCard({
|
||||
proposal,
|
||||
onApprove,
|
||||
onReject,
|
||||
}: {
|
||||
proposal: IdentityChangeProposal;
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const identityManager = getAgentIdentityManager();
|
||||
|
||||
const fileName = proposal.filePath.split('/').pop() || proposal.filePath;
|
||||
const fileType = fileName.toLowerCase().replace('.md', '').toUpperCase();
|
||||
|
||||
return (
|
||||
<div className="border border-yellow-300 dark:border-yellow-700 rounded-lg overflow-hidden bg-yellow-50 dark:bg-yellow-900/20">
|
||||
<div className="flex items-center gap-3 p-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-yellow-100 dark:bg-yellow-800 flex items-center justify-center">
|
||||
<FileText className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{fileType} 变更提议
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 text-xs bg-yellow-200 dark:bg-yellow-800 text-yellow-700 dark:text-yellow-300 rounded">
|
||||
待审批
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-yellow-600 dark:text-yellow-400 truncate">
|
||||
{proposal.reason}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="p-1 text-yellow-600 dark:text-yellow-400 hover:bg-yellow-100 dark:hover:bg-yellow-800 rounded"
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{expanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-yellow-200 dark:border-yellow-700"
|
||||
>
|
||||
<div className="p-3 space-y-3">
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-yellow-700 dark:text-yellow-300 mb-1">
|
||||
当前内容
|
||||
</h5>
|
||||
<pre className="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
||||
{proposal.currentContent.slice(0, 500)}
|
||||
{proposal.currentContent.length > 500 && '...'}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-yellow-700 dark:text-yellow-300 mb-1">
|
||||
建议内容
|
||||
</h5>
|
||||
<pre className="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
||||
{proposal.proposedContent.slice(0, 500)}
|
||||
{proposal.proposedContent.length > 500 && '...'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex justify-end gap-2 p-3 border-t border-yellow-200 dark:border-yellow-700">
|
||||
<button
|
||||
onClick={onReject}
|
||||
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
拒绝
|
||||
</button>
|
||||
<button
|
||||
onClick={onApprove}
|
||||
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-green-500 hover:bg-green-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
批准
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReflectionEntry({
|
||||
result,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: {
|
||||
result: ReflectionResult;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const positivePatterns = result.patterns.filter((p) => p.sentiment === 'positive').length;
|
||||
const negativePatterns = result.patterns.filter((p) => p.sentiment === 'negative').length;
|
||||
const highPriorityImprovements = result.improvements.filter((i) => i.priority === 'high').length;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center gap-3 p-4 hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||
<Brain className="w-5 h-5 text-purple-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
自我反思
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{new Date(result.timestamp).toLocaleString('zh-CN')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<span className="text-green-600 dark:text-green-400">
|
||||
{positivePatterns} 积极
|
||||
</span>
|
||||
<span className="text-red-600 dark:text-red-400">
|
||||
{negativePatterns} 消极
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{result.improvements.length} 建议
|
||||
</span>
|
||||
{result.identityProposals.length > 0 && (
|
||||
<span className="text-yellow-600 dark:text-yellow-400">
|
||||
{result.identityProposals.length} 变更
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Patterns */}
|
||||
{result.patterns.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
||||
行为模式
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.patterns.map((pattern, i) => (
|
||||
<PatternCard key={i} pattern={pattern} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Improvements */}
|
||||
{result.improvements.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
||||
改进建议
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.improvements.map((improvement, i) => (
|
||||
<ImprovementCard key={i} improvement={improvement} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<span>新增记忆: {result.newMemories}</span>
|
||||
<span>身份变更提议: {result.identityProposals.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// === Main Component ===
|
||||
|
||||
export function ReflectionLog({
|
||||
className = '',
|
||||
agentId = 'zclaw-main',
|
||||
onProposalApprove,
|
||||
onProposalReject,
|
||||
}: ReflectionLogProps) {
|
||||
const [engine] = useState(() => new ReflectionEngine());
|
||||
const [history, setHistory] = useState<ReflectionResult[]>([]);
|
||||
const [pendingProposals, setPendingProposals] = useState<IdentityChangeProposal[]>([]);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [isReflecting, setIsReflecting] = useState(false);
|
||||
const [config, setConfig] = useState<ReflectionConfig>(DEFAULT_REFLECTION_CONFIG);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
|
||||
// Load history and pending proposals
|
||||
useEffect(() => {
|
||||
const loadedHistory = engine.getHistory();
|
||||
setHistory([...loadedHistory].reverse()); // Most recent first
|
||||
|
||||
const identityManager = getAgentIdentityManager();
|
||||
const proposals = identityManager.getPendingProposals(agentId);
|
||||
setPendingProposals(proposals);
|
||||
}, [engine, agentId]);
|
||||
|
||||
const handleReflect = useCallback(async () => {
|
||||
setIsReflecting(true);
|
||||
try {
|
||||
const result = await engine.reflect(agentId);
|
||||
setHistory((prev) => [result, ...prev]);
|
||||
|
||||
// Update pending proposals
|
||||
if (result.identityProposals.length > 0) {
|
||||
setPendingProposals((prev) => [...prev, ...result.identityProposals]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ReflectionLog] Reflection failed:', error);
|
||||
} finally {
|
||||
setIsReflecting(false);
|
||||
}
|
||||
}, [engine, agentId]);
|
||||
|
||||
const handleApproveProposal = useCallback(
|
||||
(proposal: IdentityChangeProposal) => {
|
||||
const identityManager = getAgentIdentityManager();
|
||||
identityManager.approveChange(proposal.id);
|
||||
setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id));
|
||||
onProposalApprove?.(proposal);
|
||||
},
|
||||
[onProposalApprove]
|
||||
);
|
||||
|
||||
const handleRejectProposal = useCallback(
|
||||
(proposal: IdentityChangeProposal) => {
|
||||
const identityManager = getAgentIdentityManager();
|
||||
identityManager.rejectChange(proposal.id);
|
||||
setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id));
|
||||
onProposalReject?.(proposal);
|
||||
},
|
||||
[onProposalReject]
|
||||
);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const totalReflections = history.length;
|
||||
const totalPatterns = history.reduce((sum, r) => sum + r.patterns.length, 0);
|
||||
const totalImprovements = history.reduce((sum, r) => sum + r.improvements.length, 0);
|
||||
const totalIdentityChanges = history.reduce((sum, r) => sum + r.identityProposals.length, 0);
|
||||
return { totalReflections, totalPatterns, totalImprovements, totalIdentityChanges };
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col h-full ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="w-5 h-5 text-purple-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">反思日志</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowConfig(!showConfig)}
|
||||
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
title="配置"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReflect}
|
||||
disabled={isReflecting}
|
||||
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-purple-500 hover:bg-purple-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
|
||||
>
|
||||
{isReflecting ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="w-4 h-4" />
|
||||
)}
|
||||
反思
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="flex items-center gap-4 px-4 py-2 bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
反思: <span className="font-medium text-gray-900 dark:text-gray-100">{stats.totalReflections}</span>
|
||||
</span>
|
||||
<span className="text-purple-600 dark:text-purple-400">
|
||||
模式: <span className="font-medium">{stats.totalPatterns}</span>
|
||||
</span>
|
||||
<span className="text-blue-600 dark:text-blue-400">
|
||||
建议: <span className="font-medium">{stats.totalImprovements}</span>
|
||||
</span>
|
||||
<span className="text-yellow-600 dark:text-yellow-400">
|
||||
变更: <span className="font-medium">{stats.totalIdentityChanges}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Config Panel */}
|
||||
<AnimatePresence>
|
||||
{showConfig && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-b border-gray-200 dark:border-gray-700 overflow-hidden"
|
||||
>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800/50 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">对话后触发反思</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
value={config.triggerAfterConversations}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({ ...prev, triggerAfterConversations: parseInt(e.target.value) || 5 }))
|
||||
}
|
||||
className="w-16 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">允许修改 SOUL.md</span>
|
||||
<button
|
||||
onClick={() => setConfig((prev) => ({ ...prev, allowSoulModification: !prev.allowSoulModification }))}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${
|
||||
config.allowSoulModification ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: config.allowSoulModification ? 18 : 0 }}
|
||||
className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">变更需审批</span>
|
||||
<button
|
||||
onClick={() => setConfig((prev) => ({ ...prev, requireApproval: !prev.requireApproval }))}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${
|
||||
config.requireApproval ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: config.requireApproval ? 18 : 0 }}
|
||||
className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* Pending Proposals */}
|
||||
{pendingProposals.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="flex items-center gap-2 text-sm font-medium text-yellow-700 dark:text-yellow-300">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
待审批变更 ({pendingProposals.length})
|
||||
</h3>
|
||||
{pendingProposals.map((proposal) => (
|
||||
<ProposalCard
|
||||
key={proposal.id}
|
||||
proposal={proposal}
|
||||
onApprove={() => handleApproveProposal(proposal)}
|
||||
onReject={() => handleRejectProposal(proposal)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* History */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<History className="w-4 h-4" />
|
||||
反思历史
|
||||
</h3>
|
||||
|
||||
{history.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<Brain className="w-8 h-8 mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无反思记录</p>
|
||||
<button
|
||||
onClick={handleReflect}
|
||||
className="mt-2 text-purple-500 hover:text-purple-600 text-sm"
|
||||
>
|
||||
触发第一次反思
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
history.map((result, i) => (
|
||||
<ReflectionEntry
|
||||
key={result.timestamp}
|
||||
result={result}
|
||||
isExpanded={expandedId === result.timestamp}
|
||||
onToggle={() => setExpandedId((prev) => (prev === result.timestamp ? null : result.timestamp))}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReflectionLog;
|
||||
Reference in New Issue
Block a user