Files
openclaw-monitor/code/frontend/src/components/MemoryModal/index.js
flying-hero 6cc47ef45c feat: 完成 Agent Diary 开源重构 🎉
🚀 重构内容:
- 重命名 Lobster → Agent(通用化)
- 重命名 LobsterDiary → AgentDiary
- 更新所有 API 端点:/api/lobsters/ → /api/agents/
- 前端组件重命名:LobsterDetail → AgentDetail
- 数据迁移:8 Lobsters → 8 Agents, 4 Diaries

📦 开源准备:
- 创建 .env.example(环境变量配置)
- 创建 docker-compose.yml(一键部署)
- 创建 Dockerfile(前后端)
- 创建 .gitignore
- 添加 MIT LICENSE
- 完善 README.md(中英双语)
- 创建 USAGE.md(使用说明)

📝 文档完善:
- REFACTOR_PLAN.md(重构计划)
- REFACTOR_PROGRESS.md(重构进度)
- REFACTOR_COMPLETE.md(重构完成报告)
- FINAL_REPORT.md(最终报告)
- 工作区同步报告.md

 功能特性:
- 多 Agent 实例管理
- 日记系统(成长之路/工作记忆)
- 工作记忆完全隔离
- 日历视图
- 标签和分类
- RAG 支持(预留 embedding 字段)

🎯 开源准备度:100%

🦸 感谢北极星  的耐心指导!
2026-04-03 19:14:21 +08:00

