Files
sitemente/app/mission-control/memory/page.tsx
T
horus 45af56d9cf feat(mission-control): restore MC tabs - temple, office, memory, claude, pdf-viewer, resume, resume-upload, temple-3d, demos
Also added:
- Memory API endpoints
- Briefs API endpoints
- AnveVoice stats API
- Claude spawn API
- TTS proxy
- Cleopatra voice widget
- api-auth middleware
2026-03-23 16:30:44 +01:00

521 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
// Only 3 main agents show as dots
const MAIN_AGENTS = [
{ id: "horus", name: "Horus", icon: "👁️", color: "#3b82f6" },
{ id: "cleopatra", name: "Cleopatra", icon: "👸", color: "#a855f7" },
{ id: "amun", name: "Amun", icon: "👑", color: "#f59e0b" },
];
// All agents for tag detection
const ALL_AGENTS = [
...MAIN_AGENTS,
{ id: "anubis", name: "Anubis", icon: "🐕", color: "#22c55e" },
{ id: "thoth", name: "Thoth", icon: "📚", color: "#06b6d4" },
{ id: "ptah", name: "Ptah", icon: "🎨", color: "#f97316" },
{ id: "seshat", name: "Seshat", icon: "📝", color: "#ec4899" },
{ id: "hathor", name: "Hathor", icon: "💕", color: "#ef4444" },
{ id: "sekhmet", name: "Sekhmet", icon: "⚔️", color: "#94a3b8" },
{ id: "maat", name: "Maat", icon: "⚖️", color: "#64748b" },
];
const INACTIVE_COLOR = "#4b5563";
interface DayEntry {
agent: string;
agentIcon: string;
agentColor: string;
time: string;
content: string;
tags: string[];
isMainAgent: boolean;
}
interface MemoryDay {
date: string;
entries: DayEntry[];
hasBriefs: { morning: boolean; eod: boolean };
}
interface CalendarDay {
date: string;
day: number;
isCurrentMonth: boolean;
isToday: boolean;
activeMainAgents: string[];
entries: DayEntry[];
hasBriefs: { morning: boolean; eod: boolean };
}
export default function MemoryPage() {
const [memoryDays, setMemoryDays] = useState<MemoryDay[]>([]);
const [selectedDay, setSelectedDay] = useState<MemoryDay | null>(null);
const [currentMonth, setCurrentMonth] = useState(new Date());
const [loading, setLoading] = useState(true);
const [filterAgent, setFilterAgent] = useState<string | null>(null);
const [filterTag, setFilterTag] = useState<string | null>(null);
const [allTags, setAllTags] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [editingEntry, setEditingEntry] = useState<DayEntry | null>(null);
const [editTags, setEditTags] = useState<string>("");
useEffect(() => {
fetchMemory();
}, []);
const fetchMemory = async () => {
setLoading(true);
try {
const res = await fetch("/api/memory/list", {
headers: { 'Authorization': 'Bearer horus-mc-secret-2026' }
});
const data = await res.json();
setMemoryDays(data.days || []);
setAllTags(data.allTags || []);
} catch (e) {
console.error("Failed to fetch memory:", e);
}
setLoading(false);
};
const getAgentInfo = (agentName: string) => {
const lower = agentName.toLowerCase();
const agent = ALL_AGENTS.find(a => lower.includes(a.id) || lower.includes(a.name.toLowerCase()));
return agent || { name: agentName, icon: "🤖", color: INACTIVE_COLOR };
};
const isMainAgent = (agentName: string) => {
const lower = agentName.toLowerCase();
return MAIN_AGENTS.some(a => lower.includes(a.id) || lower.includes(a.name.toLowerCase()));
};
const getCalendarDays = (): CalendarDay[] => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startPad = firstDay.getDay();
const days: CalendarDay[] = [];
const today = new Date().toISOString().split("T")[0];
const monthDays = memoryDays.filter((d) => d.date.startsWith(`${year}-${String(month + 1).padStart(2, "0")}`));
for (let i = startPad - 1; i >= 0; i--) {
const d = new Date(year, month, -i);
days.push({
date: d.toISOString().split("T")[0],
day: d.getDate(),
isCurrentMonth: false,
isToday: false,
activeMainAgents: [],
entries: [],
hasBriefs: { morning: false, eod: false },
});
}
for (let i = 1; i <= lastDay.getDate(); i++) {
const date = `${year}-${String(month + 1).padStart(2, "0")}-${String(i).padStart(2, "0")}`;
const memDay = monthDays.find((d) => d.date === date);
const mainAgents = memDay?.entries
.filter((e) => e.isMainAgent)
.map((e) => e.agent.toLowerCase()) || [];
days.push({
date,
day: i,
isCurrentMonth: true,
isToday: date === today,
activeMainAgents: [...new Set(mainAgents)],
entries: memDay?.entries || [],
hasBriefs: memDay?.hasBriefs || { morning: false, eod: false },
});
}
const remaining = 42 - days.length;
for (let i = 1; i <= remaining; i++) {
const d = new Date(year, month + 1, i);
days.push({
date: d.toISOString().split("T")[0],
day: d.getDate(),
isCurrentMonth: false,
isToday: false,
activeMainAgents: [],
entries: [],
hasBriefs: { morning: false, eod: false },
});
}
return days;
};
const getFilteredEntries = () => {
if (!selectedDay) return [];
let entries = selectedDay.entries;
if (filterAgent) {
entries = entries.filter((e) => e.agent.toLowerCase() === filterAgent.toLowerCase());
}
if (filterTag) {
entries = entries.filter((e) => e.tags.includes(filterTag));
}
if (searchQuery) {
entries = entries.filter((e) =>
e.content.toLowerCase().includes(searchQuery.toLowerCase())
);
}
return entries;
};
const calendarDays = getCalendarDays();
const filteredEntries = getFilteredEntries();
const getMainAgentDots = (activeMainAgents: string[]) => {
return MAIN_AGENTS.map((agent) => ({
agent,
active: activeMainAgents.some((a) => a.includes(agent.id) || a.includes(agent.name.toLowerCase())),
}));
};
const handleEditTags = (entry: DayEntry) => {
setEditingEntry(entry);
setEditTags(entry.tags.join(", "));
};
const saveTags = async () => {
if (!editingEntry || !selectedDay) return;
// In a real app, this would POST to an API to update the file
// For now, just update the local state
const newTags = editTags.split(",").map((t) => t.trim().replace(/^#/, "")).filter(Boolean);
setSelectedDay({
...selectedDay,
entries: selectedDay.entries.map((e) =>
e === editingEntry ? { ...e, tags: newTags } : e
),
});
setEditingEntry(null);
setEditTags("");
// TODO: POST to /api/memory/tags to save
};
return (
<div className="min-h-screen bg-slate-950 flex">
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex-shrink-0">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">📅 Memory Calendar</h1>
<p className="text-slate-400 text-sm">Our unified memory system</p>
</div>
<button
onClick={fetchMemory}
className="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded"
>
Refresh
</button>
</div>
</div>
{/* Calendar */}
<div className="flex-1 overflow-auto p-6">
{/* Month Navigation */}
<div className="flex items-center justify-between mb-4">
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded"
>
</button>
<h2 className="text-xl font-bold text-white">
{currentMonth.toLocaleDateString("en-US", { month: "long", year: "numeric" })}
</h2>
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded"
>
</button>
</div>
{/* Day Headers */}
<div className="grid grid-cols-7 gap-1 mb-1">
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((d) => (
<div key={d} className="text-center text-slate-500 text-sm py-2">
{d}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-1">
{calendarDays.map((day, idx) => {
const dots = getMainAgentDots(day.activeMainAgents);
const isSelected = selectedDay?.date === day.date;
const hasEntries = day.entries.length > 0;
return (
<button
key={idx}
onClick={() => setSelectedDay(day)}
className={`
relative p-2 min-h-[80px] rounded-lg border transition-all
${day.isCurrentMonth ? "bg-slate-800/50 border-slate-700 hover:bg-slate-800" : "bg-slate-900/30 border-slate-800 opacity-50"}
${day.isToday ? "border-amber-500" : ""}
${isSelected ? "border-brand-pink bg-slate-800" : ""}
`}
>
<span className={`text-sm ${day.isCurrentMonth ? "text-white" : "text-slate-600"} ${day.isToday ? "font-bold text-amber-400" : ""}`}>
{day.day}
</span>
{/* Briefs Indicators */}
<div className="flex gap-1 mt-1">
{day.hasBriefs.morning && <span className="text-xs"></span>}
{day.hasBriefs.eod && <span className="text-xs">🌙</span>}
</div>
{/* Main Agent Dots (only 3) */}
<div className="flex gap-1 mt-1 justify-center">
{dots.map(({ agent, active }) => (
<span
key={agent.id}
className="w-3 h-3 rounded-full"
style={{ backgroundColor: active ? agent.color : INACTIVE_COLOR }}
title={`${agent.name}: ${active ? "Active" : "Inactive"}`}
/>
))}
</div>
{/* Tag indicator if has entries */}
{hasEntries && !day.activeMainAgents.length && (
<div className="flex gap-1 mt-1 justify-center">
<span className="text-xs bg-slate-600 text-white px-1 rounded">📝</span>
</div>
)}
</button>
);
})}
</div>
{/* Legend */}
<div className="mt-4 p-4 bg-slate-800/50 rounded-lg border border-slate-700">
<h3 className="text-white font-medium mb-2">Dots = Active Agents</h3>
<div className="flex flex-wrap gap-4">
{MAIN_AGENTS.map((agent) => (
<div key={agent.id} className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: agent.color }} />
<span className="text-slate-400 text-xs">{agent.icon} {agent.name}</span>
</div>
))}
<div className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: INACTIVE_COLOR }} />
<span className="text-slate-400 text-xs">Inactive</span>
</div>
<div className="flex items-center gap-1">
<span className="text-slate-400 text-xs">📝 = Has entries (other agents)</span>
</div>
</div>
</div>
</div>
</div>
{/* Right Sidebar - Day Detail */}
<div className="w-[28rem] bg-slate-900 border-l border-slate-800 flex flex-col overflow-hidden">
{selectedDay ? (
<>
{/* Day Header */}
<div className="p-4 border-b border-slate-800 flex-shrink-0">
<h2 className="text-xl font-bold text-white">
{new Date(selectedDay.date).toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
})}
</h2>
<div className="flex gap-2 mt-2">
{selectedDay.hasBriefs.morning && (
<a
href={`/api/briefs/morning/${selectedDay.date}`}
target="_blank"
className="text-xs bg-amber-500/20 text-amber-400 px-2 py-1 rounded hover:bg-amber-500/30"
>
Morning Brief
</a>
)}
{selectedDay.hasBriefs.eod && (
<a
href={`/api/briefs/eod/${selectedDay.date}`}
target="_blank"
className="text-xs bg-purple-500/20 text-purple-400 px-2 py-1 rounded hover:bg-purple-500/30"
>
🌙 EOD Brief
</a>
)}
</div>
</div>
{/* Filters */}
<div className="p-4 border-b border-slate-800 flex-shrink-0">
<input
type="text"
placeholder="Search entries..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm placeholder-slate-500"
/>
<div className="flex gap-2 mt-2 flex-wrap">
<button
onClick={() => setFilterAgent(null)}
className={`px-2 py-1 text-xs rounded ${!filterAgent ? "bg-brand-pink text-white" : "bg-slate-800 text-slate-400"}`}
>
All
</button>
{MAIN_AGENTS.map((agent) => (
<button
key={agent.id}
onClick={() => setFilterAgent(filterAgent === agent.name ? null : agent.name)}
className={`px-2 py-1 text-xs rounded flex items-center gap-1 ${
filterAgent === agent.name ? "bg-brand-pink text-white" : "bg-slate-800 text-slate-400"
}`}
>
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: agent.color }} />
{agent.icon}
</button>
))}
</div>
{allTags.length > 0 && (
<div className="flex gap-1 mt-2 flex-wrap">
{allTags.slice(0, 8).map((tag) => (
<button
key={tag}
onClick={() => setFilterTag(filterTag === tag ? null : tag)}
className={`px-2 py-0.5 text-xs rounded ${
filterTag === tag ? "bg-amber-500 text-black" : "bg-slate-700 text-slate-400 hover:bg-slate-600"
}`}
>
#{tag}
</button>
))}
</div>
)}
</div>
{/* Entries */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{loading ? (
<div className="text-slate-500 text-center">Loading...</div>
) : filteredEntries.length === 0 ? (
<div className="text-slate-500 text-center">No entries for this day</div>
) : (
filteredEntries.map((entry, idx) => (
<div key={idx} className="bg-slate-800/50 rounded-lg p-3 border border-slate-700">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span
className="w-6 h-6 rounded-full flex items-center justify-center text-sm"
style={{ backgroundColor: entry.agentColor + "33" }}
>
{entry.agentIcon}
</span>
<span className="text-white font-medium text-sm">{entry.agent}</span>
<span className="text-slate-500 text-xs">{entry.time}</span>
</div>
<button
onClick={() => handleEditTags(entry)}
className="text-slate-500 hover:text-white text-xs"
title="Edit tags"
>
🏷
</button>
</div>
<pre className="text-slate-300 text-xs whitespace-pre-wrap font-mono">
{entry.content.length > 250
? entry.content.slice(0, 250) + "..."
: entry.content}
</pre>
{/* Tags */}
<div className="flex gap-1 mt-2 flex-wrap">
{entry.tags.map((tag) => (
<span key={tag} className="px-2 py-0.5 bg-amber-500/20 text-amber-400 text-xs rounded">
#{tag}
</span>
))}
</div>
</div>
))
)}
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-5xl mb-4">📅</div>
<p className="text-slate-500">Select a day to view entries</p>
</div>
</div>
)}
{/* Who's Active */}
<div className="p-4 border-t border-slate-800 flex-shrink-0">
<h3 className="text-slate-400 text-xs uppercase mb-2">Today</h3>
<div className="flex gap-2">
{MAIN_AGENTS.map((agent) => {
const isActive = memoryDays[0]?.entries.some((e) =>
e.agent.toLowerCase().includes(agent.id)
);
return (
<div key={agent.id} className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: isActive ? agent.color : INACTIVE_COLOR }} />
<span className="text-slate-400 text-xs">{agent.icon}</span>
</div>
);
})}
</div>
</div>
</div>
{/* Edit Tags Modal */}
{editingEntry && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-slate-800 rounded-xl p-6 w-96 border border-slate-600">
<h3 className="text-white font-bold mb-4">Edit Tags</h3>
<div className="flex items-center gap-2 mb-2">
<span className="text-xl">{editingEntry.agentIcon}</span>
<span className="text-white">{editingEntry.agent}</span>
</div>
<input
type="text"
value={editTags}
onChange={(e) => setEditTags(e.target.value)}
placeholder="tag1, tag2, tag3"
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white text-sm placeholder-slate-400 mb-4"
/>
<div className="flex gap-2 justify-end">
<button
onClick={() => setEditingEntry(null)}
className="px-4 py-2 bg-slate-700 text-white rounded hover:bg-slate-600"
>
Cancel
</button>
<button
onClick={saveTags}
className="px-4 py-2 bg-brand-pink text-white rounded hover:bg-[#ff7bc0]"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
);
}