Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- S9: MessageSearch 新增 Session/Global 双模式,Global 调用 VikingStorage memory_search - M4b: LLM 压缩器集成到 kernel AgentLoop,支持 use_llm 配置切换 - M4c: 压缩时自动提取记忆到 VikingStorage (runtime + tauri 双路径) - H6: 新增 ChartRenderer(recharts)、Document/Slideshow 完整渲染 - 累计修复 23 项,整体完成度 ~72%,真实可用率 ~80%
764 lines
27 KiB
TypeScript
764 lines
27 KiB
TypeScript
/**
|
|
* 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,
|
|
Check,
|
|
X,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
RefreshCw,
|
|
AlertTriangle,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Minus,
|
|
FileText,
|
|
History,
|
|
Play,
|
|
Settings,
|
|
} from 'lucide-react';
|
|
import {
|
|
intelligenceClient,
|
|
type ReflectionResult,
|
|
type IdentityChangeProposal,
|
|
type ReflectionConfig,
|
|
type PatternObservation,
|
|
type ImprovementSuggestion,
|
|
} from '../lib/intelligence-client';
|
|
|
|
// === Config Persistence ===
|
|
|
|
const REFLECTION_CONFIG_KEY = 'zclaw-reflection-config';
|
|
|
|
const DEFAULT_CONFIG: ReflectionConfig = {
|
|
trigger_after_conversations: 5,
|
|
allow_soul_modification: true,
|
|
require_approval: true,
|
|
use_llm: true,
|
|
llm_fallback_to_rules: true,
|
|
};
|
|
|
|
function loadConfig(): ReflectionConfig {
|
|
try {
|
|
const stored = localStorage.getItem(REFLECTION_CONFIG_KEY);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
return { ...DEFAULT_CONFIG, ...parsed };
|
|
}
|
|
} catch {
|
|
console.warn('[ReflectionLog] Failed to load config from localStorage');
|
|
}
|
|
return DEFAULT_CONFIG;
|
|
}
|
|
|
|
function saveConfig(config: ReflectionConfig): void {
|
|
try {
|
|
localStorage.setItem(REFLECTION_CONFIG_KEY, JSON.stringify(config));
|
|
} catch {
|
|
console.warn('[ReflectionLog] Failed to save config to localStorage');
|
|
}
|
|
}
|
|
|
|
// === 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',
|
|
},
|
|
};
|
|
|
|
// === Field to File Mapping ===
|
|
|
|
/**
|
|
* Maps reflection field names to identity file types.
|
|
* This ensures correct routing of identity change proposals.
|
|
*/
|
|
function mapFieldToFile(field: string): 'soul' | 'instructions' {
|
|
// Direct matches
|
|
if (field === 'soul' || field === 'instructions') {
|
|
return field;
|
|
}
|
|
|
|
// Known soul fields (core personality traits)
|
|
const soulFields = [
|
|
'personality',
|
|
'traits',
|
|
'values',
|
|
'identity',
|
|
'character',
|
|
'essence',
|
|
'core_behavior',
|
|
];
|
|
|
|
// Known instructions fields (operational guidelines)
|
|
const instructionsFields = [
|
|
'guidelines',
|
|
'rules',
|
|
'behavior_rules',
|
|
'response_format',
|
|
'communication_guidelines',
|
|
'task_handling',
|
|
];
|
|
|
|
const lowerField = field.toLowerCase();
|
|
|
|
// Check explicit mappings
|
|
if (soulFields.some((f) => lowerField.includes(f))) {
|
|
return 'soul';
|
|
}
|
|
if (instructionsFields.some((f) => lowerField.includes(f))) {
|
|
return 'instructions';
|
|
}
|
|
|
|
// Fallback heuristics
|
|
if (lowerField.includes('soul') || lowerField.includes('personality') || lowerField.includes('trait')) {
|
|
return 'soul';
|
|
}
|
|
|
|
// Default to instructions for operational changes
|
|
return 'instructions';
|
|
}
|
|
|
|
// === 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 fileName = proposal.file.split('/').pop() || proposal.file;
|
|
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.current_content.slice(0, 500)}
|
|
{proposal.current_content.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.suggested_content.slice(0, 500)}
|
|
{proposal.suggested_content.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;
|
|
|
|
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.identity_proposals.length > 0 && (
|
|
<span className="text-yellow-600 dark:text-yellow-400">
|
|
{result.identity_proposals.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.new_memories}</span>
|
|
<span>身份变更提议: {result.identity_proposals.length}</span>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// === Main Component ===
|
|
|
|
export function ReflectionLog({
|
|
className = '',
|
|
agentId = 'zclaw-main',
|
|
onProposalApprove,
|
|
onProposalReject,
|
|
}: ReflectionLogProps) {
|
|
const [history, setHistory] = useState<ReflectionResult[]>([]);
|
|
const [pendingProposals, setPendingProposals] = useState<IdentityChangeProposal[]>([]);
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [isReflecting, setIsReflecting] = useState(false);
|
|
const [showConfig, setShowConfig] = useState(false);
|
|
const [config, setConfig] = useState<ReflectionConfig>(() => loadConfig());
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Persist config changes
|
|
useEffect(() => {
|
|
saveConfig(config);
|
|
}, [config]);
|
|
|
|
// Load history and pending proposals
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
try {
|
|
// Initialize reflection engine with config that allows soul modification
|
|
await intelligenceClient.reflection.init(config);
|
|
|
|
const loadedHistory = await intelligenceClient.reflection.getHistory(undefined, agentId);
|
|
setHistory([...loadedHistory].reverse()); // Most recent first
|
|
|
|
const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
|
|
setPendingProposals(proposals);
|
|
} catch (error) {
|
|
console.error('[ReflectionLog] Failed to load data:', error);
|
|
}
|
|
};
|
|
loadData();
|
|
}, [agentId, config]);
|
|
|
|
const handleReflect = useCallback(async () => {
|
|
setIsReflecting(true);
|
|
setError(null);
|
|
try {
|
|
// Fetch recent memories for analysis
|
|
const memories = await intelligenceClient.memory.search({
|
|
agentId,
|
|
limit: 50, // Get enough memories for pattern analysis
|
|
});
|
|
|
|
// Convert to analysis format
|
|
const memoriesForAnalysis = memories.map((m) => ({
|
|
memory_type: m.type,
|
|
content: m.content,
|
|
importance: m.importance,
|
|
access_count: m.accessCount,
|
|
tags: m.tags,
|
|
}));
|
|
|
|
const result = await intelligenceClient.reflection.reflect(agentId, memoriesForAnalysis);
|
|
setHistory((prev) => [result, ...prev]);
|
|
|
|
// 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 {
|
|
// Map field to file type with explicit mapping rules
|
|
const file = mapFieldToFile(proposal.field);
|
|
|
|
// 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 (err) {
|
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
console.error('[ReflectionLog] Reflection failed:', err);
|
|
setError(`反思失败: ${errorMessage}`);
|
|
} finally {
|
|
setIsReflecting(false);
|
|
}
|
|
}, [agentId]);
|
|
|
|
const handleApproveProposal = useCallback(
|
|
async (proposal: IdentityChangeProposal) => {
|
|
await intelligenceClient.identity.approveProposal(proposal.id);
|
|
setPendingProposals((prev: IdentityChangeProposal[]) => prev.filter((p: IdentityChangeProposal) => p.id !== proposal.id));
|
|
onProposalApprove?.(proposal);
|
|
},
|
|
[onProposalApprove]
|
|
);
|
|
|
|
const handleRejectProposal = useCallback(
|
|
async (proposal: IdentityChangeProposal) => {
|
|
await intelligenceClient.identity.rejectProposal(proposal.id);
|
|
setPendingProposals((prev: IdentityChangeProposal[]) => prev.filter((p: IdentityChangeProposal) => p.id !== proposal.id));
|
|
onProposalReject?.(proposal);
|
|
},
|
|
[onProposalReject]
|
|
);
|
|
|
|
const stats = useMemo(() => {
|
|
const totalReflections = history.length;
|
|
const totalPatterns = history.reduce((sum: number, r: ReflectionResult) => sum + r.patterns.length, 0);
|
|
const totalImprovements = history.reduce((sum: number, r: ReflectionResult) => sum + r.improvements.length, 0);
|
|
const totalIdentityChanges = history.reduce((sum: number, r: ReflectionResult) => sum + r.identity_proposals.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>
|
|
|
|
{/* Error Banner */}
|
|
<AnimatePresence>
|
|
{error && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="flex items-center justify-between gap-2 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800">
|
|
<div className="flex items-center gap-2 text-red-700 dark:text-red-300 text-sm">
|
|
<AlertTriangle className="w-4 h-4" />
|
|
<span>{error}</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setError(null)}
|
|
className="p-1 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 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.trigger_after_conversations || 5}
|
|
onChange={(e) =>
|
|
setConfig((prev) => ({ ...prev, trigger_after_conversations: 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, allow_soul_modification: !prev.allow_soul_modification }))}
|
|
className={`relative w-9 h-5 rounded-full transition-colors ${
|
|
config.allow_soul_modification ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
>
|
|
<motion.div
|
|
animate={{ x: config.allow_soul_modification ? 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, require_approval: !prev.require_approval }))}
|
|
className={`relative w-9 h-5 rounded-full transition-colors ${
|
|
config.require_approval ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
>
|
|
<motion.div
|
|
animate={{ x: config.require_approval ? 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) => (
|
|
<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;
|