Files
sitemente/components/mission-control/MissionControlDashboard.tsx
T
2026-02-16 13:19:21 +00:00

270 lines
16 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";
import { useMissionControl } from "@/lib/mission-control/store";
import { TaskStatus } from "@/lib/mission-control/types";
import VoiceChat from "./VoiceChat";
import AIManagement from "@/components/ai-management/AIManagement";
import Council from "@/components/council/Council";
interface SidebarItem {
id: string;
name: string;
icon: string;
color?: string;
category: string;
}
interface SidebarCategory {
id: string;
name: string;
icon: string;
items: SidebarItem[];
}
const sidebarCategories: SidebarCategory[] = [
{ id: "projects", name: "Projects", icon: "🎯", items: [
{ id: "sitemente", name: "SiteMente", icon: "🌐", color: "#ff7bc0", category: "projects" },
{ id: "holacompi", name: "HolaCompi", icon: "🤝", color: "#6366f1", category: "projects" },
]},
{ id: "tasks", name: "Tasks", icon: "✓", items: [
{ id: "all", name: "All Tasks", icon: "📋", category: "tasks" },
]},
{ id: "chat", name: "Chat", icon: "💬", items: [
{ id: "voice", name: "Voice Chat", icon: "🎤", category: "chat" },
]},
{ id: "council", name: "Council", icon: "🏛️", items: [
{ id: "teams", name: "Agent Teams", icon: "👥", category: "council" },
{ id: "ai-settings", name: "AI Settings", icon: "🤖", category: "council-settings" },
]},
{ id: "calendar", name: "Calendar", icon: "📅", items: [
{ id: "brief", name: "Morning Brief", icon: "☀️", category: "calendar" },
]},
{ id: "memory", name: "Memory", icon: "🧠", items: [
{ id: "logs", name: "Session Logs", icon: "📝", category: "memory" },
]},
];
const statusConfig: Record<TaskStatus, { label: string; color: string }> = {
todo: { label: "To Do", color: "text-white/70" },
in_progress: { label: "In Progress", color: "text-yellow-400" },
done: { label: "Done", color: "text-green-400" },
blocked: { label: "Blocked", color: "text-red-400" },
paused: { label: "Paused", color: "text-gray-400" },
};
export default function MissionControlDashboard() {
const { tasks, toggleTask, updateTaskStatus, addTask, getProjectProgress, getTasksByProject } = useMissionControl();
const [selectedItem, setSelectedItem] = useState<string>("sitemente");
const [filter, setFilter] = useState<TaskStatus | "all">("all");
const [expandedCategories, setExpandedCategories] = useState<string[]>(["projects"]);
const [searchQuery, setSearchQuery] = useState("");
const [showAddTask, setShowAddTask] = useState(false);
const [newTaskTitle, setNewTaskTitle] = useState("");
const [newTaskProject, setNewTaskProject] = useState("sitemente");
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "/" && !e.ctrlKey && !e.metaKey) {
const target = e.target as HTMLElement;
if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") {
e.preventDefault();
document.getElementById("search-input")?.focus();
}
}
if (e.key === "n" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
setShowAddTask(true);
}
if (e.key === "Escape") {
setShowAddTask(false);
setSearchQuery("");
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
let selectedCategory = sidebarCategories.find(c => c.items.some(i => i.id === selectedItem));
let currentItem = selectedCategory?.items.find(i => i.id === selectedItem);
const projectTasks = currentItem?.category === "projects" ? getTasksByProject(selectedItem as any) : tasks;
const filteredTasks = filter === "all" ? projectTasks : projectTasks.filter((t) => t.status === filter);
const searchedTasks = searchQuery ? filteredTasks.filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase())) : filteredTasks;
const progress = currentItem?.category === "projects" ? getProjectProgress(selectedItem as any) : 0;
const toggleCategory = (catId: string) => setExpandedCategories(prev => prev.includes(catId) ? prev.filter(id => id !== catId) : [...prev, catId]);
const collapseAll = () => setExpandedCategories([]);
const todayTasks = projectTasks.filter(t => t.priority === "critical" || t.status === "in_progress").slice(0, 3);
const exportTasks = () => {
const data = JSON.stringify(projectTasks, null, 2);
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `tasks-${new Date().toISOString().split("T")[0]}.json`;
a.click();
};
const handleAddTask = () => {
if (!newTaskTitle.trim()) return;
addTask({ title: newTaskTitle, description: "", status: "todo", priority: "medium", project: newTaskProject as any });
setShowAddTask(false);
setNewTaskTitle("");
};
return (
<div className="min-h-screen bg-[#1a1625] text-white flex">
<aside className="w-64 border-r border-white/10 bg-[#1a1625] p-4">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-white/10">
<div className="w-10 h-10 rounded-xl bg-brand-pink flex items-center justify-center text-xl">👁</div>
<div><h1 className="font-bold">Mission Control</h1><p className="text-xs text-white/50">Horus AI Assistant</p></div>
</div>
<nav className="space-y-1">
{sidebarCategories.map((category) => (
<div key={category.id}>
<button onClick={() => toggleCategory(category.id)} className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium text-white/70 hover:bg-white/5 hover:text-white transition">
<span className="flex items-center gap-2"><span>{category.icon}</span><span>{category.name}</span></span>
<span className={`transition ${expandedCategories.includes(category.id) ? "rotate-90" : ""}`}></span>
</button>
{expandedCategories.includes(category.id) && (
<div className="ml-4 mt-1 space-y-1">
{category.items.map((item) => (
<button key={item.id} onClick={() => setSelectedItem(item.id)} className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition ${selectedItem === item.id ? "bg-white/10 text-white border border-white/20" : "text-white/60 hover:bg-white/5 hover:text-white"}`}>
<span>{item.icon}</span><span>{item.name}</span>
</button>
))}
</div>
)}
</div>
))}
</nav>
<div className="mt-6 pt-4 border-t border-white/10">
<p className="text-xs text-white/40 uppercase mb-2 px-3">Quick</p>
<a href="/" className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/5 hover:text-white transition">🏠 SiteMente Site</a>
<button onClick={collapseAll} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/5 hover:text-white transition"> Collapse All</button>
</div>
</aside>
<main className="flex-1 p-6 overflow-y-auto">
<header className="flex items-center justify-between mb-6 pb-4 border-b border-white/10">
<div className="flex items-center gap-4">
<button onClick={() => { setSelectedItem("sitemente"); setExpandedCategories(["projects"]); }} className="w-10 h-10 rounded-xl bg-brand-pink flex items-center justify-center text-xl hover:bg-[#ff7bc0] transition">👁</button>
<div>
<h2 className="text-xl font-bold flex items-center gap-2"><span>{currentItem?.icon}</span><span>{currentItem?.name}</span></h2>
<p className="text-sm text-white/50">{currentItem?.category === "projects" ? `${progress}% complete` : `${tasks.length} total tasks`}</p>
</div>
</div>
{currentItem?.category === "projects" && (
<div className="h-12 w-12 rounded-full border-4 border-brand-pink bg-white/10">
<svg className="h-full w-full -rotate-90" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="16" fill="none" stroke="currentColor" strokeWidth="3" className="text-white/20" />
<circle cx="18" cy="18" r="16" fill="none" stroke="currentColor" strokeWidth="3" strokeDasharray={`${progress}, 100`} className="text-brand-pink" />
</svg>
</div>
)}
</header>
{currentItem?.category === "chat" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><VoiceChat /></div>}
{currentItem?.category === "council" && currentItem?.id === "teams" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><Council /></div>}
{currentItem?.category === "council-settings" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><AIManagement /></div>}
{currentItem?.category === "calendar" && (
<div className="space-y-4">
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h3 className="text-lg font-semibold mb-4">📅 Morning Brief</h3>
<p className="text-white/60 mb-4">Daily intelligence at 6am CET</p>
<a href="/morning-brief" className="inline-flex items-center gap-2 px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition"> Open Calendar</a>
</div>
<VoiceChat />
</div>
)}
{currentItem?.category === "memory" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h3 className="text-lg font-semibold mb-4">🧠 Memory & Logs</h3>
<p className="text-white/60 mb-4">Session history and daily logs</p>
<div className="space-y-2 text-sm text-white/70"><p>Memory is stored in:</p><ul className="text-white/50 space-y-1"><li> localStorage (browser)</li><li> GitHub repo (daily commits)</li><li> MEMORY.md (curated)</li></ul></div>
</div>
)}
{currentItem?.category === "projects" && (
<>
{todayTasks.length > 0 && (
<div className="mb-6 p-4 rounded-xl border border-brand-pink/30 bg-brand-pink/10">
<p className="text-xs text-brand-pink font-medium mb-2">🎯 TODAY'S FOCUS</p>
<div className="space-y-2">{todayTasks.map(task => (
<div key={task.id} className="flex items-center gap-2 text-sm">
<span className={`w-2 h-2 rounded-full ${task.status === "in_progress" ? "bg-yellow-400" : "bg-red-400"}`} />
<span className="truncate">{task.title}</span>
</div>
))}</div>
</div>
)}
<div className="flex items-center gap-3 mb-4">
<div className="flex-1 relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40">🔍</span>
<input id="search-input" type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search tasks... (press /)" className="w-full bg-white/10 border border-white/20 rounded-lg pl-9 pr-4 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink" />
</div>
<button onClick={() => setShowAddTask(!showAddTask)} className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">+ Add</button>
<button onClick={exportTasks} className="px-3 py-2 bg-white/10 rounded-lg text-sm hover:bg-white/20 transition">📥</button>
</div>
{showAddTask && (
<div className="mb-4 p-4 rounded-xl border border-brand-pink/30 bg-brand-pink/5">
<input type="text" value={newTaskTitle} onChange={(e) => setNewTaskTitle(e.target.value)} placeholder="New task title..." className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink mb-3" autoFocus onKeyDown={(e) => e.key === "Enter" && handleAddTask()} />
<div className="flex items-center gap-2">
<select value={newTaskProject} onChange={(e) => setNewTaskProject(e.target.value)} className="bg-white/10 border border-white/20 rounded-lg px-3 py-1.5 text-sm">
<option value="sitemente">SiteMente</option>
<option value="holacompi">HolaCompi</option>
<option value="infrastructure">Infrastructure</option>
</select>
<button onClick={handleAddTask} className="px-4 py-1.5 bg-brand-pink rounded-lg text-sm font-medium">Add Task</button>
<button onClick={() => setShowAddTask(false)} className="px-3 py-1.5 text-sm text-white/60 hover:text-white">Cancel</button>
</div>
</div>
)}
<div className="mb-4 grid grid-cols-4 gap-3">
{(["todo", "in_progress", "done", "blocked"] as TaskStatus[]).map((status) => {
const count = projectTasks.filter((t) => t.status === status).length;
const config = statusConfig[status];
return (
<button key={status} onClick={() => setFilter(filter === status ? "all" : status)} className={`rounded-xl border p-3 text-center transition ${filter === status ? "border-white/40 bg-white/10" : "border-white/10 bg-white/5 hover:border-white/20"}`}>
<p className={`text-xl font-bold ${config.color}`}>{count}</p>
<p className="text-xs text-white/50">{config.label}</p>
</button>
);
})}
</div>
<div className="rounded-xl border border-white/10 bg-white/5">
<div className="border-b border-white/10 px-4 py-3">
<div className="flex items-center justify-between">
<h2 className="font-semibold">{currentItem?.name}{searchQuery && <span className="ml-2 text-white/50 text-sm">({searchedTasks.length} results)</span>}</h2>
<p className="text-sm text-white/50">{filteredTasks.filter((t) => t.status === "done").length} / {filteredTasks.length} done</p>
</div>
</div>
<div className="divide-y divide-white/5 max-h-[500px] overflow-y-auto">
{searchedTasks.map((task) => (
<div key={task.id} className={`flex items-center gap-3 px-4 py-3 transition hover:bg-white/5 ${task.status === "done" ? "opacity-50" : ""}`}>
<button onClick={() => toggleTask(task.id)} className={`flex-shrink-0 w-5 h-5 rounded-full border-2 flex items-center justify-center transition ${task.status === "done" ? "border-green-500 bg-green-500 text-white" : "border-white/30 hover:border-white/50"}`}>
{task.status === "done" && <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>}
</button>
<div className="flex-1 min-w-0"><p className={`text-sm font-medium truncate ${task.status === "done" ? "line-through text-white/50" : ""}`}>{task.title}</p></div>
{task.priority === "critical" && <span className="px-2 py-0.5 rounded-full bg-red-500/20 text-red-400 text-xs">CRITICAL</span>}
<select value={task.status} onChange={(e) => updateTaskStatus(task.id, e.target.value as TaskStatus)} className="text-xs bg-transparent border border-white/20 rounded px-2 py-1">
<option value="todo">To Do</option><option value="in_progress">In Progress</option><option value="done">Done</option><option value="blocked">Blocked</option>
</select>
</div>
))}
</div>
{searchedTasks.length === 0 && <div className="px-4 py-8 text-center text-white/50 text-sm">No tasks match.</div>}
</div>
</>
)}
</main>
</div>
);
}