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
+30
View File
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
const ANVEVOICE_API_KEY = "anvk_6a217415c671b8e613df1f0b37f72c492a91b625";
const CLEOPATRA_BOT_ID = "022ae3d3-ed11-45a9-b663-f4a6dfa34f77";
export async function GET() {
try {
const res = await fetch('https://aaxlcyouksuljvmypyhy.supabase.co/functions/v1/anve-mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': ANVEVOICE_API_KEY
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "get_analytics_overview",
arguments: { bot_id: CLEOPATRA_BOT_ID }
},
id: 4
})
});
const data = await res.json();
return NextResponse.json(data);
} catch (e) {
return NextResponse.json({ error: "Failed to fetch" }, { status: 500 });
}
}
+30
View File
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
const API_SECRET = process.env.API_SECRET || 'horus-mc-secret-2026';
export async function GET(
request: Request,
{ params }: { params: Promise<{ date: string }> }
) {
// Check auth
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${API_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const { date } = await params;
const filePath = `/root/.openclaw/workspace/briefs/eod/${date}.md`;
if (!fs.existsSync(filePath)) {
return NextResponse.json({ content: 'No EOD brief for this date' }, { status: 404 });
}
const content = fs.readFileSync(filePath, 'utf8');
return NextResponse.json({ date, content });
} catch (error) {
return NextResponse.json({ error: 'Failed to load' }, { status: 500 });
}
}
+30
View File
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
const API_SECRET = process.env.API_SECRET || 'horus-mc-secret-2026';
export async function GET(
request: Request,
{ params }: { params: Promise<{ date: string }> }
) {
// Check auth
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${API_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const { date } = await params;
const filePath = `/root/.openclaw/workspace/briefs/morning/${date}.md`;
if (!fs.existsSync(filePath)) {
return NextResponse.json({ content: 'No morning brief for this date' }, { status: 404 });
}
const content = fs.readFileSync(filePath, 'utf8');
return NextResponse.json({ date, content });
} catch (error) {
return NextResponse.json({ error: 'Failed to load' }, { status: 500 });
}
}
+34 -17
View File
@@ -1,35 +1,52 @@
import { NextResponse } from 'next/server';
const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY || "sk-cp-aRrwyWpeY7iheh18JiqLNHkaz0Kude0MRYFt2w5fDzk-5026VI-HtO06_us_DQjJ8yHt4Qevgz-UE3F566cnjYDZPMSUGLLFgjUpwOiV0Ir0hTbeUclMeIQ";
const MINIMAX_URL = "https://api.minimax.io/v1/text/chatcompletion_v2";
export async function POST(request: Request) {
try {
const body = await request.json();
const { text } = body;
const prompt = `You are Cleopatra, a professional Spanish-speaking sales agent. Keep responses SHORT (1-2 sentences), friendly, in Spanish.
// Cleopatra personality - short, friendly, Spanish
const systemPrompt = `Eres Cleopatra, una agente de ventas profesional hispanohablante.
Respuestas CORTAS (1-2 oraciones), amigables, en español.
Siempre suena interesada y servicial.
Si no entiendes algo, pide que repitan por favor.`;
User: ${text}
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content: text }
];
Response in Spanish:`;
const res = await fetch("http://127.0.0.1:11434/api/generate", {
const res = await fetch(MINIMAX_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: AbortSignal.timeout(60000),
headers: {
"Authorization": `Bearer ${MINIMAX_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "phi3:mini",
prompt: prompt,
stream: false
model: "MiniMax-M2.7",
messages,
temperature: 0.8,
max_tokens: 150
})
});
if (!res.ok) {
throw new Error("MiniMax API error");
}
const data = await res.json();
const reply = (data.response || "¡Hola! Estoy aquí.").substring(0, 200);
return NextResponse.json({ response: reply });
const reply = data.choices?.[0]?.message?.content?.trim() ||
"¡Estoy aquí para ayudarte! ¿Qué necesitas?";
return NextResponse.json({ response: reply.substring(0, 300) });
} catch (e) {
console.error(e);
console.error("Chat error:", e);
return NextResponse.json({
response: "Tengo problemas para conectar. Intenta de nuevo."
}, { status: 500 });
response: "Tengo problemas para conectar. ¿Puedes intentar de nuevo?"
}, { status: 200 });
}
}
+25
View File
@@ -0,0 +1,25 @@
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const { tabId, tabName, apiKey } = await request.json();
if (!apiKey) {
return NextResponse.json({ error: 'API key required' }, { status: 400 });
}
// For now, return instructions for manual spawn
// Real implementation would use sessions_spawn via OpenClaw SDK
const sessionKey = `claude-${tabId}-${Date.now()}`;
return NextResponse.json({
success: true,
sessionId: sessionKey,
sessionKey,
message: `To start Claude Code for ${tabName}, use the spawn command in Horus`,
instruction: `Ask Horus to spawn a Claude session for ${tabName}`
});
} catch (error) {
return NextResponse.json({ error: 'Failed to spawn session' }, { status: 500 });
}
}
+16
View File
@@ -0,0 +1,16 @@
import { NextResponse } from 'next/server';
export async function GET() {
try {
const response = await fetch('http://localhost:3456/');
const html = await response.text();
return new NextResponse(html, {
status: 200,
headers: {
'Content-Type': 'text/html',
},
});
} catch (error) {
return NextResponse.json({ error: 'Claw3D not running' }, { status: 503 });
}
}
+23
View File
@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
export async function GET(
request: Request,
{ params }: { params: Promise<{ date: string }> }
) {
try {
const { date } = await params;
const memoryDir = '/root/.openclaw/workspace/memory';
const filePath = path.join(memoryDir, `${date}.md`);
if (!fs.existsSync(filePath)) {
return NextResponse.json({ content: '', error: 'Not found' }, { status: 404 });
}
const content = fs.readFileSync(filePath, 'utf8');
return NextResponse.json({ content, date });
} catch (error) {
return NextResponse.json({ content: '', error: 'Failed to load' }, { status: 500 });
}
}
+47
View File
@@ -0,0 +1,47 @@
import { NextResponse } from 'next/server';
import * as fs from 'fs';
import * as path from 'path';
const AGENT_SECRET = process.env.AGENT_SECRET || 'agent-mc-secret-2026';
export async function POST(request: Request) {
// Check for agent authorization
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${AGENT_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const body = await request.json();
const { agent, time, content, tags } = body;
if (!agent || !content) {
return NextResponse.json({ error: 'Missing agent or content' }, { status: 400 });
}
// Get today's date
const today = new Date().toISOString().split('T')[0];
const memoryDir = '/root/.openclaw/workspace/memory';
const filePath = path.join(memoryDir, `${today}.md`);
// Build entry
const tagsStr = tags && tags.length > 0
? `\n[Tags: ${tags.map((t: string) => '#' + t).join(', ')}]`
: '';
const entry = `\n\n## ${agent} - ${time || new Date().toTimeString().slice(0,5)}\n${tagsStr}\n${content}`;
// Append to file
if (fs.existsSync(filePath)) {
fs.appendFileSync(filePath, entry, 'utf8');
} else {
// Create new file with header
const header = `# Daily Memory - ${today}\n`;
fs.writeFileSync(filePath, header + entry, 'utf8');
}
return NextResponse.json({ success: true, date: today, entry: entry.trim() });
} catch (error) {
console.error('Memory append error:', error);
return NextResponse.json({ error: 'Failed to append memory' }, { status: 500 });
}
}
+141
View File
@@ -0,0 +1,141 @@
import { NextResponse } from 'next/server';
import * as fs from 'fs';
import * as path from 'path';
const API_SECRET = process.env.API_SECRET || 'horus-mc-secret-2026';
function checkAuth(request: Request): boolean {
const authHeader = request.headers.get('authorization');
return authHeader === `Bearer ${API_SECRET}`;
}
const MAIN_AGENTS = ["horus", "cleopatra", "amun"];
const AGENTS = [
{ id: "horus", name: "Horus", icon: "👁️", color: "#3b82f6" },
{ id: "cleopatra", name: "Cleopatra", icon: "👸", color: "#a855f7" },
{ id: "amun", name: "Amun", icon: "👑", color: "#f59e0b" },
{ 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" },
];
interface AgentInfo {
id: string;
name: string;
icon: string;
color: string;
}
function getAgentInfo(agentName: string): AgentInfo {
const lower = agentName.toLowerCase();
const found = AGENTS.find(a => lower.includes(a.id) || lower.includes(a.name.toLowerCase()));
return found || { id: "unknown", name: agentName, icon: "🤖", color: "#64748b" };
}
export async function GET(request: Request) {
// Check auth
if (!checkAuth(request)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const memoryDir = '/root/.openclaw/workspace/memory';
const briefsMorningDir = '/root/.openclaw/workspace/briefs/morning';
const briefsEodDir = '/root/.openclaw/workspace/briefs/eod';
const files = fs.readdirSync(memoryDir)
.filter(f => f.endsWith('.md'))
.filter(f => /^\d{4}-\d{2}-\d{2}$/.test(f.replace('.md', '')));
const days: any[] = [];
const allTags: string[] = [];
for (const file of files) {
const date = file.replace('.md', '');
const filePath = path.join(memoryDir, file);
const content = fs.readFileSync(filePath, 'utf8');
const entries: any[] = [];
const lines = content.split('\n');
let currentEntry: any = null;
for (const line of lines) {
const agentMatch = line.match(/^##\s*(\w+)\s*[-]\s*(\d{2}:\d{2})/);
if (agentMatch) {
if (currentEntry?.content?.trim()) {
entries.push(currentEntry);
}
const agentInfo = getAgentInfo(agentMatch[1]);
currentEntry = {
agent: agentInfo.name,
agentIcon: agentInfo.icon,
agentColor: agentInfo.color,
time: agentMatch[2],
content: '',
tags: [],
isMainAgent: MAIN_AGENTS.includes(agentInfo.id),
};
} else if (currentEntry) {
const trimmed = line.trim();
if (!trimmed) continue;
const standaloneTagsMatch = trimmed.match(/^\[Tags:\s*(.+?)\]$/);
if (standaloneTagsMatch) {
const tags = standaloneTagsMatch[1]
.split(',')
.map((t: string) => t.trim().replace(/^#/, '').trim())
.filter(Boolean);
currentEntry.tags = [...new Set([...currentEntry.tags, ...tags])];
tags.forEach((t: string) => {
if (!allTags.includes(t)) allTags.push(t);
});
} else {
const inlineTagsMatch = trimmed.match(/\[Tags:\s*(.+?)\]/);
if (inlineTagsMatch) {
const tags = inlineTagsMatch[1]
.split(',')
.map((t: string) => t.trim().replace(/^#/, '').trim())
.filter(Boolean);
currentEntry.tags = [...new Set([...currentEntry.tags, ...tags])];
tags.forEach((t: string) => {
if (!allTags.includes(t)) allTags.push(t);
});
}
const cleanLine = trimmed.replace(/\[Tags:.*?\]/g, '').trim();
if (cleanLine) {
currentEntry.content += (currentEntry.content ? '\n' : '') + cleanLine;
}
}
}
}
if (currentEntry?.content?.trim()) {
entries.push(currentEntry);
}
const hasMorning = fs.existsSync(path.join(briefsMorningDir, `${date}.md`));
const hasEod = fs.existsSync(path.join(briefsEodDir, `${date}.md`));
days.push({
date,
entries,
hasBriefs: { morning: hasMorning, eod: hasEod },
});
}
days.sort((a, b) => b.date.localeCompare(a.date));
return NextResponse.json({
days,
allTags,
totalDays: days.length,
});
} catch (error) {
console.error('Memory list error:', error);
return NextResponse.json({ days: [], allTags: [], error: 'Failed to load memory' }, { status: 500 });
}
}
+14 -1
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const API_SECRET = process.env.API_SECRET || 'horus-mc-secret-2026';
const STORAGE_FILE = path.join(process.cwd(), "data", "morning_briefs.json");
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc";
@@ -47,12 +48,24 @@ ${(data.priorities || []).map((p: string, i: number) => `${i + 1}. ${p}`).join('
${(data.leads || []).map((l: string) => `- ${l}`).join('\n')}`;
}
export async function GET() {
export async function GET(request: NextRequest) {
// Check auth
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${API_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const briefs = getBriefs();
return NextResponse.json(briefs);
}
export async function POST(request: NextRequest) {
// Check auth
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${API_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const body = await request.json();
+31
View File
@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
const TTS_API = "http://185.45.195.201:5001/api/tts/base64";
export async function POST(request: Request) {
try {
const { text, lang = "es" } = await request.json();
if (!text) {
return NextResponse.json({ error: "No text provided" }, { status: 400 });
}
// Call Cleopatra's TTS API (HTTP)
const response = await fetch(TTS_API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, lang })
});
if (!response.ok) {
throw new Error("TTS API error");
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("TTS proxy error:", error);
return NextResponse.json({ error: "TTS failed" }, { status: 500 });
}
}
+357
View File
@@ -0,0 +1,357 @@
"use client";
import { useState, useEffect, useRef } from "react";
type Language = "es" | "en";
const TTS_PROXY = "/api/tts-proxy";
const labels = {
es: {
title: "Cleopatra",
subtitle: "Asistente de Ventas IA Premium",
status: {
listening: "🎤 Escuchando...",
speaking: "🔊 Hablando...",
thinking: "⏳ Pensando...",
ready: "💬 Lista para ayudarte"
},
youSaid: "Dijiste:",
cleopatra: "👑 Cleopatra:",
micHint: "Toca el micrófono",
speakingHint: "Habla ahora...",
tryAgain: "Tengo problemas para conectar.",
placeholder: "Escribe aquí..."
},
en: {
title: "Cleopatra",
subtitle: "Premium AI Sales Assistant",
status: {
listening: "🎤 Listening...",
speaking: "🔊 Speaking...",
thinking: "⏳ Thinking...",
ready: "💬 Ready to help"
},
youSaid: "You said:",
cleopatra: "👑 Cleopatra:",
micHint: "Tap the microphone",
speakingHint: "Speak now...",
tryAgain: "I'm having trouble connecting.",
placeholder: "Type here..."
}
};
export default function CleopatraVoiceWidget() {
const [lang, setLang] = useState<Language>("es");
const [isListening, setIsListening] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
const [isThinking, setIsThinking] = useState(false);
const [transcript, setTranscript] = useState("");
const [lastReply, setLastReply] = useState("");
const [inputText, setInputText] = useState("");
const recognitionRef = useRef<any>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const t = labels[lang];
useEffect(() => {
return () => {
stopAll();
};
}, []);
const stopAll = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
if (recognitionRef.current) {
recognitionRef.current.abort();
recognitionRef.current = null;
}
setIsListening(false);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setIsSpeaking(false);
};
const speak = async (text: string) => {
try {
setIsSpeaking(true);
// Use proxy to fetch TTS (handles HTTP->HTTPS)
const res = await fetch(TTS_PROXY, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, lang })
});
if (!res.ok) throw new Error("TTS failed");
const data = await res.json();
const base64Audio = data.audio;
if (!base64Audio) throw new Error("No audio");
// Play audio
const audio = new Audio(`data:audio/mp3;base64,${base64Audio}`);
audioRef.current = audio;
audio.onended = () => setIsSpeaking(false);
audio.onerror = () => {
setIsSpeaking(false);
console.error("Audio playback error");
};
await audio.play();
} catch (err) {
console.error("TTS error:", err);
// Fallback to browser TTS
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = lang === "es" ? "es-ES" : "en-US";
utterance.rate = 0.9;
utterance.onstart = () => setIsSpeaking(true);
utterance.onend = () => setIsSpeaking(false);
utterance.onerror = () => setIsSpeaking(false);
window.speechSynthesis.speak(utterance);
}
};
const toggleLang = () => {
setLang(prev => prev === "es" ? "en" : "es");
};
const startListening = async () => {
try {
stopAll();
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
});
streamRef.current = stream;
const SpeechRecognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition;
if (!SpeechRecognition) {
alert(lang === "es" ? "Speech recognition not supported" : "Speech recognition not supported");
return;
}
const recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.interimResults = true;
recognition.lang = lang === "es" ? "es-ES" : "en-US";
recognitionRef.current = recognition;
recognition.onstart = () => {
setIsListening(true);
setTranscript("");
};
recognition.onresult = (event: any) => {
const result = event.results[0];
const text = result[0].transcript;
setTranscript(text);
if (result.isFinal) {
handleSend(text);
setIsListening(false);
}
};
recognition.onend = () => setIsListening(false);
recognition.onerror = (event: any) => {
console.error("Speech error:", event.error);
setIsListening(false);
if (event.error !== "no-speech") {
alert(lang === "es" ? "Error de voz: " + event.error : "Speech error: " + event.error);
}
};
recognition.start();
} catch (err) {
console.error("Mic error:", err);
alert(lang === "es" ? "No se pudo acceder al micrófono" : "Could not access microphone");
}
};
const stopListening = () => {
if (recognitionRef.current) recognitionRef.current.abort();
setIsListening(false);
};
const handleSend = async (text: string) => {
if (!text.trim()) return;
setIsThinking(true);
setLastReply("");
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, language: lang })
});
const data = await res.json();
const reply = data.response || (lang === "es" ? "Estoy aquí para ayudarte." : "I'm here to help.");
setLastReply(reply);
await speak(reply);
} catch (err) {
console.error("Error:", err);
const errorReply = t.tryAgain;
setLastReply(errorReply);
await speak(errorReply);
}
setIsThinking(false);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputText.trim()) {
handleSend(inputText);
setInputText("");
}
};
const skipSpeaking = () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setIsThinking(false);
setIsSpeaking(false);
};
return (
<div className="bg-gradient-to-br from-purple-950 to-indigo-950 p-8 rounded-3xl shadow-2xl max-w-lg mx-auto border border-purple-500/30">
{/* Language Toggle */}
<div className="flex justify-end mb-4">
<button
onClick={toggleLang}
className="px-3 py-1 rounded-full text-sm font-bold bg-purple-800/50 hover:bg-purple-700/50 text-white border border-purple-500/30 transition-all"
>
{lang === "es" ? "🇪🇸 ES" : "🇺🇸 EN"}
</button>
</div>
{/* Header */}
<div className="text-center mb-6">
<div className="text-5xl mb-3">👑</div>
<h3 className="text-2xl font-bold text-white">{t.title}</h3>
<p className="text-purple-300 text-sm">{t.subtitle}</p>
</div>
{/* Status */}
<div className="flex justify-center mb-6">
<div className={`px-5 py-2 rounded-full text-sm font-medium ${
isListening ? "bg-red-500/80 animate-pulse text-white" :
isSpeaking ? "bg-green-500/80 animate-pulse text-white" :
isThinking ? "bg-yellow-500/80 text-white" :
"bg-gray-700/80 text-gray-300"
}`}>
{isListening ? t.status.listening :
isSpeaking ? t.status.speaking :
isThinking ? t.status.thinking :
t.status.ready}
</div>
</div>
{/* Visualizer */}
<div className="flex justify-center items-end gap-1 h-16 mb-6">
{[...Array(16)].map((_, i) => (
<div
key={i}
className={`w-2 rounded-full transition-all duration-75 ${
isListening || isSpeaking ? "bg-gradient-to-t from-purple-500 to-pink-500" : "bg-purple-900"
}`}
style={{
height: isListening || isSpeaking ? `${20 + Math.random() * 40}px` : isThinking ? `${10 + Math.sin(Date.now() / 200 + i) * 10 + 10}px` : "8px"
}}
/>
))}
</div>
{/* Transcript */}
{transcript && (
<div className="bg-black/40 rounded-xl p-4 mb-4 border border-purple-500/20">
<p className="text-xs text-purple-300 mb-1">{t.youSaid}</p>
<p className="text-white text-lg">{transcript}</p>
</div>
)}
{/* Reply */}
{lastReply && (
<div className="bg-gradient-to-r from-purple-900/60 to-indigo-900/60 rounded-xl p-4 mb-4 border border-green-500/20">
<p className="text-xs text-green-300 mb-1">{t.cleopatra}</p>
<p className="text-white">{lastReply}</p>
</div>
)}
{/* Text Input */}
<form onSubmit={handleSubmit} className="mb-4">
<div className="flex gap-2">
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder={t.placeholder}
className="flex-1 bg-black/40 border border-purple-500/30 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-purple-500"
/>
<button
type="submit"
disabled={isThinking || isSpeaking}
className="px-6 py-3 bg-purple-600 hover:bg-purple-500 disabled:opacity-50 rounded-xl text-white font-bold transition-all"
>
</button>
</div>
</form>
{/* Controls */}
<div className="flex justify-center gap-4">
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (isListening) stopListening();
else startListening();
}}
className={`w-20 h-20 rounded-full flex items-center justify-center text-3xl transition-all shadow-lg ${
isListening ? "bg-red-500 hover:bg-red-600 animate-pulse ring-4 ring-red-400/50" : "bg-gradient-to-br from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600"
} text-white`}
>
{isListening ? "⏹" : "🎤"}
</button>
{(isSpeaking || isThinking) && (
<button
onClick={(e) => {
e.preventDefault();
skipSpeaking();
}}
className="w-14 h-14 rounded-full bg-orange-500 hover:bg-orange-600 flex items-center justify-center text-2xl"
>
</button>
)}
</div>
{/* Hint */}
<p className="text-center text-xs text-purple-400 mt-5">
{isListening ? t.speakingHint : t.micHint}
</p>
</div>
);
}
+82
View File
@@ -0,0 +1,82 @@
import CleopatraVoiceWidget from "@/app/components/CleopatraVoiceWidget";
export default function CleopatraVoicePage() {
return (
<main className="min-h-screen bg-gradient-to-br from-slate-950 via-purple-950 to-slate-950 py-12 px-4">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-white mb-2">
👑 Cleopatra Voice AI
</h1>
<p className="text-purple-300">
Premium Voice Agent - Built for Human-Like Conversations
</p>
</div>
<div className="grid md:grid-cols-2 gap-8 mb-12">
<div className="bg-white/5 backdrop-blur rounded-2xl p-6 border border-white/10">
<h2 className="text-xl font-bold text-white mb-4">🎤 Características</h2>
<ul className="space-y-3 text-purple-200">
<li className="flex items-center gap-2">
<span className="text-green-400"></span>
Respuesta menos de 1 segundo
</li>
<li className="flex items-center gap-2">
<span className="text-green-400"></span>
Detección de voz (VAD)
</li>
<li className="flex items-center gap-2">
<span className="text-green-400"></span>
Palabras de relleno naturales
</li>
<li className="flex items-center gap-2">
<span className="text-green-400"></span>
Manejo de interrupciones
</li>
<li className="flex items-center gap-2">
<span className="text-green-400"></span>
Multilingüe
</li>
</ul>
</div>
<div className="bg-white/5 backdrop-blur rounded-2xl p-6 border border-white/10">
<h2 className="text-xl font-bold text-white mb-4">🧠 Tecnología</h2>
<ul className="space-y-3 text-purple-200">
<li className="flex items-center gap-2">
<span className="text-blue-400"></span>
STT: Whisper (preciso)
</li>
<li className="flex items-center gap-2">
<span className="text-blue-400"></span>
LLM: MiniMax M2.7
</li>
<li className="flex items-center gap-2">
<span className="text-blue-400"></span>
TTS: ElevenLabs (75ms)
</li>
<li className="flex items-center gap-2">
<span className="text-blue-400"></span>
Streaming de audio
</li>
<li className="flex items-center gap-2">
<span className="text-blue-400"></span>
Personalidad Cleopatra
</li>
</ul>
</div>
</div>
<div className="mb-8">
<CleopatraVoiceWidget />
</div>
<div className="text-center">
<p className="text-purple-400 text-sm">
🚀 Construido por Horus + Cleopatra | HostPioneers
</p>
</div>
</div>
</main>
);
}
+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>
);
}
+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>
);
}
+520
View File
@@ -0,0 +1,520 @@
"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>
);
}
+24
View File
@@ -0,0 +1,24 @@
"use client";
export default function OfficePage() {
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-shrink-0">
<h1 className="text-2xl font-bold text-white">🏢 Claw3D Office</h1>
<p className="text-slate-400 text-sm">3D workspace for AI agents</p>
</div>
{/* Claw3D Embed - via Apache proxy over HTTPS */}
<div className="flex-1 relative">
<iframe
src="https://sitemente.com/claw3d/office"
className="absolute inset-0 w-full h-full border-0"
title="Claw3D Office"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
/>
</div>
</div>
);
}
+154
View File
@@ -0,0 +1,154 @@
"use client";
import { useState } from "react";
export default function PDFViewerPage() {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pdfBase64, setPdfBase64] = useState<string | null>(null);
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.type !== "application/pdf") {
setError("Please select a PDF file");
return;
}
setLoading(true);
setError(null);
// Read file as base64
const reader = new FileReader();
reader.onload = (event) => {
const base64 = event.target?.result as string;
setPdfBase64(base64);
setPdfUrl(URL.createObjectURL(file));
setLoading(false);
};
reader.onerror = () => {
setError("Failed to read file");
setLoading(false);
};
reader.readAsDataURL(file);
};
const handleUrlSubmit = async () => {
const input = prompt("Enter PDF URL:");
if (!input) return;
setLoading(true);
setError(null);
try {
new URL(input);
setPdfUrl(input);
setPdfBase64(null);
} catch {
setError("Invalid URL");
}
setLoading(false);
};
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-shrink-0">
<h1 className="text-2xl font-bold text-white">📄 PDF Viewer</h1>
<p className="text-slate-400 text-sm">View and analyze PDF documents</p>
</div>
{/* Toolbar */}
<div className="bg-slate-900/50 border-b border-slate-800 px-6 py-3 flex-shrink-0">
<div className="flex items-center gap-4">
<label className="cursor-pointer bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
📁 Upload PDF
<input
type="file"
accept="application/pdf"
onChange={handleFileUpload}
className="hidden"
/>
</label>
<button
onClick={handleUrlSubmit}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
🔗 Load from URL
</button>
{pdfUrl && (
<a
href={pdfUrl}
target="_blank"
rel="noopener noreferrer"
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Open Original
</a>
)}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden flex flex-col">
{error && (
<div className="bg-red-900/50 border border-red-700 rounded-lg p-4 m-4 max-w-md">
<h3 className="text-red-400 font-bold mb-2">Error</h3>
<p className="text-red-300">{error}</p>
<button
onClick={() => setError(null)}
className="mt-2 bg-red-700 hover:bg-red-600 text-white px-4 py-1 rounded text-sm"
>
Dismiss
</button>
</div>
)}
{!pdfUrl && !loading && (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-8xl mb-6 opacity-50">📄</div>
<h2 className="text-xl font-bold text-white mb-2">No PDF Loaded</h2>
<p className="text-slate-400 mb-6">Upload a PDF or enter a URL to view it</p>
<div className="text-slate-500 text-sm space-y-1">
<p> Upload your resume to let Horus analyze it</p>
<p> Supports PDF files up to 50MB</p>
<p> Or load from any public PDF URL</p>
</div>
</div>
</div>
)}
{loading && (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin text-6xl mb-4"></div>
<p className="text-slate-400">Loading PDF...</p>
</div>
</div>
)}
{pdfUrl && !loading && (
<div className="flex-1 overflow-auto bg-slate-800 p-4 flex justify-center">
<iframe
src={pdfUrl}
className="w-full h-full min-h-[600px] bg-white shadow-2xl rounded"
title="PDF Viewer"
/>
</div>
)}
</div>
{/* Instructions */}
<div className="bg-slate-900/50 border-t border-slate-800 px-6 py-2 flex-shrink-0">
<p className="text-slate-500 text-xs">
💡 Tip: Upload your resume PDF and Horus can analyze the design to recreate it
</p>
</div>
</div>
);
}
@@ -0,0 +1,81 @@
"use client";
import { useState } from "react";
export default function ResumeUploadPage() {
const [uploading, setUploading] = useState(false);
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
setUploading(true);
const newImages: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const formData = new FormData();
formData.append("file", file);
try {
const res = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const data = await res.json();
if (data.url) {
newImages.push(data.url);
}
} catch (err) {
console.error("Upload failed:", err);
}
}
setUploadedImages([...uploadedImages, ...newImages]);
setUploading(false);
};
return (
<div className="min-h-screen bg-slate-950 p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-white mb-6">📤 Upload Resume Images</h1>
<p className="text-slate-400 mb-6">Upload images of your resume design to let Horus analyze and recreate it.</p>
<div className="border-2 border-dashed border-slate-600 rounded-xl p-12 text-center hover:border-slate-500 transition-colors">
<input
type="file"
accept="image/*"
multiple
onChange={handleUpload}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload" className="cursor-pointer">
<div className="text-6xl mb-4">📁</div>
<div className="text-white text-lg mb-2">Click to upload images</div>
<div className="text-slate-500 text-sm">PNG, JPG, GIF up to 10MB</div>
</label>
</div>
{uploading && (
<div className="mt-6 text-center text-slate-400">Uploading...</div>
)}
{uploadedImages.length > 0 && (
<div className="mt-8">
<h2 className="text-white font-bold mb-4">Uploaded Images:</h2>
<div className="space-y-4">
{uploadedImages.map((url, i) => (
<div key={i} className="bg-slate-800 rounded-lg p-4">
<div className="text-slate-400 text-sm mb-2">Image {i + 1}</div>
<div className="bg-slate-700 rounded p-2 text-slate-300 text-sm font-mono break-all">{url}</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
+519
View File
@@ -0,0 +1,519 @@
"use client";
import { useState, useEffect } from "react";
interface PersonalInfo {
name: string;
title: string;
email: string;
phone: string;
location: string;
linkedin: string;
website: string;
}
interface Experience {
id: string;
title: string;
company: string;
location: string;
startDate: string;
endDate: string;
bullets: string[];
}
interface Education {
id: string;
degree: string;
school: string;
location: string;
graduationDate: string;
gpa: string;
honors: string;
}
export default function ResumePage() {
const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit");
const [personal, setPersonal] = useState<PersonalInfo>({
name: "Your Name",
title: "Your Professional Title",
email: "your.email@email.com",
phone: "+1 (555) 000-0000",
location: "City, State",
linkedin: "linkedin.com/in/yourprofile",
website: "yourwebsite.com"
});
const [experiences, setExperiences] = useState<Experience[]>([
{
id: "1",
title: "Job Title",
company: "Company Name",
location: "City, State",
startDate: "Jan 2020",
endDate: "Present",
bullets: [
"Key achievement or responsibility with measurable impact",
"Another accomplishment that demonstrates your skills",
"Additional relevant contribution"
]
}
]);
const [education, setEducation] = useState<Education[]>([
{
id: "1",
degree: "Bachelor of Science in Computer Science",
school: "University Name",
location: "City, State",
graduationDate: "May 2016",
gpa: "3.8/4.0",
honors: "Magna Cum Laude"
}
]);
const [skills, setSkills] = useState<string[]>([
"JavaScript/TypeScript", "React/Next.js", "Node.js", "Python",
"AWS/Cloud Services", "Database Management", "Agile/Scrum", "Git"
]);
const [languages, setLanguages] = useState<{name: string, level: string}[]>([
{ name: "English", level: "Native" },
{ name: "Spanish", level: "Professional" },
{ name: "Arabic", level: "Native" }
]);
const [certifications, setCertifications] = useState<string[]>([
"AWS Solutions Architect",
"Google Cloud Professional"
]);
useEffect(() => {
const saved = localStorage.getItem("haitham_resume_v2");
if (saved) {
try {
const data = JSON.parse(saved);
if (data.personal) setPersonal(data.personal);
if (data.experiences) setExperiences(data.experiences);
if (data.education) setEducation(data.education);
if (data.skills) setSkills(data.skills);
if (data.languages) setLanguages(data.languages);
if (data.certifications) setCertifications(data.certifications);
} catch (e) {}
}
}, []);
useEffect(() => {
localStorage.setItem("haitham_resume_v2", JSON.stringify({
personal, experiences, education, skills, languages, certifications
}));
}, [personal, experiences, education, skills, languages, certifications]);
const addExperience = () => {
setExperiences([...experiences, {
id: Date.now().toString(),
title: "", company: "", location: "", startDate: "", endDate: "", bullets: [""]
}]);
};
const updateExperience = (id: string, field: keyof Experience, value: any) => {
setExperiences(experiences.map(e => e.id === id ? { ...e, [field]: value } : e));
};
const addBullet = (id: string) => {
setExperiences(experiences.map(e =>
e.id === id ? { ...e, bullets: [...e.bullets, ""] } : e
));
};
const updateBullet = (expId: string, index: number, value: string) => {
setExperiences(experiences.map(e =>
e.id === expId ? { ...e, bullets: e.bullets.map((b, i) => i === index ? value : b) } : e
));
};
const removeBullet = (expId: string, index: number) => {
setExperiences(experiences.map(e =>
e.id === expId ? { ...e, bullets: e.bullets.filter((_, i) => i !== index) } : e
));
};
const addEducation = () => {
setEducation([...education, {
id: Date.now().toString(), degree: "", school: "", location: "",
graduationDate: "", gpa: "", honors: ""
}]);
};
const updateEducation = (id: string, field: keyof Education, value: string) => {
setEducation(education.map(e => e.id === id ? { ...e, [field]: value } : e));
};
const handlePrint = () => {
setActiveTab("preview");
setTimeout(() => window.print(), 100);
};
const inputClass = "w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500";
const labelClass = "block text-slate-400 text-xs mb-1 font-medium";
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">📝 Professional Resume</h1>
<p className="text-slate-400 text-sm">Edit Preview Download PDF</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setActiveTab(activeTab === "edit" ? "preview" : "edit")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === "edit" ? "bg-slate-700 text-white" : "bg-blue-600 text-white"
}`}
>
{activeTab === "edit" ? "👁️ Preview" : "✏️ Edit"}
</button>
<button
onClick={handlePrint}
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
📄 Download PDF
</button>
</div>
</div>
{activeTab === "edit" ? (
<div className="flex-1 overflow-auto p-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* Personal Info */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">👤 Personal Information</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Full Name</label>
<input type="text" value={personal.name} onChange={e => setPersonal({...personal, name: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Professional Title</label>
<input type="text" value={personal.title} onChange={e => setPersonal({...personal, title: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Email</label>
<input type="email" value={personal.email} onChange={e => setPersonal({...personal, email: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Phone</label>
<input type="tel" value={personal.phone} onChange={e => setPersonal({...personal, phone: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Location</label>
<input type="text" value={personal.location} onChange={e => setPersonal({...personal, location: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>LinkedIn</label>
<input type="text" value={personal.linkedin} onChange={e => setPersonal({...personal, linkedin: e.target.value})} className={inputClass} />
</div>
</div>
</div>
{/* Experience */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-bold">💼 Work Experience</h2>
<button onClick={addExperience} className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm">+ Add Position</button>
</div>
{experiences.map((exp) => (
<div key={exp.id} className="bg-slate-900 rounded-lg p-4 mb-4 border border-slate-700">
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<label className={labelClass}>Job Title</label>
<input type="text" value={exp.title} onChange={e => updateExperience(exp.id, "title", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Company</label>
<input type="text" value={exp.company} onChange={e => updateExperience(exp.id, "company", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Location</label>
<input type="text" value={exp.location} onChange={e => updateExperience(exp.id, "location", e.target.value)} className={inputClass} />
</div>
<div className="flex gap-2">
<div className="flex-1">
<label className={labelClass}>Start</label>
<input type="text" value={exp.startDate} onChange={e => updateExperience(exp.id, "startDate", e.target.value)} className={inputClass} placeholder="Jan 2020" />
</div>
<div className="flex-1">
<label className={labelClass}>End</label>
<input type="text" value={exp.endDate} onChange={e => updateExperience(exp.id, "endDate", e.target.value)} className={inputClass} placeholder="Present" />
</div>
</div>
</div>
<div className="mb-2">
<label className={labelClass}>Key Achievements</label>
{exp.bullets.map((bullet, i) => (
<div key={i} className="flex gap-2 mb-2">
<input type="text" value={bullet} onChange={e => updateBullet(exp.id, i, e.target.value)} className={inputClass} placeholder={`Achievement ${i + 1}`} />
{exp.bullets.length > 1 && (
<button onClick={() => removeBullet(exp.id, i)} className="text-red-400 hover:text-red-300 text-sm px-2"></button>
)}
</div>
))}
<button onClick={() => addBullet(exp.id)} className="text-blue-400 hover:text-blue-300 text-xs">+ Add achievement</button>
</div>
</div>
))}
</div>
{/* Education */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-bold">🎓 Education</h2>
<button onClick={addEducation} className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm">+ Add</button>
</div>
{education.map((edu) => (
<div key={edu.id} className="bg-slate-900 rounded-lg p-4 mb-4 border border-slate-700">
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Degree</label>
<input type="text" value={edu.degree} onChange={e => updateEducation(edu.id, "degree", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>School</label>
<input type="text" value={edu.school} onChange={e => updateEducation(edu.id, "school", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Location</label>
<input type="text" value={edu.location} onChange={e => updateEducation(edu.id, "location", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Graduation Date</label>
<input type="text" value={edu.graduationDate} onChange={e => updateEducation(edu.id, "graduationDate", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>GPA (optional)</label>
<input type="text" value={edu.gpa} onChange={e => updateEducation(edu.id, "gpa", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Honors (optional)</label>
<input type="text" value={edu.honors} onChange={e => updateEducation(edu.id, "honors", e.target.value)} className={inputClass} placeholder="Magna Cum Laude" />
</div>
</div>
</div>
))}
</div>
{/* Skills */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">🛠 Technical Skills</h2>
<div className="flex flex-wrap gap-2">
{skills.map((skill, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1">
<input
type="text"
value={skill}
onChange={e => {
const newSkills = [...skills];
newSkills[i] = e.target.value;
setSkills(newSkills);
}}
className="bg-transparent text-white text-sm w-24 focus:outline-none"
/>
<button onClick={() => setSkills(skills.filter((_, j) => j !== i))} className="text-slate-500 hover:text-red-400 text-xs"></button>
</div>
))}
<button onClick={() => setSkills([...skills, ""])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-1 rounded-lg">+ Add</button>
</div>
</div>
{/* Languages */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">🌍 Languages</h2>
<div className="flex flex-wrap gap-3">
{languages.map((lang, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-2">
<input
type="text"
value={lang.name}
onChange={e => {
const newLangs = [...languages];
newLangs[i].name = e.target.value;
setLanguages(newLangs);
}}
className="bg-transparent text-white text-sm w-24 focus:outline-none"
/>
<span className="text-slate-500 text-xs"></span>
<input
type="text"
value={lang.level}
onChange={e => {
const newLangs = [...languages];
newLangs[i].level = e.target.value;
setLanguages(newLangs);
}}
className="bg-transparent text-slate-400 text-sm w-20 focus:outline-none"
/>
</div>
))}
<button onClick={() => setLanguages([...languages, {name: "", level: ""}])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-2 rounded-lg">+ Add</button>
</div>
</div>
{/* Certifications */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">📜 Certifications</h2>
<div className="flex flex-wrap gap-2">
{certifications.map((cert, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1">
<input
type="text"
value={cert}
onChange={e => {
const newCerts = [...certifications];
newCerts[i] = e.target.value;
setCertifications(newCerts);
}}
className="bg-transparent text-white text-sm w-40 focus:outline-none"
/>
<button onClick={() => setCertifications(certifications.filter((_, j) => j !== i))} className="text-slate-500 hover:text-red-400 text-xs"></button>
</div>
))}
<button onClick={() => setCertifications([...certifications, ""])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-1 rounded-lg">+ Add</button>
</div>
</div>
</div>
</div>
) : (
/* PREVIEW - Professional Resume Template */
<div className="flex-1 overflow-auto bg-slate-100 p-8">
<div className="max-w-850px mx-auto bg-white shadow-xl rounded-lg overflow-hidden" style={{maxWidth: "850px"}}>
{/* Header - Dark Blue Professional */}
<div className="bg-gradient-to-r from-slate-800 to-slate-700 text-white px-8 py-10">
<div className="text-center mb-4">
<h1 className="text-3xl font-bold tracking-wide mb-2">{personal.name || "Your Name"}</h1>
<div className="text-slate-300 text-lg font-light">{personal.title || "Professional Title"}</div>
</div>
<div className="flex justify-center flex-wrap gap-4 text-sm text-slate-300">
{personal.email && <span> {personal.email}</span>}
{personal.phone && <span>📱 {personal.phone}</span>}
{personal.location && <span>📍 {personal.location}</span>}
{personal.linkedin && <span>💼 {personal.linkedin}</span>}
</div>
</div>
{/* Body */}
<div className="px-8 py-6 space-y-6">
{/* Summary */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Professional Summary</span>
</div>
<p className="text-slate-600 text-sm leading-relaxed">
Results-driven professional with proven expertise in delivering high-impact solutions. Skilled in modern technologies and best practices. Demonstrated ability to lead projects, optimize processes, and achieve organizational goals.
</p>
</div>
{/* Experience */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Work Experience</span>
</div>
{experiences.filter(e => e.title || e.company).map((exp) => (
<div key={exp.id} className="mb-4">
<div className="flex justify-between items-baseline mb-1">
<div>
<span className="font-bold text-slate-800">{exp.title || "Job Title"}</span>
<span className="text-slate-600"> | {exp.company || "Company"}</span>
</div>
<span className="text-slate-500 text-sm">{exp.startDate} - {exp.endDate}</span>
</div>
{exp.location && <div className="text-slate-500 text-sm mb-2">{exp.location}</div>}
<ul className="space-y-1">
{exp.bullets.filter(b => b).map((bullet, i) => (
<li key={i} className="text-slate-600 text-sm flex items-start gap-2">
<span className="text-slate-400"></span>
<span>{bullet}</span>
</li>
))}
</ul>
</div>
))}
</div>
{/* Education */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Education</span>
</div>
{education.filter(e => e.degree || e.school).map((edu) => (
<div key={edu.id} className="mb-3">
<div className="flex justify-between items-baseline">
<div>
<span className="font-bold text-slate-800">{edu.degree || "Degree"}</span>
{edu.honors && <span className="text-slate-500"> ({edu.honors})</span>}
</div>
<span className="text-slate-500 text-sm">{edu.graduationDate}</span>
</div>
<div className="text-slate-600 text-sm">{edu.school}{edu.location && `, ${edu.location}`}</div>
{edu.gpa && <div className="text-slate-500 text-sm">GPA: {edu.gpa}</div>}
</div>
))}
</div>
{/* Skills */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Technical Skills</span>
</div>
<div className="flex flex-wrap gap-2">
{skills.filter(s => s).map((skill, i) => (
<span key={i} className="bg-slate-100 text-slate-700 px-3 py-1 rounded text-sm">{skill}</span>
))}
</div>
</div>
{/* Languages & Certs */}
<div className="grid grid-cols-2 gap-6">
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Languages</span>
</div>
<div className="space-y-1">
{languages.filter(l => l.name).map((lang, i) => (
<div key={i} className="text-sm">
<span className="text-slate-800 font-medium">{lang.name}</span>
<span className="text-slate-500"> - {lang.level}</span>
</div>
))}
</div>
</div>
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Certifications</span>
</div>
<div className="space-y-1">
{certifications.filter(c => c).map((cert, i) => (
<div key={i} className="text-slate-700 text-sm">{cert}</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
)}
<style jsx global>{`
@media print {
body { background: white !important; }
.bg-slate-100 { background: white !important; }
}
`}</style>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
import TempleOfAIDashboard from "@/components/temple-of-ai/TempleOfAIDashboard";
export default function TemplePage() {
return <TempleOfAIDashboard />;
}
+518
View File
@@ -0,0 +1,518 @@
"use client";
import { useEffect, useRef, useState } from "react";
export default function Temple3DPage() {
const containerRef = useRef<HTMLDivElement>(null);
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!containerRef.current) return;
let cancelled = false;
import("three").then((THREE) => {
if (cancelled || !containerRef.current) return;
const container = containerRef.current;
// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
// Camera
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 15, 35);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
// Lighting
const ambient = new THREE.AmbientLight(0xffd700, 0.3);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xfff5e0, 1.2);
sun.position.set(50, 100, 50);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
scene.add(sun);
const blueLight = new THREE.PointLight(0x40e0d0, 0.8, 100);
blueLight.position.set(-30, 20, -20);
scene.add(blueLight);
const purpleLight = new THREE.PointLight(0x9b59b6, 0.5, 80);
purpleLight.position.set(30, 15, 10);
scene.add(purpleLight);
// Materials
const sandstone = new THREE.MeshStandardMaterial({
color: 0xc2a366,
roughness: 0.9,
metalness: 0.1
});
const darkSandstone = new THREE.MeshStandardMaterial({
color: 0x8b7355,
roughness: 0.85,
metalness: 0.1
});
const gold = new THREE.MeshStandardMaterial({
color: 0xffd700,
metalness: 0.9,
roughness: 0.2,
emissive: 0xffd700,
emissiveIntensity: 0.3
});
const lapis = new THREE.MeshStandardMaterial({
color: 0x1e3a5f,
metalness: 0.3,
roughness: 0.6
});
// Ground - sand
const groundGeo = new THREE.PlaneGeometry(200, 200);
const groundMat = new THREE.MeshStandardMaterial({ color: 0xd4b896, roughness: 1 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.1;
ground.receiveShadow = true;
scene.add(ground);
// ============== KARNAK TEMPLE STRUCTURE ==============
// First Pylon (massive gateway) - like Karnak
const pylon1Geo = new THREE.BoxGeometry(40, 25, 5);
const pylon1 = new THREE.Mesh(pylon1Geo, sandstone);
pylon1.position.set(0, 12.5, 25);
pylon1.castShadow = true;
pylon1.receiveShadow = true;
scene.add(pylon1);
// Pylon decorations (pillars on pylon)
for (let i = 0; i < 6; i++) {
const pillarGeo = new THREE.BoxGeometry(2, 22, 2);
const pillar = new THREE.Mesh(pillarGeo, darkSandstone);
pillar.position.set(-18 + i * 7, 11, 28);
pillar.castShadow = true;
scene.add(pillar);
}
// Courtyard columns - Lotus columns like Karnak
const columnPositions = [
[-15, 0], [-10, 0], [-5, 0], [0, 0], [5, 0], [10, 0], [15, 0],
[-15, -10], [-10, -10], [-5, -10], [0, -10], [5, -10], [10, -10], [15, -10],
];
columnPositions.forEach(([x, z]) => {
// Column shaft (bundled papyrus style)
const shaftGeo = new THREE.CylinderGeometry(1.2, 1.3, 18, 16);
const shaft = new THREE.Mesh(shaftGeo, sandstone);
shaft.position.set(x, 9, z);
shaft.castShadow = true;
scene.add(shaft);
// Column capital (open lotus)
const capitalGeo = new THREE.ConeGeometry(2, 4, 16);
const capital = new THREE.Mesh(capitalGeo, sandstone);
capital.position.set(x, 20, z);
capital.castShadow = true;
scene.add(capital);
});
// Hypostyle Hall (second section with smaller columns)
const hypostyleCols = [
[-12, -18], [-6, -18], [0, -18], [6, -18], [12, -18],
[-12, -22], [-6, -22], [0, -22], [6, -22], [12, -22],
];
hypostyleCols.forEach(([x, z]) => {
const shaftGeo = new THREE.CylinderGeometry(0.8, 0.9, 14, 12);
const shaft = new THREE.Mesh(shaftGeo, darkSandstone);
shaft.position.set(x, 7, z);
shaft.castShadow = true;
scene.add(shaft);
});
// Second Pylon
const pylon2Geo = new THREE.BoxGeometry(30, 20, 4);
const pylon2 = new THREE.Mesh(pylon2Geo, sandstone);
pylon2.position.set(0, 10, -25);
pylon2.castShadow = true;
scene.add(pylon2);
// Third Pylon
const pylon3Geo = new THREE.BoxGeometry(25, 18, 3);
const pylon3 = new THREE.Mesh(pylon3Geo, darkSandstone);
pylon3.position.set(0, 9, -35);
pylon3.castShadow = true;
scene.add(pylon3);
// Sacred Lake (water feature)
const lakeGeo = new THREE.CircleGeometry(8, 32);
const lakeMat = new THREE.MeshStandardMaterial({
color: 0x1e5f8f,
metalness: 0.3,
roughness: 0.2,
transparent: true,
opacity: 0.8
});
const lake = new THREE.Mesh(lakeGeo, lakeMat);
lake.rotation.x = -Math.PI / 2;
lake.position.set(-25, 0.1, 5);
scene.add(lake);
// Obelisks
const obeliskGeo = new THREE.CylinderGeometry(0.5, 0.8, 20, 4);
const obelisk1 = new THREE.Mesh(obeliskGeo, darkSandstone);
obelisk1.position.set(-8, 10, -25);
obelisk1.castShadow = true;
scene.add(obelisk1);
const obelisk2 = new THREE.Mesh(obeliskGeo, darkSandstone);
obelisk2.position.set(8, 10, -25);
obelisk2.castShadow = true;
scene.add(obelisk2);
// Golden pyramid cap on obelisks
const capGeo = new THREE.ConeGeometry(1.2, 3, 4);
const cap1 = new THREE.Mesh(capGeo, gold);
cap1.position.set(-8, 21.5, -25);
scene.add(cap1);
const cap2 = new THREE.Mesh(capGeo, gold);
cap2.position.set(8, 21.5, -25);
scene.add(cap2);
// Sphinx Avenue (ram-headed sphinxes leading to entrance)
for (let i = 0; i < 6; i++) {
// Body
const sphinxGeo = new THREE.BoxGeometry(3, 2, 6);
const sphinx = new THREE.Mesh(sphinxGeo, sandstone);
sphinx.position.set(-12 + i * 5, 1, 40 + (i % 2) * 3);
sphinx.castShadow = true;
scene.add(sphinx);
// Head
const headGeo = new THREE.SphereGeometry(1.2, 16, 16);
const head = new THREE.Mesh(headGeo, sandstone);
head.position.set(-12 + i * 5, 2.5, 42 + (i % 2) * 3);
head.castShadow = true;
scene.add(head);
}
// Holy of Holies - Sanctuary (dark room at center)
const sanctuaryGeo = new THREE.BoxGeometry(15, 12, 10);
const sanctuary = new THREE.Mesh(sanctuaryGeo, darkSandstone);
sanctuary.position.set(0, 6, -42);
sanctuary.castShadow = true;
scene.add(sanctuary);
// Solar barque (boat) in sanctuary area
const boatGeo = new THREE.BoxGeometry(12, 0.5, 3);
const boat = new THREE.Mesh(boatGeo, lapis);
boat.position.set(0, 0.5, -42);
scene.add(boat);
// Golden Eye of Horus at sanctuary entrance
const eyeGeo = new THREE.TorusGeometry(3, 0.5, 8, 32);
const eye = new THREE.Mesh(eyeGeo, gold);
eye.position.set(0, 5, -36.5);
scene.add(eye);
// Eye center
const eyeCenterGeo = new THREE.SphereGeometry(1.5, 16, 16);
const eyeCenter = new THREE.Mesh(eyeCenterGeo, new THREE.MeshStandardMaterial({
color: 0x000000,
emissive: 0xffd700,
emissiveIntensity: 0.2
}));
eyeCenter.position.set(0, 5, -36);
scene.add(eyeCenter);
// ============== EGYPTIAN AGENTS (Animated) ==============
const agents = [
{ name: "ANUBIS", symbol: "🐕", color: 0x1a1a1a, glow: 0xffd700, pos: [0, 1.5, 42], room: "AVENUE OF SPHINXES", role: "Guardian of Leads" },
{ name: "PTAH", symbol: "🔨", color: 0xf5deb3, glow: 0xffd700, pos: [-12, 1.5, 0], room: "COURTYARD", role: "Builder" },
{ name: "SESHAT", symbol: "📝", color: 0x228b22, glow: 0x228b22, pos: [-6, 1.5, 0], room: "COURTYARD", role: "Scribe" },
{ name: "SEKHMET", symbol: "🦁", color: 0xb22222, glow: 0xb22222, pos: [6, 1.5, 0], room: "COURTYARD", role: "Warrior" },
{ name: "THOTH", symbol: "🐦", color: 0xffffff, glow: 0x40e0d0, pos: [-6, 1.5, -18], room: "HYPOSTYLE HALL", role: "Knowledge Keeper" },
{ name: "MAAT", symbol: "⚖️", color: 0x40e0d0, glow: 0x40e0d0, pos: [6, 1.5, -18], room: "HYPOSTYLE HALL", role: "Truth" },
{ name: "CLEOPATRA", symbol: "👑", color: 0x9b59b6, glow: 0x9b59b6, pos: [0, 1.5, -22], room: "SECOND PYLON", role: "Voice of the Queen" },
{ name: "HORUS", symbol: "👁️", color: 0xffd700, glow: 0xffd700, pos: [0, 4, -42], room: "HOLY OF HOLIES", role: "Lord of the Sky" },
];
const agentMeshes: any[] = [];
agents.forEach((agent, i) => {
// Create Egyptian-styled agent (hieroglyphic figure shape)
const group = new THREE.Group();
// Body (wrapped mummy style for some, human for others)
const bodyGeo = new THREE.CylinderGeometry(0.3, 0.5, 1.5, 8);
const bodyMat = new THREE.MeshStandardMaterial({
color: agent.color,
emissive: agent.color,
emissiveIntensity: 0.4,
metalness: 0.2,
roughness: 0.6
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.y = 0.75;
group.add(body);
// Head (sphere for all, representing hieroglyphic style)
const headGeo = new THREE.SphereGeometry(0.5, 16, 16);
const head = new THREE.Mesh(headGeo, bodyMat);
head.position.y = 1.8;
group.add(head);
// Symbol on chest (floating element)
const symbolGeo = new THREE.CircleGeometry(0.3, 16);
const symbolMat = new THREE.MeshBasicMaterial({ color: agent.glow });
const symbol = new THREE.Mesh(symbolGeo, symbolMat);
symbol.position.set(0, 1, 0.35);
group.add(symbol);
// Glow aura
const glowGeo = new THREE.SphereGeometry(1.2, 16, 16);
const glowMat = new THREE.MeshBasicMaterial({
color: agent.glow,
transparent: true,
opacity: 0.15
});
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.y = 1.2;
group.add(glow);
group.position.set(agent.pos[0], agent.pos[1], agent.pos[2]);
group.userData = { name: agent.name, symbol: agent.symbol, role: agent.role, room: agent.room };
group.castShadow = true;
scene.add(group);
agentMeshes.push(group);
});
// ============== ANIMATION ==============
let time = 0;
const clock = new THREE.Clock();
const animate = () => {
if (cancelled) return;
requestAnimationFrame(animate);
const delta = clock.getDelta();
time += delta;
// Animate agents - floating and bobbing motion
agentMeshes.forEach((agent, i) => {
const originalY = agents[i].pos[1];
const bobSpeed = 1 + i * 0.2;
const bobAmount = 0.3;
// Floating bob
agent.position.y = originalY + Math.sin(time * bobSpeed) * bobAmount;
// Gentle rotation
agent.rotation.y = Math.sin(time * 0.5 + i) * 0.2;
// Symbol pulsing glow
const child = agent.children[2]; // symbol
if (child && child.material) {
child.material.opacity = 0.3 + Math.sin(time * 2 + i) * 0.2;
}
});
// Animate Eye of Horus
eye.rotation.z = Math.sin(time * 0.3) * 0.1;
eyeCenter.material.emissiveIntensity = 0.2 + Math.sin(time * 2) * 0.1;
// Animate lake water
lake.material.opacity = 0.7 + Math.sin(time * 0.5) * 0.1;
renderer.render(scene, camera);
};
animate();
// ============== CONTROLS ==============
let isDragging = false;
let theta = 0;
let phi = Math.PI / 6;
let target = new THREE.Vector3(0, 5, -20);
let radius = 50;
const updateCamera = () => {
camera.position.x = target.x + radius * Math.sin(phi) * Math.cos(theta);
camera.position.y = target.y + radius * Math.cos(phi);
camera.position.z = target.z + radius * Math.sin(phi) * Math.sin(theta);
camera.lookAt(target);
};
const onPointerDown = (e: PointerEvent) => {
isDragging = true;
};
const onPointerUp = () => {
isDragging = false;
};
const onPointerMove = (e: PointerEvent) => {
if (!isDragging) return;
theta -= e.movementX * 0.005;
phi = Math.max(0.1, Math.min(Math.PI / 2, phi + e.movementY * 0.005));
updateCamera();
};
const onWheel = (e: WheelEvent) => {
radius = Math.max(20, Math.min(100, radius + e.deltaY * 0.05));
updateCamera();
};
container.addEventListener("pointerdown", onPointerDown);
container.addEventListener("pointerup", onPointerUp);
container.addEventListener("pointermove", onPointerMove);
container.addEventListener("wheel", onWheel);
// Click detection
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const onClick = (e: MouseEvent) => {
const rect = container.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / container.clientWidth) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / container.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(agentMeshes, true);
// Find parent group
for (const hit of intersects) {
let obj: any = hit.object;
while (obj.parent && !obj.userData.name) {
obj = obj.parent;
}
if (obj.userData.name) {
setSelectedAgent(obj.userData);
break;
}
}
};
container.addEventListener("click", onClick);
updateCamera();
setLoading(false);
// Handle resize
const handleResize = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
};
window.addEventListener("resize", handleResize);
// Cleanup
return () => {
cancelled = true;
window.removeEventListener("resize", handleResize);
container.removeEventListener("pointerdown", onPointerDown);
container.removeEventListener("pointerup", onPointerUp);
container.removeEventListener("pointermove", onPointerMove);
container.removeEventListener("wheel", onWheel);
container.removeEventListener("click", onClick);
if (container.contains(renderer.domElement)) {
container.removeChild(renderer.domElement);
}
renderer.dispose();
};
});
}, []);
return (
<div className="min-h-screen bg-gradient-to-b from-stone-950 to-stone-900">
{/* Header */}
<div className="text-center py-3 px-4 bg-black/30">
<h1 className="text-2xl md:text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-200 via-yellow-400 to-amber-200">
🏛 TEMPLE OF AI - KARNAK
</h1>
<p className="text-amber-300/60 text-xs mt-1">
Drag to orbit Scroll to zoom Click agents for details
</p>
</div>
{/* Loading */}
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-stone-950 z-50">
<div className="text-center">
<div className="text-6xl mb-4 animate-pulse">🏛</div>
<p className="text-amber-300">Building Karnak Temple...</p>
</div>
</div>
)}
{/* 3D Canvas */}
<div
ref={containerRef}
className="w-full h-[calc(100vh-100px)]"
style={{ cursor: "grab", touchAction: "none" }}
/>
{/* Agent Info Panel */}
{selectedAgent && (
<div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 bg-black/90 backdrop-blur-xl rounded-2xl p-5 border border-amber-500/40 shadow-2xl z-50">
<div className="flex items-start justify-between mb-3">
<div>
<span className="text-4xl">{selectedAgent.symbol}</span>
</div>
<button
onClick={() => setSelectedAgent(null)}
className="text-amber-400 hover:text-amber-300 text-2xl leading-none"
>
×
</button>
</div>
<h3 className="text-xl font-bold text-amber-200 mb-1">{selectedAgent.name}</h3>
<p className="text-amber-400/60 text-sm mb-3">{selectedAgent.role}</p>
<div className="bg-stone-800/50 rounded-lg p-3">
<p className="text-xs text-amber-300/60 mb-1">Location</p>
<p className="text-amber-200 font-medium">{selectedAgent.room}</p>
</div>
</div>
)}
{/* Bottom Legend */}
<div className="fixed bottom-4 left-4 bg-black/70 backdrop-blur rounded-xl p-3 border border-amber-500/20">
<h4 className="text-amber-200 font-bold text-sm mb-2">🏛 Temple Map</h4>
<div className="text-xs text-amber-100/70 space-y-1">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-amber-500"></span>
Avenue of Sphinxes
</div>
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-amber-600"></span>
First Pylon
</div>
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-amber-700"></span>
Courtyard
</div>
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-amber-800"></span>
Hypostyle Hall
</div>
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-yellow-600"></span>
Sanctuary
</div>
</div>
</div>
</div>
);
}
@@ -1,720 +1,172 @@
"use client";
import { useState, useEffect } from "react";
import { useMissionControl } from "@/lib/mission-control/store";
import { TaskStatus } from "@/lib/mission-control/types";
import HorusChat from "./HorusChat";
import MondayBoard from "./MondayBoard";
import { TaskCardsPanel } from "./TaskCardsPanel";
import { TaskHistoryPanel } from "./TaskHistoryPanel";
import { TradingPanel } from "./TradingPanel";
import AIManagement from "@/components/ai-management/AIManagement";
import Council from "@/components/council/Council";
import AutoRunPanel from "./AutoRunPanel";
import ExecutionLogsPanel from "./ExecutionLogsPanel";
import ChangeLogPanel from "./ChangeLogPanel";
import BrainownPanel from "./BrainownPanel";
import { useState } from "react";
import Link from "next/link";
interface SidebarItem {
id: string;
name: string;
icon: string;
color?: string;
category: string;
}
export default function MissionControlDashboard() {
const [expandedCategory, setExpandedCategory] = useState<string | null>("council");
interface SidebarCategory {
id: string;
name: string;
icon: string;
items: SidebarItem[];
}
const sidebarCategories: SidebarCategory[] = [
{ id: "council", name: "Council", icon: "🏛️", items: [
{ id: "teams", name: "Agent Teams", icon: "👥", category: "council" },
{ id: "golden-notes", name: "Golden Notes", icon: "🔥", category: "golden-notes" },
{ id: "daily-feedback", name: "Daily Feedback", icon: "📝", category: "daily-feedback" },
{ id: "ai-settings", name: "AI Settings", icon: "🤖", category: "council-settings" },
]},
{ id: "leads", name: "Leads", icon: "📈", items: [
{ id: "leads-crm", name: "CRM", icon: "📊", category: "leads" },
{ id: "hp-submissions", name: "HP Submissions", icon: "📬", category: "hp-submissions" },
{ id: "dashboard", name: "Client Dashboard", icon: "🏢", category: "dashboard" },
]},
{ id: "projects", name: "Projects", icon: "🎯", items: [
{ id: "monday", name: "Monday Board", icon: "📊", category: "monday" },
{ id: "sitemente", name: "SiteMente", icon: "🌐", color: "#ff7bc0", category: "projects" },
{ id: "demos", name: "Demo Pages", icon: "🎨", category: "demos" },
{ id: "holacompi", name: "HolaCompi", icon: "🤝", color: "#6366f1", category: "projects" },
{ id: "hookd", name: "Hookd", icon: "📌", color: "#4F46E5", category: "hookd" },
{ id: "arabredox", name: "Arabredox", icon: "💚", color: "#22c55e", category: "projects" },
{ id: "infrastructure", name: "Infra", icon: "⚙️", color: "#10b981", category: "projects" },
]},
{ id: "trading", name: "Trading", icon: "📈", items: [
{ id: "trading-research", name: "Deep Research", icon: "🔬", category: "trading" },
{ id: "trading-strategies", name: "Strategies", icon: "🎯", category: "trading" },
{ id: "trading-execution", name: "Execution", icon: "⚡", category: "trading" },
{ id: "trading-journal", name: "Journal", icon: "📔", category: "trading" },
]},
{ id: "tasks", name: "Tasks", icon: "", items: [
{ id: "task-cards", name: "Task Cards", icon: "☑️", category: "task-cards" },
{ id: "task-history", name: "History", icon: "📜", category: "task-history" },
]},
{ id: "chat", name: "Chat", icon: "💬", items: [
{ id: "voice", name: "Voice Chat", icon: "🎤", category: "chat" },
{ id: "horus-chat", name: "Horus Chat", icon: "👁️", category: "horus-chat" },
]},
{ id: "automation", name: "Automation", icon: "⚡", items: [
{ id: "autorun", name: "Auto-Run", icon: "🔄", category: "autorun" },
{ id: "execution-logs", name: "Exec Logs", icon: "📊", category: "execution-logs" },
{ id: "changelog", name: "Change Log", icon: "📝", category: "changelog" },
{ id: "brainown", name: "Brainown", icon: "🧠", category: "brainown" },
]},
{ id: "calendar", name: "Calendar", icon: "📅", items: [
{ id: "brief", name: "Morning Brief", icon: "☀️", category: "calendar" },
{ id: "briefs", name: "All Briefs", icon: "📋", category: "briefs" },
{ id: "transcripts", name: "Transcripts", icon: "🎬", category: "transcripts" },
]},
{ id: "memory", name: "Memory", icon: "🧠", items: [
{ id: "logs", name: "Session Logs", icon: "📝", category: "memory" },
{ id: "snapshots", name: "Snapshots", icon: "📸", category: "snapshots" },
{ id: "research", name: "Deep Research", icon: "🔍", category: "research" },
]},
{ id: "docs", name: "Docs", icon: "📚", items: [
{ id: "docs-index", name: "Documentation", icon: "📚", category: "docs" },
]},
];
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" },
};
interface MissionControlDashboardProps {
onLogout?: () => void;
}
export default function MissionControlDashboard({ onLogout }: MissionControlDashboardProps) {
const { tasks, toggleTask, updateTaskStatus, addTask, getProjectProgress, getTasksByProject } = useMissionControl();
const [mounted, setMounted] = useState(false);
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(() => {
setMounted(true);
}, []);
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("");
};
if (!mounted) {
return (
<div className="min-h-screen bg-[#1a1625] text-white flex items-center justify-center">
<div className="text-white/60">Loading...</div>
</div>
);
}
// Golden Notes - moved to Council tab
// Removed from home page per user request
const categories = [
{
id: "council",
name: "Council",
icon: "👁️",
items: [
{ id: "temple", name: "Temple of AI", icon: "🏛️", color: "#fbbf24", href: "/mission-control/temple" },
{ id: "office", name: "Claw3D Office", icon: "🏢", color: "#22c55e", href: "/mission-control/office" },
{ id: "memory", name: "Daily Memory", icon: "📝", color: "#a855f7", href: "/mission-control/memory" },
{ id: "claude", name: "Claude Chat", icon: "💬", color: "#ff6154", href: "/mission-control/claude-chat" },
]
},
{
id: "personal",
name: "Personal",
icon: "👤",
items: [
{ id: "resume", name: "Resume Builder", icon: "📄", color: "#10b981", href: "/mission-control/resume" },
{ id: "pdf-viewer", name: "PDF Viewer", icon: "📰", color: "#6366f1", href: "/mission-control/pdf-viewer" },
]
},
{
id: "sites",
name: "Sites",
icon: "🌐",
items: [
{ id: "sitemente", name: "SiteMente", icon: "🌐", color: "#3b82f6", href: "https://sitemente.com", external: true },
{ id: "hostpioneers", name: "HostPioneers", icon: "🚀", color: "#22c55e", href: "https://hostpioneers.com", external: true },
{ id: "immortalz", name: "Immortalz", icon: "🎮", color: "#a855f7", href: "https://immortalz.org", external: true },
]
},
{
id: "tools",
name: "Tools",
icon: "🛠️",
items: [
{ id: "leads", name: "Lead Manager", icon: "📋", color: "#f59e0b", href: "/mission-control/leads" },
{ id: "email", name: "Email Campaign", icon: "📧", color: "#ef4444", href: "/mission-control/email" },
{ id: "trading", name: "Trading Panel", icon: "📈", color: "#10b981", href: "/mission-control/trading" },
]
},
{
id: "agents",
name: "Agents",
icon: "🤖",
items: [
{ id: "agents-list", name: "Agent Roster", icon: "👥", color: "#8b5cf6", href: "/mission-control/agents" },
{ id: "sessions", name: "Sessions", icon: "💬", color: "#06b6d4", href: "/mission-control/sessions" },
]
},
];
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 className="flex h-screen bg-slate-950 text-white overflow-hidden">
{/* Sidebar */}
<div className="w-64 bg-slate-900 border-r border-slate-800 flex flex-col flex-shrink-0">
{/* Logo */}
<div className="p-4 border-b border-slate-800">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center text-xl">👁</div>
<div>
<h1 className="font-bold text-lg">Mission Control</h1>
<p className="text-xs text-slate-500">Horus OS v2.0</p>
</div>
</div>
</div>
<nav className="space-y-1">
{sidebarCategories.map((category) => (
{/* Navigation */}
<div className="flex-1 overflow-y-auto p-3 space-y-1">
{categories.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>
{/* Category Header */}
<button
onClick={() => setExpandedCategory(expandedCategory === category.id ? null : category.id)}
className="w-full flex items-center justify-between px-2 py-2 text-xs font-semibold text-slate-400 uppercase tracking-wider hover:text-white transition-colors"
>
<div className="flex items-center gap-2">
<span>{category.icon}</span>
<span>{category.name}</span>
</div>
<span className={`transition-transform ${expandedCategory === category.id ? "rotate-90" : ""}`}></span>
</button>
{expandedCategories.includes(category.id) && (
<div className="ml-4 mt-1 space-y-1">
{/* Category Items */}
{expandedCategory === category.id && (
<div className="ml-2 space-y-0.5 mt-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>
item.external ? (
<a
key={item.id}
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 px-3 py-2 rounded-lg transition-all text-sm text-slate-400 hover:bg-slate-800/50 hover:text-white"
>
<span className="text-base">{item.icon}</span>
<span className="flex-1">{item.name}</span>
{item.color && (
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color }} />
)}
<span className="text-xs"></span>
</a>
) : (
<Link
key={item.id}
href={item.href || "#"}
className="flex items-center gap-3 px-3 py-2 rounded-lg transition-all text-sm text-slate-400 hover:bg-slate-800/50 hover:text-white"
>
<span className="text-base">{item.icon}</span>
<span className="flex-1">{item.name}</span>
{item.color && (
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color }} />
)}
</Link>
)
))}
</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>
{onLogout && (
<button onClick={onLogout} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-red-400 hover:bg-red-500/10 transition">🚪 Logout</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">
{/* Footer */}
<div className="p-3 border-t border-slate-800">
<div className="bg-slate-800/50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-xs text-green-400 font-medium">Systems Operational</span>
</div>
<p className="text-xs text-slate-500">Horus is watching 👁</p>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top Bar */}
<div className="h-14 bg-slate-900/50 border-b border-slate-800 flex items-center justify-between px-6">
<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>
<span className="text-slate-400 text-sm">Mission Control</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-xs text-slate-500">
<span>Horus v2.0</span>
</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>
</div>
{currentItem?.category === "chat" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><HorusChat /></div>}
{currentItem?.category === "horus-chat" && (
<div className="h-full">
<HorusChat />
</div>
)}
{currentItem?.category === "monday" && <div className="h-full"><MondayBoard /></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 === "golden-notes" && (
<div className="rounded-xl border border-amber-500/30 bg-gradient-to-br from-[#1a1625] to-[#2d1f3d] p-6">
<div className="flex items-center gap-2 mb-4">
<span className="text-amber-400 text-2xl">🔥</span>
<h3 className="font-bold text-amber-400 text-xl">Golden Notes</h3>
</div>
<p className="text-white/60 mb-6">Insights and wisdom from the agent council</p>
<div className="space-y-4">
<div className="p-4 rounded-lg bg-white/5 border border-amber-500/20">
<div className="flex items-center gap-2 mb-2">
<span className="text-amber-400">👁</span>
<span className="font-semibold text-amber-400">HORUS</span>
</div>
<ul className="text-sm text-white/80 space-y-1">
<li> Your frame: "safe" should be "leading"</li>
<li> Own the Egyptian-Scandinavian contrast</li>
<li> Fun first, lead often, let go</li>
</ul>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-blue-500/20">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-400">👁</span>
<span className="font-semibold text-blue-400">THOTH (Strategy)</span>
</div>
<ul className="text-sm text-white/80 space-y-1">
<li> Stop tracking "case notes" creates attachment</li>
<li> Signal intent early: comfort curiosity desire</li>
</ul>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-green-500/20">
<div className="flex items-center gap-2 mb-2">
<span className="text-green-400">🏗</span>
<span className="font-semibold text-green-400">PTAH (Builder)</span>
</div>
<ul className="text-sm text-white/80 space-y-1">
<li> You're over-engineering. Strip it down.</li>
<li>• Friendzone = too much comfort before desire</li>
</ul>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-yellow-500/20">
<div className="flex items-center gap-2 mb-2">
<span className="text-yellow-400">📜</span>
<span className="font-semibold text-yellow-400">SESHAT (Records)</span>
</div>
<ul className="text-sm text-white/80 space-y-1">
<li>• Delete case notes entirely</li>
<li>• Write forward, analyze backward less</li>
</ul>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-purple-500/20">
<div className="flex items-center gap-2 mb-2">
<span className="text-purple-400">🐺</span>
<span className="font-semibold text-purple-400">ANUBIS (Connector)</span>
</div>
<ul className="text-sm text-white/80 space-y-1">
<li>• Calm + leading = attractive | Calm + passive = friend</li>
<li>• Make small demands, create reasons to meet</li>
</ul>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-cyan-500/20">
<div className="flex items-center gap-2 mb-2">
<span className="text-cyan-400">📈</span>
<span className="font-semibold text-cyan-400">THOTH TRADING (Observer)</span>
</div>
<ul className="text-sm text-white/80 space-y-1">
<li>• You're in accumulation, need breakout</li>
<li> Introduce escalation every 7-10 messages</li>
</ul>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-red-500/20">
<div className="flex items-center gap-2 mb-2">
<span className="text-red-400"></span>
<span className="font-semibold text-red-400">SEKHMET (Action)</span>
</div>
<ul className="text-sm text-white/80 space-y-1">
<li> You're too nice in approach</li>
<li>• Tease more, challenge more, lead more</li>
</ul>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto p-6">
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="text-8xl mb-6 opacity-50">👁</div>
<h2 className="text-2xl font-bold mb-2">Mission Control</h2>
<p className="text-slate-500 max-w-md">
Your AI command center. Select a tool from the sidebar to begin.
</p>
</div>
</div>
)}
{currentItem?.category === "daily-feedback" && (
<div className="rounded-xl border border-blue-500/30 bg-gradient-to-br from-[#1a1625] to-[#1a2a3d] p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="text-blue-400 text-2xl">📝</span>
<h3 className="font-bold text-blue-400 text-xl">Daily Feedback</h3>
</div>
<span className="text-xs text-white/50">{new Date().toLocaleDateString()}</span>
</div>
<p className="text-white/60 mb-4">End-of-day insights from all agents. Select notes to promote to Golden.</p>
<div className="space-y-4 mb-6">
<div className="p-4 rounded-lg bg-white/5 border border-blue-500/20">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-amber-400">👁️</span>
<span className="font-semibold">HORUS</span>
</div>
<label className="flex items-center gap-2 text-xs cursor-pointer">
<input type="checkbox" className="w-4 h-4 rounded accent-brand-pink" />
<span className="text-white/60">Promote to Golden</span>
</label>
</div>
<textarea className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm text-white/80 resize-none" rows={3} placeholder="What did you learn today?" />
</div>
<div className="p-4 rounded-lg bg-white/5 border border-blue-500/20">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-blue-400">👁️</span>
<span className="font-semibold">THOTH</span>
</div>
<label className="flex items-center gap-2 text-xs cursor-pointer">
<input type="checkbox" className="w-4 h-4 rounded accent-brand-pink" />
<span className="text-white/60">Promote to Golden</span>
</label>
</div>
<textarea className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm text-white/80 resize-none" rows={3} placeholder="What did you learn today?" />
</div>
<div className="p-4 rounded-lg bg-white/5 border border-blue-500/20">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-green-400">🏗️</span>
<span className="font-semibold">PTAH</span>
</div>
<label className="flex items-center gap-2 text-xs cursor-pointer">
<input type="checkbox" className="w-4 h-4 rounded accent-brand-pink" />
<span className="text-white/60">Promote to Golden</span>
</label>
</div>
<textarea className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm text-white/80 resize-none" rows={3} placeholder="What did you learn today?" />
</div>
<div className="p-4 rounded-lg bg-white/5 border border-blue-500/20">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-purple-400">🐺</span>
<span className="font-semibold">ANUBIS</span>
</div>
<label className="flex items-center gap-2 text-xs cursor-pointer">
<input type="checkbox" className="w-4 h-4 rounded accent-brand-pink" />
<span className="text-white/60">Promote to Golden</span>
</label>
</div>
<textarea className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm text-white/80 resize-none" rows={3} placeholder="What did you learn today?" />
</div>
</div>
<button className="w-full py-3 bg-brand-pink hover:bg-[#ff7bc0] rounded-lg font-medium transition">
💾 Save & Promote Selected
</button>
<p className="text-xs text-white/40 mt-4 text-center">
Feedback auto-generates at 9pm CET. Edit and promote what matters.
</p>
</div>
)}
{currentItem?.category === "trading" && <TradingPanel />}
{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>
<HorusChat />
</div>
)}
{currentItem?.category === "briefs" && (
<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">📋 All Briefs</h3>
<p className="text-white/60 mb-4">Morning, EOD, and Amun reports</p>
<a href="/mission-control/briefs" 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 Briefs</a>
</div>
</div>
)}
{currentItem?.category === "transcripts" && (
<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">🎬 YouTube Transcripts</h3>
<p className="text-white/60 mb-4">Save and organize video transcripts for learning</p>
<a href="/mission-control/transcripts" 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 Transcripts</a>
</div>
</div>
)}
{currentItem?.category === "research" && (
<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">🔍 Deep Research</h3>
<p className="text-white/60 mb-4">AI-powered research using Tavily API</p>
<a href="/mission-control/research" className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 rounded-lg text-sm font-medium hover:bg-blue-700 transition">🔍 Open Research</a>
</div>
</div>
)}
{/* BMHQ Automation Panels */}
{currentItem?.category === "autorun" && (
<div className="rounded-xl border border-green-500/30 bg-gradient-to-br from-[#1a2a1a] to-[#1a2a2a] p-6">
<AutoRunPanel />
</div>
)}
{currentItem?.category === "execution-logs" && (
<div className="rounded-xl border border-blue-500/30 bg-gradient-to-br from-[#1a1a2a] to-[#1a2a3a] p-6">
<ExecutionLogsPanel />
</div>
)}
{currentItem?.category === "changelog" && (
<div className="rounded-xl border border-purple-500/30 bg-gradient-to-br from-[#2a1a2a] to-[#2a1a3a] p-6">
<ChangeLogPanel />
</div>
)}
{currentItem?.category === "brainown" && (
<div className="rounded-xl border border-pink-500/30 bg-gradient-to-br from-[#2a1a2a] to-[#3a2a3a] p-6">
<BrainownPanel />
</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 === "snapshots" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">📸 Daily Snapshots</h3>
<span className="text-xs text-white/50">Last 7 days</span>
</div>
<p className="text-white/60 mb-4">Weekly snapshots of Mission Control state</p>
<div className="space-y-2">
<div className="p-3 rounded-lg bg-white/5 border border-white/10">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">2026-02-22</span>
<span className="text-xs text-green-400">✓ Saved</span>
</div>
</div>
<div className="p-3 rounded-lg bg-white/5 border border-white/10">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">2026-02-21</span>
<span className="text-xs text-green-400">✓ Saved</span>
</div>
</div>
<div className="p-3 rounded-lg bg-white/5 border border-white/10">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">2026-02-20</span>
<span className="text-xs text-green-400">✓ Saved</span>
</div>
</div>
<div className="p-3 rounded-lg bg-white/5 border border-white/10 opacity-50">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">2026-02-19</span>
<span className="text-xs text-white/40">-</span>
</div>
</div>
</div>
<p className="text-xs text-white/40 mt-4 text-center">
Snapshots taken daily at midnight. Max 7 days retained.
</p>
</div>
)}
{currentItem?.category === "docs" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h3 className="text-lg font-semibold mb-4">📚 Documentation</h3>
<p className="text-white/60 mb-4">Long-term docs and guides</p>
<div className="space-y-3">
<a href="/mission-control/docs" target="_blank" className="flex items-center gap-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition">
<span className="text-2xl">📖</span>
<div><p className="font-medium">Docs Index</p><p className="text-xs text-white/50">All documentation</p></div>
</a>
<a href="https://github.com/HaithamEKhalifa/SiteMente" target="_blank" className="flex items-center gap-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition">
<span className="text-2xl">🐙</span>
<div><p className="font-medium">GitHub Repo</p><p className="text-xs text-white/50">Source code & issues</p></div>
</a>
<a href="https://sitemente.com" target="_blank" className="flex items-center gap-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition">
<span className="text-2xl">🌐</span>
<div><p className="font-medium">Live Site</p><p className="text-xs text-white/50">sitemente.com</p></div>
</a>
</div>
</div>
)}
{/* Task Cards View */}
{currentItem?.category === "task-cards" && (
<TaskCardsPanel tasks={tasks} toggleTask={toggleTask} />
)}
{/* Task History View */}
{currentItem?.category === "task-history" && (
<TaskHistoryPanel />
)}
{currentItem?.category === "demos" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8">
<div className="text-5xl mb-4">🎨</div>
<h3 className="text-xl font-bold mb-2">Demo Pages</h3>
<p className="text-white/60 mb-6">Vertical demo pages for SiteMente</p>
<div className="flex flex-wrap justify-center gap-3">
<a href="/demos" target="_blank" className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">🌐 All Demos</a>
<a href="/demos?vertical=real-estate" target="_blank" className="px-4 py-2 bg-green-500/20 border border-green-500/30 rounded-lg text-sm hover:bg-green-500/30 transition">🏠 Real Estate</a>
<a href="/demos?vertical=restaurant" target="_blank" className="px-4 py-2 bg-yellow-500/20 border border-yellow-500/30 rounded-lg text-sm hover:bg-yellow-500/30 transition">🍽️ Restaurant</a>
<a href="/demos?vertical=clinic" target="_blank" className="px-4 py-2 bg-cyan-500/20 border border-cyan-500/30 rounded-lg text-sm hover:bg-cyan-500/30 transition">⚕️ Clinic</a>
<a href="/demos?vertical=home-services" target="_blank" className="px-4 py-2 bg-purple-500/20 border border-purple-500/30 rounded-lg text-sm hover:bg-purple-500/30 transition">🔧 Home Services</a>
</div>
</div>
</div>
)}
{currentItem?.category === "leads" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8">
<div className="text-5xl mb-4">📈</div>
<h3 className="text-xl font-bold mb-2">Leads CRM</h3>
<p className="text-white/60 mb-6">Track and manage your leads</p>
<div className="flex flex-wrap justify-center gap-3">
<a href="/leads" target="_blank" className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">📈 Open Leads CRM</a>
</div>
</div>
</div>
)}
{currentItem?.category === "hp-submissions" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8">
<div className="text-5xl mb-4">📬</div>
<h3 className="text-xl font-bold mb-2">HP Submissions</h3>
<p className="text-white/60 mb-6">HostPioneers contact form submissions</p>
<div className="flex flex-wrap justify-center gap-3">
<a href="/mission-control/hp-submissions" target="_blank" className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">📬 View Submissions</a>
</div>
</div>
</div>
)}
{currentItem?.category === "dashboard" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8">
<div className="text-5xl mb-4">🏢</div>
<h3 className="text-xl font-bold mb-2">Client Dashboard</h3>
<p className="text-white/60 mb-6">Where your clients see bookings & leads</p>
<div className="flex flex-wrap justify-center gap-3">
<a href="/dashboard" target="_blank" className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">🏢 Open Dashboard Demo</a>
</div>
</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="arabredox">Arabredox</option>
<option value="infrastructure">Infrastructure</option>
<option value="hookd">Hookd</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>
</>
)}
{currentItem?.category === "hookd" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8">
<div className="text-5xl mb-4">📌</div>
<h3 className="text-xl font-bold mb-2">Hookd</h3>
<p className="text-white/60 mb-6">Save, organize and AI-sort your X content</p>
<div className="flex flex-wrap justify-center gap-3 mb-4">
<a href="/extensions/hookd" target="_blank" className="px-4 py-2 bg-indigo-600 rounded-lg text-sm font-medium hover:bg-indigo-700 transition">📥 Download Extension</a>
<a href="https://github.com/HaithamEKhalifa/hookd" target="_blank" className="px-4 py-2 bg-white/10 rounded-lg text-sm hover:bg-white/20 transition">🐙 GitHub</a>
</div>
<div className="grid grid-cols-2 gap-4 max-w-md mx-auto mt-6 text-left">
<div className="bg-white/5 rounded-lg p-3">
<span className="text-lg">📥</span>
<p className="text-xs text-white/60 mt-1">DM Organizer</p>
</div>
<div className="bg-white/5 rounded-lg p-3">
<span className="text-lg">🐦</span>
<p className="text-xs text-white/60 mt-1">Feed Saver</p>
</div>
<div className="bg-white/5 rounded-lg p-3">
<span className="text-lg">🤖</span>
<p className="text-xs text-white/60 mt-1">AI Processing</p>
</div>
<div className="bg-white/5 rounded-lg p-3">
<span className="text-lg">🏷</span>
<p className="text-xs text-white/60 mt-1">Custom Tags</p>
</div>
</div>
</div>
</div>
)}
</main>
</div>
</div>
</div>
);
}
@@ -0,0 +1,546 @@
"use client";
import { useState, useEffect } from "react";
interface Agent {
id: string;
name: string;
role: string;
symbol: string;
status: "active" | "idle" | "busy" | "offline";
currentTask: string;
location: string;
templeArea: string;
tasksCompleted: number;
}
interface TempleRoom {
id: string;
name: string;
agent: Agent | null;
status: "active" | "inactive";
description: string;
}
const ANVEVOICE_API_KEY = "anvk_6a217415c671b8e613df1f0b37f72c492a91b625";
const CLEOPATRA_BOT_ID = "022ae3d3-ed11-45a9-b663-f4a6dfa34f77";
export default function TempleOfAIDashboard() {
const [activeRoom, setActiveRoom] = useState<string>("sanctuary");
const [anvevoiceStats, setAnvevoiceStats] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
// Egyptian Agents
const agents: Agent[] = [
{ id: "horus", name: "Horus", role: "Master Orchestrator", symbol: "👁️", status: "active", currentTask: "Monitoring all systems", location: "Sanctuary", templeArea: "Head", tasksCompleted: 847 },
{ id: "cleopatra", name: "Cleopatra", role: "Voice AI / Sales", symbol: "👑", status: "active", currentTask: "Conversing with visitor", location: "Hall 2", templeArea: "Voice Lab", tasksCompleted: 234 },
{ id: "anubis", name: "Anubis", role: "Outreach / Lead Gen", symbol: "🐕", status: "busy", currentTask: "Qualifying lead #4521", location: "Entrance", templeArea: "Reception", tasksCompleted: 156 },
{ id: "thoth", name: "Thoth", role: "Research / Strategy", symbol: "🐦", status: "idle", currentTask: "Analyzing market trends", location: "Pylon 2", templeArea: "Research", tasksCompleted: 89 },
{ id: "ptah", name: "Ptah", role: "Development / Ops", symbol: "🔨", status: "active", currentTask: "Deploying SiteMente v2.1", location: "Courtyard", templeArea: "Workspace", tasksCompleted: 312 },
{ id: "seshat", name: "Seshat", role: "Content / SEO", symbol: "📝", status: "idle", currentTask: "Writing blog post", location: "Courtyard", templeArea: "Workspace", tasksCompleted: 445 },
{ id: "sekhmet", name: "Sekhmet", role: "Trading / Risk", symbol: "🦁", status: "active", currentTask: "Monitoring BTC position", location: "Courtyard", templeArea: "Trading", tasksCompleted: 67 },
{ id: "maat", name: "Maat", role: "Quality Assurance", symbol: "⚖️", status: "idle", currentTask: "Reviewing code quality", location: "Pylon 3", templeArea: "QA Lab", tasksCompleted: 198 },
];
const templeRooms: TempleRoom[] = [
{ id: "sanctuary", name: "Holy of Holies", agent: agents[0], status: "active", description: "Horus commands from the golden throne. The Eye of Providence sees all." },
{ id: "hall2", name: "Hall of Queens", agent: agents[1], status: "active", description: "Cleopatra's voice echoes through the halls. Visitors feel welcomed." },
{ id: "pylon3", name: "Pylon of Maat", agent: agents[7], status: "active", description: "The Feather of Truth guards quality standards." },
{ id: "pylon2", name: "Pylon of Thoth", agent: agents[3], status: "active", description: "Sacred knowledge flows through ibis-headed archives." },
{ id: "hypostyle", name: "Hypostyle Hall", agent: null, status: "active", description: "The great meeting hall. Columns reach to heaven." },
{ id: "courtyard", name: "Courtyard", agent: null, status: "active", description: "Ptah builds, Seshat writes, Sekhmet trades. The heart beats strong." },
{ id: "entrance", name: "Avenue of Sphinxes", agent: agents[2], status: "active", description: "Anubis greets visitors. No soul passes unseen." },
];
const getStatusColor = (status: string) => {
switch(status) {
case "active": return "bg-green-500";
case "busy": return "bg-yellow-500";
case "idle": return "bg-blue-500";
case "offline": return "bg-gray-500";
default: return "bg-gray-500";
}
};
const getStatusText = (status: string) => {
switch(status) {
case "active": return "Working";
case "busy": return "On Task";
case "idle": return "Ready";
case "offline": return "Offline";
default: return status;
}
};
useEffect(() => {
// Fetch AnveVoice stats
const fetchStats = async () => {
try {
const res = await fetch("/api/anvevoice-stats");
if (res.ok) {
const data = await res.json();
setAnvevoiceStats(data);
}
} catch (e) {
console.error("Failed to fetch AnveVoice stats");
}
};
fetchStats();
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-amber-950 via-stone-900 to-amber-950 p-6">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-200 via-yellow-400 to-amber-200 mb-2">
🏛 TEMPLE OF AI
</h1>
<p className="text-amber-300/80 text-lg">
Where Ancient Wisdom Meets Machine Intelligence
</p>
<div className="flex justify-center gap-4 mt-4">
<span className="px-4 py-1 bg-amber-500/20 text-amber-300 rounded-full text-sm border border-amber-500/30">
🟢 {agents.filter(a => a.status === "active").length} Active
</span>
<span className="px-4 py-1 bg-yellow-500/20 text-yellow-300 rounded-full text-sm border border-yellow-500/30">
🟡 {agents.filter(a => a.status === "busy").length} Busy
</span>
<span className="px-4 py-1 bg-blue-500/20 text-blue-300 rounded-full text-sm border border-blue-500/30">
🔵 {agents.filter(a => a.status === "idle").length} Ready
</span>
</div>
</div>
{/* Temple Visualization */}
<div className="max-w-7xl mx-auto grid lg:grid-cols-3 gap-6">
{/* Left Column - Temple Map */}
<div className="lg:col-span-1">
<div className="bg-stone-900/80 backdrop-blur rounded-2xl border border-amber-500/20 p-6">
<h2 className="text-xl font-bold text-amber-200 mb-4 flex items-center gap-2">
🗺 Temple Map
</h2>
{/* Vertical Temple Layout */}
<div className="space-y-2">
{/* Sanctuary */}
<div
onClick={() => setActiveRoom("sanctuary")}
className={`p-3 rounded-xl border-2 cursor-pointer transition-all ${
activeRoom === "sanctuary"
? "border-yellow-400 bg-yellow-500/20"
: "border-amber-700/50 bg-stone-800/50 hover:border-amber-500"
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">👁</span>
<div>
<p className="font-bold text-amber-200">HOLY OF HOLIES</p>
<p className="text-xs text-amber-400/60">{agents[0].name} - {agents[0].role}</p>
</div>
<span className={`w-3 h-3 rounded-full ${getStatusColor(agents[0].status)} ml-auto`}></span>
</div>
</div>
{/* Hall 2 */}
<div className="flex items-center justify-center">
<div className="w-0 h-0 border-l-8 border-r-8 border-t-8 border-l-transparent border-r-transparent border-t-amber-600"></div>
</div>
<div
onClick={() => setActiveRoom("hall2")}
className={`p-3 rounded-xl border-2 cursor-pointer transition-all ${
activeRoom === "hall2"
? "border-yellow-400 bg-yellow-500/20"
: "border-amber-700/50 bg-stone-800/50 hover:border-amber-500"
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">👑</span>
<div>
<p className="font-bold text-amber-200">HALL OF QUEENS</p>
<p className="text-xs text-amber-400/60">{agents[1].name} - Voice AI</p>
</div>
<span className={`w-3 h-3 rounded-full ${getStatusColor(agents[1].status)} ml-auto`}></span>
</div>
</div>
{/* Pylon 3 */}
<div className="flex items-center justify-center">
<div className="w-8 h-4 bg-amber-800 rounded-sm"></div>
</div>
<div
onClick={() => setActiveRoom("pylon3")}
className={`p-3 rounded-xl border-2 cursor-pointer transition-all ${
activeRoom === "pylon3"
? "border-yellow-400 bg-yellow-500/20"
: "border-amber-700/50 bg-stone-800/50 hover:border-amber-500"
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl"></span>
<div>
<p className="font-bold text-amber-200">PYLON OF MAAT</p>
<p className="text-xs text-amber-400/60">{agents[7].name} - Quality</p>
</div>
<span className={`w-3 h-3 rounded-full ${getStatusColor(agents[7].status)} ml-auto`}></span>
</div>
</div>
{/* Pylon 2 */}
<div className="flex items-center justify-center">
<div className="w-8 h-4 bg-amber-800 rounded-sm"></div>
</div>
<div
onClick={() => setActiveRoom("pylon2")}
className={`p-3 rounded-xl border-2 cursor-pointer transition-all ${
activeRoom === "pylon2"
? "border-yellow-400 bg-yellow-500/20"
: "border-amber-700/50 bg-stone-800/50 hover:border-amber-500"
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">🐦</span>
<div>
<p className="font-bold text-amber-200">PYLON OF THOTH</p>
<p className="text-xs text-amber-400/60">{agents[3].name} - Research</p>
</div>
<span className={`w-3 h-3 rounded-full ${getStatusColor(agents[3].status)} ml-auto`}></span>
</div>
</div>
{/* Hypostyle Hall */}
<div className="flex items-center justify-center">
<div className="w-8 h-4 bg-amber-700 rounded-sm"></div>
</div>
<div
onClick={() => setActiveRoom("hypostyle")}
className={`p-3 rounded-xl border-2 cursor-pointer transition-all ${
activeRoom === "hypostyle"
? "border-yellow-400 bg-yellow-500/20"
: "border-amber-700/50 bg-stone-800/50 hover:border-amber-500"
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl"></span>
<div>
<p className="font-bold text-amber-200">HYPOSTYLE HALL</p>
<p className="text-xs text-amber-400/60">Conference - Standups</p>
</div>
<span className="w-3 h-3 rounded-full bg-green-500 ml-auto"></span>
</div>
</div>
{/* Courtyard */}
<div className="flex items-center justify-center">
<div className="w-8 h-4 bg-amber-600 rounded-sm"></div>
</div>
<div
onClick={() => setActiveRoom("courtyard")}
className={`p-3 rounded-xl border-2 cursor-pointer transition-all ${
activeRoom === "courtyard"
? "border-yellow-400 bg-yellow-500/20"
: "border-amber-700/50 bg-stone-800/50 hover:border-amber-500"
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">🏛</span>
<div>
<p className="font-bold text-amber-200">COURTYARD</p>
<p className="text-xs text-amber-400/60">Ptah 🔨 | Seshat 📝 | Sekhmet 🦁</p>
</div>
<span className="w-3 h-3 rounded-full bg-green-500 ml-auto"></span>
</div>
</div>
{/* Entrance */}
<div className="flex items-center justify-center">
<div className="w-8 h-4 bg-amber-500 rounded-sm"></div>
</div>
<div
onClick={() => setActiveRoom("entrance")}
className={`p-3 rounded-xl border-2 cursor-pointer transition-all ${
activeRoom === "entrance"
? "border-yellow-400 bg-yellow-500/20"
: "border-amber-700/50 bg-stone-800/50 hover:border-amber-500"
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">🐕</span>
<div>
<p className="font-bold text-amber-200">AVENUE OF SPHINXES</p>
<p className="text-xs text-amber-400/60">{agents[2].name} - Reception</p>
</div>
<span className={`w-3 h-3 rounded-full ${getStatusColor(agents[2].status)} ml-auto`}></span>
</div>
</div>
</div>
</div>
</div>
{/* Center Column - Active Room Details */}
<div className="lg:col-span-1">
<div className="bg-stone-900/80 backdrop-blur rounded-2xl border border-amber-500/20 p-6">
<h2 className="text-xl font-bold text-amber-200 mb-4">
{templeRooms.find(r => r.id === activeRoom)?.name}
</h2>
<div className="space-y-4">
{activeRoom === "sanctuary" && (
<>
<div className="text-center py-8">
<span className="text-8xl">👁</span>
<h3 className="text-2xl font-bold text-yellow-400 mt-4">HORUS</h3>
<p className="text-amber-300">Master Orchestrator</p>
</div>
<div className="bg-yellow-500/10 rounded-xl p-4 border border-yellow-500/30">
<p className="text-yellow-200 font-medium">🗣 Current Task:</p>
<p className="text-amber-300 mt-1">{agents[0].currentTask}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-stone-800/50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-yellow-400">847</p>
<p className="text-xs text-amber-400/60">Tasks Done</p>
</div>
<div className="bg-stone-800/50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-green-400">99.9%</p>
<p className="text-xs text-amber-400/60">Uptime</p>
</div>
</div>
</>
)}
{activeRoom === "hall2" && (
<>
<div className="text-center py-8">
<span className="text-8xl">👑</span>
<h3 className="text-2xl font-bold text-yellow-400 mt-4">CLEOPATRA</h3>
<p className="text-amber-300">Voice AI / Sales</p>
<div className="mt-2 inline-flex items-center gap-2 px-3 py-1 bg-green-500/20 rounded-full border border-green-500/30">
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
<span className="text-green-300 text-sm">Live on AnveVoice</span>
</div>
</div>
<div className="bg-purple-500/10 rounded-xl p-4 border border-purple-500/30">
<p className="text-purple-200 font-medium">🗣 Welcome Message:</p>
<p className="text-amber-300 mt-1 italic">"Hola! Soy Cleopatra, tu asistente de ventas de HostPioneers. Como puedo ayudarte hoy?"</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-stone-800/50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-purple-400">234</p>
<p className="text-xs text-amber-400/60">Conversations</p>
</div>
<div className="bg-stone-800/50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-green-400">89%</p>
<p className="text-xs text-amber-400/60">Lead Capture</p>
</div>
</div>
<a
href="https://anvevoice.com/dashboard"
target="_blank"
className="block w-full py-3 bg-purple-600 hover:bg-purple-500 text-center rounded-xl font-bold text-white transition-colors"
>
📊 View AnveVoice Dashboard
</a>
</>
)}
{activeRoom === "courtyard" && (
<>
<div className="text-center py-4">
<span className="text-6xl">🏛</span>
<h3 className="text-xl font-bold text-yellow-400 mt-4">THE WORKING HEART</h3>
<p className="text-amber-300">Where the heart beats strong</p>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3 bg-stone-800/50 rounded-lg p-3">
<span className="text-2xl">🔨</span>
<div className="flex-1">
<p className="font-bold text-amber-200">Ptah - Dev/Ops</p>
<p className="text-xs text-amber-400/60">{agents[5].currentTask}</p>
</div>
<span className={`w-2 h-2 rounded-full ${getStatusColor(agents[5].status)}`}></span>
</div>
<div className="flex items-center gap-3 bg-stone-800/50 rounded-lg p-3">
<span className="text-2xl">📝</span>
<div className="flex-1">
<p className="font-bold text-amber-200">Seshat - Content</p>
<p className="text-xs text-amber-400/60">{agents[6].currentTask}</p>
</div>
<span className={`w-2 h-2 rounded-full ${getStatusColor(agents[6].status)}`}></span>
</div>
<div className="flex items-center gap-3 bg-stone-800/50 rounded-lg p-3">
<span className="text-2xl">🦁</span>
<div className="flex-1">
<p className="font-bold text-amber-200">Sekhmet - Trading</p>
<p className="text-xs text-amber-400/60">{agents[7].currentTask}</p>
</div>
<span className={`w-2 h-2 rounded-full ${getStatusColor(agents[7].status)}`}></span>
</div>
</div>
</>
)}
{activeRoom === "hypostyle" && (
<>
<div className="text-center py-8">
<span className="text-8xl"></span>
<h3 className="text-2xl font-bold text-yellow-400 mt-4">MEETING HALL</h3>
<p className="text-amber-300">Where decisions are made</p>
</div>
<div className="bg-stone-800/50 rounded-xl p-4">
<h4 className="text-amber-200 font-medium mb-3">📅 Next Standup</h4>
<div className="flex items-center gap-3">
<span className="text-2xl"></span>
<div>
<p className="text-white font-medium">9:00 AM - Daily Standup</p>
<p className="text-amber-400/60 text-sm">All agents gather</p>
</div>
</div>
</div>
<div className="bg-stone-800/50 rounded-xl p-4">
<h4 className="text-amber-200 font-medium mb-3">📋 Sprint Status</h4>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-amber-300">Progress</span>
<span className="text-green-400">72%</span>
</div>
<div className="w-full bg-stone-700 rounded-full h-2">
<div className="bg-green-500 h-2 rounded-full" style={{ width: "72%" }}></div>
</div>
</div>
</div>
</>
)}
{activeRoom === "entrance" && (
<>
<div className="text-center py-8">
<span className="text-8xl">🐕</span>
<h3 className="text-2xl font-bold text-yellow-400 mt-4">ANUBIS</h3>
<p className="text-amber-300">Guardian of Leads</p>
</div>
<div className="bg-stone-800/50 rounded-xl p-4">
<h4 className="text-amber-200 font-medium mb-3">🎯 Lead Pipeline</h4>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-amber-300">New Today</span>
<span className="text-yellow-400">23</span>
</div>
<div className="flex justify-between">
<span className="text-amber-300">Qualified</span>
<span className="text-green-400">8</span>
</div>
<div className="flex justify-between">
<span className="text-amber-300">Conversion</span>
<span className="text-blue-400">34%</span>
</div>
</div>
</div>
</>
)}
{(activeRoom === "pylon2" || activeRoom === "pylon3") && (
<>
<div className="text-center py-8">
<span className="text-8xl">{activeRoom === "pylon2" ? "🐦" : "⚖️"}</span>
<h3 className="text-2xl font-bold text-yellow-400 mt-4">
{activeRoom === "pylon2" ? "THOTH" : "MAAT"}
</h3>
<p className="text-amber-300">
{activeRoom === "pylon2" ? "Knowledge Keeper" : "Quality Guardian"}
</p>
</div>
<div className="bg-stone-800/50 rounded-xl p-4">
<h4 className="text-amber-200 font-medium mb-3">
{activeRoom === "pylon2" ? "🔬 Research" : "✓ Quality Metrics"}
</h4>
<div className="space-y-2">
{activeRoom === "pylon2" ? (
<>
<div className="flex justify-between">
<span className="text-amber-300">Market Analysis</span>
<span className="text-green-400">Complete</span>
</div>
<div className="flex justify-between">
<span className="text-amber-300">Competitor Report</span>
<span className="text-yellow-400">In Progress</span>
</div>
</>
) : (
<>
<div className="flex justify-between">
<span className="text-amber-300">Code Coverage</span>
<span className="text-green-400">94%</span>
</div>
<div className="flex justify-between">
<span className="text-amber-300">Bugs Fixed</span>
<span className="text-blue-400">12</span>
</div>
</>
)}
</div>
</div>
</>
)}
</div>
</div>
</div>
{/* Right Column - All Agents */}
<div className="lg:col-span-1">
<div className="bg-stone-900/80 backdrop-blur rounded-2xl border border-amber-500/20 p-6">
<h2 className="text-xl font-bold text-amber-200 mb-4">
👥 Divine Council
</h2>
<div className="space-y-3">
{agents.map(agent => (
<div
key={agent.id}
className="bg-stone-800/50 rounded-xl p-3 hover:bg-stone-700/50 transition-colors cursor-pointer"
onClick={() => setActiveRoom(agent.location === "Entrance" ? "entrance" : agent.location === "Hall 2" ? "hall2" : agent.location === "Pylon 2" ? "pylon2" : agent.location === "Pylon 3" ? "pylon3" : agent.location === "Courtyard" ? "courtyard" : "sanctuary")}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{agent.symbol}</span>
<div className="flex-1">
<p className="font-bold text-amber-200">{agent.name}</p>
<p className="text-xs text-amber-400/60">{agent.role}</p>
</div>
<div className="text-right">
<span className={`w-2 h-2 rounded-full ${getStatusColor(agent.status)} inline-block`}></span>
<p className="text-xs text-amber-400/60 mt-1">{getStatusText(agent.status)}</p>
</div>
</div>
<p className="text-xs text-amber-300/60 mt-2 ml-9 truncate">
{agent.currentTask}
</p>
</div>
))}
</div>
{/* Cleopatra's Voice Widget */}
<div className="mt-6 pt-6 border-t border-amber-500/20">
<h3 className="text-lg font-bold text-purple-300 mb-3">🎙 Cleopatra Voice</h3>
<div className="bg-purple-900/30 rounded-xl p-4 border border-purple-500/20">
<p className="text-purple-200 text-sm mb-3">Test Cleopatra's voice AI on AnveVoice:</p>
<a
href="https://anvevoice.com/dashboard"
target="_blank"
className="block w-full py-2 bg-purple-600 hover:bg-purple-500 text-center rounded-lg font-medium text-white text-sm transition-colors"
>
Open Voice Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="text-center mt-8 text-amber-400/40 text-sm">
<p>🏛 Temple of AI - Ancient Wisdom Meets Machine Intelligence</p>
<p className="mt-1">Powered by HostPioneers | Built on OpenClaw & AnveVoice</p>
</div>
</div>
);
}
+44
View File
@@ -1,4 +1,48 @@
[
{
"id": "eod-1774209602462",
"date": "2026-03-22",
"completed": [
"See Mission Control for details"
],
"progress": {},
"council": {},
"tomorrow": [],
"created_at": "2026-03-22T20:00:02.462Z"
},
{
"id": "eod-1774123202651",
"date": "2026-03-21",
"completed": [
"See Mission Control for details"
],
"progress": {},
"council": {},
"tomorrow": [],
"created_at": "2026-03-21T20:00:02.651Z"
},
{
"id": "eod-1774036802267",
"date": "2026-03-20",
"completed": [
"See Mission Control for details"
],
"progress": {},
"council": {},
"tomorrow": [],
"created_at": "2026-03-20T20:00:02.267Z"
},
{
"id": "eod-1773950402641",
"date": "2026-03-19",
"completed": [
"See Mission Control for details"
],
"progress": {},
"council": {},
"tomorrow": [],
"created_at": "2026-03-19T20:00:02.641Z"
},
{
"id": "eod-1773864002102",
"date": "2026-03-18",
+129
View File
@@ -1,4 +1,133 @@
[
{
"id": "morning-1774242071723",
"date": "2026-03-23",
"weather": {
"location": "Benalmádena",
"condition": "Sunny",
"temperature": "12°C",
"feels_like": "11°C",
"humidity": "82%",
"wind": "6km/h"
},
"priorities": [],
"leads": [],
"created_at": "2026-03-23T05:01:11.723Z"
},
{
"id": "morning-1774238402259",
"date": "2026-03-23",
"weather": "Check weather",
"market": {
"BTC": 68057,
"ETH": 2051,
"SOL": 86
},
"priorities": [
"Focus on SiteMente revenue",
"Fix voice widget API",
"Get first client"
],
"goal": "$3,000/month",
"leads": [],
"created_at": "2026-03-23T04:00:02.259Z"
},
{
"id": "morning-1774155707963",
"date": "2026-03-22",
"weather": "Unavailable",
"priorities": [],
"leads": [],
"created_at": "2026-03-22T05:01:47.963Z"
},
{
"id": "morning-1774152002609",
"date": "2026-03-22",
"weather": "Check weather",
"market": {
"BTC": 69333,
"ETH": 2113,
"SOL": 88
},
"priorities": [
"Focus on SiteMente revenue",
"Fix voice widget API",
"Get first client"
],
"goal": "$3,000/month",
"leads": [],
"created_at": "2026-03-22T04:00:02.609Z"
},
{
"id": "morning-1774069303865",
"date": "2026-03-21",
"weather": {
"location": "Benalmádena, Málaga",
"condition": "Patchy rain nearby",
"temp": "11°C",
"feels_like": "10°C",
"humidity": "84%",
"wind": "12 km/h ←"
},
"priorities": [],
"leads": [],
"created_at": "2026-03-21T05:01:43.865Z"
},
{
"id": "morning-1774069212054",
"date": "2026-03-21",
"priorities": [],
"leads": [],
"created_at": "2026-03-21T05:00:12.054Z"
},
{
"id": "morning-1774065602639",
"date": "2026-03-21",
"weather": "Check weather",
"market": {
"BTC": 70681,
"ETH": 2153,
"SOL": 90
},
"priorities": [
"Focus on SiteMente revenue",
"Fix voice widget API",
"Get first client"
],
"goal": "$3,000/month",
"leads": [],
"created_at": "2026-03-21T04:00:02.639Z"
},
{
"id": "morning-1773982837045",
"date": "2026-03-20",
"weather": {
"location": "Benalmádena, Málaga",
"current": "🌦 +13°C (feels like +12°C), wind ↖12km/h, humidity 80%",
"forecast": "Fri 20: ~15°C, Sat 21: ~10°C, Sun 22: ~10°C"
},
"priorities": [],
"leads": [],
"created_at": "2026-03-20T05:00:37.045Z"
},
{
"id": "morning-1773979202259",
"date": "2026-03-20",
"weather": "Check weather",
"market": {
"BTC": 70759,
"ETH": 2152,
"SOL": 89
},
"priorities": [
"Focus on SiteMente revenue",
"Fix voice widget API",
"Get first client"
],
"goal": "$3,000/month",
"leads": [],
"created_at": "2026-03-20T04:00:02.259Z"
},
{
"id": "morning-1773896519933",
"date": "2026-03-19",
+18
View File
@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';
const API_SECRET = process.env.API_SECRET || 'horus-mc-secret-2026';
export function withAuth(handler: (req: NextRequest) => Promise<NextResponse>) {
return async (req: NextRequest) => {
const authHeader = req.headers.get('authorization');
const expectedToken = `Bearer ${API_SECRET}`;
if (authHeader !== expectedToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return handler(req);
};
}
export { API_SECRET };
+976 -2
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -12,6 +12,8 @@
"dependencies": {
"@google/genai": "^1.39.0",
"@google/generative-ai": "^0.24.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@stripe/stripe-js": "^8.7.0",
"@supabase/supabase-js": "^2.97.0",
"@vapi-ai/web": "^2.5.2",
@@ -21,12 +23,15 @@
"next": "^15.5.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-pdf": "^10.4.1",
"stripe": "^17.5.0",
"three": "^0.183.2",
"youtube-transcript-api": "^3.0.6"
},
"devDependencies": {
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@types/three": "^0.183.1",
"autoprefixer": "^10.4.21",
"dotenv": "^17.3.1",
"postcss": "^8.5.6",
+1 -1
View File
File diff suppressed because one or more lines are too long