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
+263
View File
@@ -0,0 +1,263 @@
"use client";
import { useState, useEffect, useRef } from "react";
interface Message {
role: "user" | "assistant";
content: string;
timestamp: Date;
}
export default function ClaudeChatPage() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [apiKey, setApiKey] = useState("");
const [showSettings, setShowSettings] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const savedKey = localStorage.getItem("anthropic_api_key") || "";
setApiKey(savedKey);
if (!savedKey) {
setShowSettings(true);
}
}, []);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || !apiKey) return;
const userMessage: Message = {
role: "user",
content: input,
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInput("");
setLoading(true);
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages: [
...messages.map(m => ({ role: m.role, content: m.content })),
{ role: "user", content: input }
],
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || "API Error");
}
const data = await response.json();
const assistantMessage: Message = {
role: "assistant",
content: data.content[0].text,
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error: any) {
const errorMessage: Message = {
role: "assistant",
content: `❌ Error: ${error.message}`,
timestamp: new Date(),
};
setMessages(prev => [...prev, errorMessage]);
}
setLoading(false);
};
const clearChat = () => {
setMessages([]);
};
const activeTab = messages.length === 0;
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 flex-shrink-0">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-2">
<span style={{ color: "#ff6154" }}></span>
Claude Code Chat
</h1>
<p className="text-slate-400 text-sm">Direct chat with Claude AI</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearChat}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
disabled={messages.length === 0}
>
🗑 Clear
</button>
<button
onClick={() => setShowSettings(true)}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
>
</button>
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-4xl mx-auto space-y-4">
{messages.length === 0 && !loading && (
<div className="text-center py-12">
<div className="text-6xl mb-4">💬</div>
<h2 className="text-xl font-bold text-white mb-2">Chat with Claude</h2>
<p className="text-slate-400 max-w-md mx-auto">
Send a message to start a conversation. Claude will respond directly.
</p>
{!apiKey && (
<button
onClick={() => setShowSettings(true)}
className="mt-4 bg-[#ff6154] hover:bg-[#ff4f3a] text-white px-6 py-3 rounded-lg font-medium"
>
Add API Key to Start
</button>
)}
</div>
)}
{messages.map((msg, i) => (
<div
key={i}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
msg.role === "user"
? "bg-[#ff6154] text-white"
: "bg-slate-800 text-slate-100"
}`}
>
<div className="text-sm whitespace-pre-wrap font-mono leading-relaxed">
{msg.content}
</div>
<div className={`text-xs mt-1 ${msg.role === "user" ? "text-red-200" : "text-slate-500"}`}>
{msg.timestamp.toLocaleTimeString()}
</div>
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div className="bg-slate-800 rounded-2xl px-4 py-3">
<div className="flex items-center gap-2 text-slate-400">
<div className="animate-spin"></div>
<span>Claude is thinking...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* Input */}
<div className="bg-slate-900 border-t border-slate-800 p-4 flex-shrink-0">
<div className="max-w-4xl mx-auto">
<div className="flex gap-3">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder={apiKey ? "Message Claude..." : "Add API key in settings to start"}
disabled={!apiKey}
className="flex-1 bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white placeholder-slate-500 resize-none focus:outline-none focus:border-slate-500 disabled:opacity-50"
rows={1}
/>
<button
onClick={sendMessage}
disabled={!input.trim() || loading || !apiKey}
className="bg-[#ff6154] hover:bg-[#ff4f3a] disabled:opacity-50 disabled:cursor-not-allowed text-white px-6 py-3 rounded-xl font-medium transition-colors"
>
Send
</button>
</div>
<p className="text-slate-500 text-xs mt-2 text-center">
Press Enter to send, Shift+Enter for new line
</p>
</div>
</div>
{/* Settings Modal */}
{showSettings && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl p-6 w-full max-w-md 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 focus:outline-none focus:border-slate-500"
/>
<p className="text-slate-500 text-xs mt-1">
Get your key from <span className="text-blue-400">console.anthropic.com</span>
</p>
</div>
<div className="bg-slate-900 rounded-lg p-3 mb-4">
<p className="text-slate-400 text-xs">
<strong>Note:</strong> This chat uses your API key directly.
Your conversations are processed by Anthropic's Claude AI.
</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-[#ff6154] text-white rounded-lg text-sm hover:bg-[#ff4f3a]"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
);
}