45af56d9cf
Also added: - Memory API endpoints - Briefs API endpoints - AnveVoice stats API - Claude spawn API - TTS proxy - Cleopatra voice widget - api-auth middleware
284 lines
9.8 KiB
TypeScript
284 lines
9.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|