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
This commit is contained in:
2026-03-23 16:30:44 +01:00
parent d5575b58e3
commit 45af56d9cf
30 changed files with 5092 additions and 715 deletions
+283
View File
@@ -0,0 +1,283 @@
"use client";
import { useState, useEffect } from "react";
interface Tab {
id: string;
name: string;
icon: string;
color: string;
projectPath?: string;
status?: "inactive" | "starting" | "active";
sessionKey?: string;
}
const TABS_STORAGE_KEY = "mc_claude_tabs";
export default function ClaudePage() {
const [tabs, setTabs] = useState<Tab[]>([
{ id: "main", name: "Claude Code", icon: "🤖", color: "#ff6154", status: "inactive" },
]);
const [activeTab, setActiveTab] = useState("main");
const [showSettings, setShowSettings] = useState(false);
const [apiKey, setApiKey] = useState("");
const [message, setMessage] = useState("");
useEffect(() => {
const saved = localStorage.getItem(TABS_STORAGE_KEY);
if (saved) {
try {
setTabs(JSON.parse(saved));
} catch (e) {}
}
const savedKey = localStorage.getItem("anthropic_api_key") || "";
setApiKey(savedKey);
}, []);
const saveTabs = (newTabs: Tab[]) => {
localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(newTabs));
setTabs(newTabs);
};
const spawnSession = async (tabId: string) => {
const tab = tabs.find(t => t.id === tabId);
if (!tab) return;
// Update status to starting
const newTabs = tabs.map(t =>
t.id === tabId ? { ...t, status: "starting" as const } : t
);
saveTabs(newTabs);
try {
const response = await fetch("/api/claude/spawn", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tabId, tabName: tab.name, apiKey }),
});
const data = await response.json();
if (data.success) {
// Update with session info
const finalTabs = tabs.map(t =>
t.id === tabId
? { ...t, status: "active" as const, sessionKey: data.sessionKey }
: t
);
saveTabs(finalTabs);
setMessage(`Session spawned! Ask Horus to connect.`);
} else {
setMessage(`Error: ${data.error}`);
}
} catch (e) {
setMessage(`Connection failed. Try asking Horus directly.`);
}
};
const addTab = () => {
const name = prompt("Project name:");
if (!name) return;
const colors = ["#ff6154", "#3b82f6", "#22c55e", "#f59e0b", "#8b5cf6", "#ec4899"];
const newTab: Tab = {
id: `tab-${Date.now()}`,
name,
icon: "📁",
color: colors[Math.floor(Math.random() * colors.length)],
status: "inactive",
};
saveTabs([...tabs, newTab]);
setActiveTab(newTab.id);
};
const removeTab = (tabId: string) => {
if (tabs.length <= 1) return;
const newTabs = tabs.filter(t => t.id !== tabId);
saveTabs(newTabs);
if (activeTab === tabId) {
setActiveTab(newTabs[0].id);
}
};
const activeTabData = tabs.find(t => t.id === activeTab);
return (
<div className="min-h-screen bg-slate-950 flex flex-col">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">🤖 Claude Code Sessions</h1>
<p className="text-slate-400 text-sm">Spawn coding agents powered by Claude</p>
</div>
<button
onClick={() => setShowSettings(true)}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
>
Settings
</button>
</div>
{/* Tabs Bar */}
<div className="bg-slate-900/50 border-b border-slate-800 px-4 py-2 flex items-center gap-2 overflow-x-auto">
{tabs.map((tab) => (
<div
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg cursor-pointer transition-all text-sm font-medium whitespace-nowrap
${activeTab === tab.id
? "bg-slate-800 text-white border border-slate-600"
: "bg-slate-800/50 text-slate-400 border border-transparent hover:bg-slate-800 hover:text-white"
}
`}
>
<span>{tab.icon}</span>
<span>{tab.name}</span>
{tab.status === "active" && (
<span className="w-2 h-2 rounded-full bg-green-500" title="Active" />
)}
{tab.status === "starting" && (
<span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" title="Starting..." />
)}
{tabs.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }}
className="ml-1 text-slate-500 hover:text-red-400 text-xs"
>
</button>
)}
</div>
))}
<button
onClick={addTab}
className="bg-slate-800/50 hover:bg-slate-800 text-slate-400 hover:text-white px-3 py-2 rounded-lg text-sm border border-dashed border-slate-600"
>
+ Add Project
</button>
</div>
{/* Content */}
<div className="flex-1 flex items-center justify-center p-8">
<div className="text-center max-w-lg">
{activeTabData?.status === "inactive" && (
<>
<div className="text-6xl mb-6" style={{ color: activeTabData.color }}>
{activeTabData.icon}
</div>
<h2 className="text-2xl font-bold text-white mb-2">{activeTabData.name}</h2>
<p className="text-slate-400 mb-6">
Start a Claude Code session for this project
</p>
<button
onClick={() => spawnSession(activeTab)}
className="bg-[#ff6154] hover:bg-[#ff4f3a] text-white px-8 py-4 rounded-xl font-bold text-lg transition-all transform hover:scale-105 shadow-lg"
style={{ boxShadow: "0 4px 20px rgba(255, 97, 84, 0.4)" }}
>
🚀 Start Claude Session
</button>
</>
)}
{activeTabData?.status === "starting" && (
<>
<div className="text-6xl mb-6 animate-pulse"></div>
<h2 className="text-2xl font-bold text-white mb-2">Starting Session...</h2>
<p className="text-slate-400">Connecting to Claude Code</p>
</>
)}
{activeTabData?.status === "active" && (
<>
<div className="text-6xl mb-6"></div>
<h2 className="text-2xl font-bold text-white mb-2">Session Active!</h2>
<p className="text-slate-400 mb-4">
Session ID: <code className="text-amber-400">{activeTabData.sessionKey?.slice(0, 12)}...</code>
</p>
<div className="bg-slate-800 rounded-lg p-4 text-left text-sm">
<p className="text-slate-300 mb-2">
💡 <strong>To use this session:</strong>
</p>
<p className="text-slate-400">
Ask Horus to connect to this session for coding tasks.
</p>
</div>
<button
onClick={() => spawnSession(activeTab)}
className="mt-4 bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
>
Restart Session
</button>
</>
)}
{message && (
<div className="mt-6 bg-slate-800 rounded-lg p-4 text-left">
<p className="text-slate-300 text-sm">{message}</p>
</div>
)}
</div>
</div>
{/* Instructions */}
<div className="bg-slate-900/50 border-t border-slate-800 px-6 py-4">
<div className="flex items-center justify-between text-sm">
<div className="text-slate-500">
💡 Claude Code sessions run as separate agents. Ask Horus to delegate coding tasks to them.
</div>
<div className="flex items-center gap-2">
{tabs.filter(t => t.status === "active").length > 0 && (
<span className="text-green-400">
{tabs.filter(t => t.status === "active").length} active
</span>
)}
</div>
</div>
</div>
{/* Settings Modal */}
{showSettings && (
<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-700">
<h3 className="text-white font-bold text-lg mb-4"> Settings</h3>
<div className="mb-4">
<label className="block text-slate-400 text-sm mb-2">Anthropic API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-..."
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm"
/>
<p className="text-slate-500 text-xs mt-1">
Get key from console.anthropic.com
</p>
</div>
<div className="flex gap-2 justify-end">
<button
onClick={() => setShowSettings(false)}
className="px-4 py-2 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-600"
>
Cancel
</button>
<button
onClick={() => {
localStorage.setItem("anthropic_api_key", apiKey);
setShowSettings(false);
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-500"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
);
}