457 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
const API_BASE = 'http://localhost:8000/api';
function MemoryModal({ agentId, agentName, onClose }) {
const [activeTab, setActiveTab] = useState('diary'); // 'memory' 或 'diary'
const [dates, setDates] = useState([]);
const [diaryDates, setDiaryDates] = useState([]);
const [selectedDate, setSelectedDate] = useState('');
const [content, setContent] = useState('');
const [currentMonth, setCurrentMonth] = useState(new Date());
const [loading, setLoading] = useState(false);
// 加载记忆和日记的日期
useEffect(() => {
loadDates();
}, [agentId, activeTab]);
const loadDates = async () => {
setLoading(true);
try {
if (activeTab === 'memory') {
// 加载记忆日期(每日记忆文件)
const response = await fetch(`${API_BASE}/agents/${agentId}/memory/dates/`);
const data = await response.json();
setDates(data.dates || []);
if (data.dates && data.dates.length > 0) {
setSelectedDate(data.dates[0]);
}
} else {
// 加载日记日期(成才之路)
const response = await fetch(`${API_BASE}/agents/${agentId}/diary/dates/`);
const data = await response.json();
setDiaryDates(data.dates || []);
if (data.dates && data.dates.length > 0) {
setSelectedDate(data.dates[0]);
}
}
} catch (error) {
console.error('加载日期失败:', error);
} finally {
setLoading(false);
}
};
// 加载选中日期的内容
useEffect(() => {
if (selectedDate) {
loadContent(selectedDate);
}
}, [selectedDate, activeTab, agentId]);
const loadContent = async (date) => {
setLoading(true);
try {
if (activeTab === 'memory') {
const response = await fetch(`${API_BASE}/agents/${agentId}/memory/${date}/`);
const data = await response.json();
setContent(data.content || '');
} else {
const response = await fetch(`${API_BASE}/agents/${agentId}/diary/${date}/`);
const data = await response.json();
setContent(data.content || '');
}
} catch (error) {
console.error('加载内容失败:', error);
setContent('');
} finally {
setLoading(false);
}
};
// 渲染日历
const renderCalendar = () => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startWeekday = firstDay.getDay();
const days = [];
const hasContentDates = activeTab === 'memory' ? dates : diaryDates;
// 空白格子
for (let i = 0; i < startWeekday; i++) {
days.push(<div key={`empty-${i}`} className="calendar-day empty"></div>);
}
// 日期格子
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const hasContent = hasContentDates.includes(dateStr);
const isSelected = selectedDate === dateStr;
days.push(
<div
key={day}
className={`calendar-day ${hasContent ? 'has-memory' : ''} ${isSelected ? 'selected' : ''}`}
onClick={() => hasContent && setSelectedDate(dateStr)}
>
{day}
</div>
);
}
return days;
};
const changeMonth = (delta) => {
setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() + delta)));
};
const currentDates = activeTab === 'memory' ? dates : diaryDates;
const title = activeTab === 'memory' ? '📔 工作记忆' : '📖 成长之路';
const emptyText = activeTab === 'memory' ? '这一天还没有工作记忆' : '这一天还没有日记';
return (
<div className="memory-modal-overlay" onClick={onClose}>
<div className="memory-modal" onClick={e => e.stopPropagation()}>
<div className="memory-modal-header">
<h2>{title} - {agentName}</h2>
<button className="close-btn" onClick={onClose}>×</button>
</div>
{/* 切换标签 */}
<div className="tab-header">
<button
className={`tab-btn ${activeTab === 'diary' ? 'active' : ''}`}
onClick={() => setActiveTab('diary')}
>
📖 成长之路
</button>
<button
className={`tab-btn ${activeTab === 'memory' ? 'active' : ''}`}
onClick={() => setActiveTab('memory')}
>
📔 工作记忆
</button>
</div>
<div className="memory-modal-body">
{/* 内容区域 */}
<div className="memory-content">
{loading ? (
<div className="loading-state">加载中...</div>
) : content ? (
<div className="memory-text">
<h3>📅 {selectedDate}</h3>
<pre>{content}</pre>
</div>
) : (
<div className="memory-empty">
<p>{emptyText}</p>
</div>
)}
</div>
{/* 日历 */}
<div className="memory-calendar-panel">
<div className="calendar-header">
<button onClick={() => changeMonth(-1)}></button>
<span>{currentMonth.getFullYear()} {currentMonth.getMonth() + 1}</span>
<button onClick={() => changeMonth(1)}></button>
</div>
<div className="calendar-grid">
{['日', '一', '二', '三', '四', '五', '六'].map(day => (
<div key={day} className="calendar-weekday">{day}</div>
))}
{renderCalendar()}
</div>
<div className="calendar-legend">
<span className="legend-item has-memory"> 有内容</span>
<span className="legend-item no-memory"> 无内容</span>
</div>
<div className="calendar-stats">
<span className="stat-badge">
📅 本月 {currentDates.filter(d =>
d.startsWith(`${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`)
).length}
</span>
</div>
</div>
</div>
</div>
<style>{`
.memory-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.memory-modal {
background: white;
border-radius: 16px;
width: 90%;
max-width: 1000px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.memory-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e2e8f0;
}
.memory-modal-header h2 {
color: #1a365d;
margin: 0;
font-size: 1.4em;
}
.close-btn {
background: none;
border: none;
font-size: 2em;
cursor: pointer;
color: #718096;
}
.close-btn:hover {
color: #2d3748;
}
.tab-header {
display: flex;
border-bottom: 2px solid #e2e8f0;
}
.tab-btn {
flex: 1;
padding: 15px 20px;
background: #f7fafc;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 1em;
font-weight: 600;
color: #718096;
transition: all 0.2s;
}
.tab-btn:hover {
background: #edf2f7;
color: #4a5568;
}
.tab-btn.active {
background: white;
color: #553c9a;
border-bottom-color: #553c9a;
}
.memory-modal-body {
display: grid;
grid-template-columns: 1fr 350px;
gap: 20px;
padding: 20px;
overflow: hidden;
}
.memory-content {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
overflow-y: auto;
max-height: 600px;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #718096;
font-size: 1.1em;
}
.memory-text h3 {
color: #553c9a;
margin-bottom: 15px;
}
.memory-text pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #2d3748;
line-height: 1.6;
}
.memory-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #a0aec0;
font-size: 1.1em;
}
.memory-calendar-panel {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
background: white;
display: flex;
flex-direction: column;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.calendar-header button {
background: #4299e1;
color: white;
border: none;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
}
.calendar-header button:hover {
background: #3182ce;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
flex: 1;
}
.calendar-weekday {
text-align: center;
font-size: 0.8em;
color: #718096;
padding: 8px 0;
font-weight: 500;
}
.calendar-day {
width: 100% !important;
height: 36px !important;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 0.85em;
cursor: pointer;
transition: all 0.2s;
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box;
}
.calendar-day:hover:not(.empty) {
background: #e2e8f0;
}
.calendar-day.empty {
cursor: default;
}
.calendar-day.has-memory {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
font-weight: bold !important;
}
.calendar-day.selected {
border: 2px solid #ed8936 !important;
background: #f6ad55 !important;
color: white !important;
}
.calendar-legend {
margin-top: 10px;
display: flex;
gap: 20px;
font-size: 0.85em;
padding-top: 10px;
border-top: 1px solid #e2e8f0;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.legend-item.has-memory::before {
content: '●';
color: #667eea;
}
.legend-item.no-memory::before {
content: '○';
color: #a0aec0;
}
.calendar-stats {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: center;
}
.stat-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
}
@media (max-width: 768px) {
.memory-modal-body {
grid-template-columns: 1fr;
}
.memory-calendar-panel {
order: -1;
}
.tab-btn {
padding: 12px 15px;
font-size: 0.9em;
}
}
`}</style>
</div>
);
}
export default MemoryModal;