From 45af56d9cfd9239c75aa4fb38ab2aa147fac62bf Mon Sep 17 00:00:00 2001 From: Horus AI Date: Mon, 23 Mar 2026 16:30:44 +0100 Subject: [PATCH] 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 --- app/api/anvevoice-stats/route.ts | 30 + app/api/briefs/eod/[date]/route.ts | 30 + app/api/briefs/morning/[date]/route.ts | 30 + app/api/chat/route.ts | 51 +- app/api/claude/spawn/route.ts | 25 + app/api/claw3d-proxy/route.ts | 16 + app/api/memory/[date]/route.ts | 23 + app/api/memory/append/route.ts | 47 + app/api/memory/list/route.ts | 141 +++ app/api/morning/route.ts | 15 +- app/api/tts-proxy/route.ts | 31 + app/components/CleopatraVoiceWidget.tsx | 357 +++++++ app/demos/cleopatra-voice/page.tsx | 82 ++ app/mission-control/claude-chat/page.tsx | 263 +++++ app/mission-control/claude/page.tsx | 283 +++++ app/mission-control/memory/page.tsx | 520 ++++++++++ app/mission-control/office/page.tsx | 24 + app/mission-control/pdf-viewer/page.tsx | 154 +++ app/mission-control/resume-upload/page.tsx | 81 ++ app/mission-control/resume/page.tsx | 519 ++++++++++ app/mission-control/temple/page.tsx | 5 + app/temple-3d/page.tsx | 518 ++++++++++ .../MissionControlDashboard.tsx | 840 +++------------ .../temple-of-ai/TempleOfAIDashboard.tsx | 546 ++++++++++ data/eod_briefs.json | 44 + data/morning_briefs.json | 129 +++ lib/api-auth.ts | 18 + package-lock.json | 978 +++++++++++++++++- package.json | 5 + tsconfig.tsbuildinfo | 2 +- 30 files changed, 5092 insertions(+), 715 deletions(-) create mode 100644 app/api/anvevoice-stats/route.ts create mode 100644 app/api/briefs/eod/[date]/route.ts create mode 100644 app/api/briefs/morning/[date]/route.ts create mode 100644 app/api/claude/spawn/route.ts create mode 100644 app/api/claw3d-proxy/route.ts create mode 100644 app/api/memory/[date]/route.ts create mode 100644 app/api/memory/append/route.ts create mode 100644 app/api/memory/list/route.ts create mode 100644 app/api/tts-proxy/route.ts create mode 100644 app/components/CleopatraVoiceWidget.tsx create mode 100644 app/demos/cleopatra-voice/page.tsx create mode 100644 app/mission-control/claude-chat/page.tsx create mode 100644 app/mission-control/claude/page.tsx create mode 100644 app/mission-control/memory/page.tsx create mode 100644 app/mission-control/office/page.tsx create mode 100644 app/mission-control/pdf-viewer/page.tsx create mode 100644 app/mission-control/resume-upload/page.tsx create mode 100644 app/mission-control/resume/page.tsx create mode 100644 app/mission-control/temple/page.tsx create mode 100644 app/temple-3d/page.tsx create mode 100644 components/temple-of-ai/TempleOfAIDashboard.tsx create mode 100644 lib/api-auth.ts diff --git a/app/api/anvevoice-stats/route.ts b/app/api/anvevoice-stats/route.ts new file mode 100644 index 0000000..39eca6f --- /dev/null +++ b/app/api/anvevoice-stats/route.ts @@ -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 }); + } +} diff --git a/app/api/briefs/eod/[date]/route.ts b/app/api/briefs/eod/[date]/route.ts new file mode 100644 index 0000000..cabd291 --- /dev/null +++ b/app/api/briefs/eod/[date]/route.ts @@ -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 }); + } +} diff --git a/app/api/briefs/morning/[date]/route.ts b/app/api/briefs/morning/[date]/route.ts new file mode 100644 index 0000000..c37a030 --- /dev/null +++ b/app/api/briefs/morning/[date]/route.ts @@ -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 }); + } +} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 8758425..1bd72c3 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -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 }); } } diff --git a/app/api/claude/spawn/route.ts b/app/api/claude/spawn/route.ts new file mode 100644 index 0000000..61b79a1 --- /dev/null +++ b/app/api/claude/spawn/route.ts @@ -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 }); + } +} diff --git a/app/api/claw3d-proxy/route.ts b/app/api/claw3d-proxy/route.ts new file mode 100644 index 0000000..5c965b2 --- /dev/null +++ b/app/api/claw3d-proxy/route.ts @@ -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 }); + } +} diff --git a/app/api/memory/[date]/route.ts b/app/api/memory/[date]/route.ts new file mode 100644 index 0000000..2bb25de --- /dev/null +++ b/app/api/memory/[date]/route.ts @@ -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 }); + } +} diff --git a/app/api/memory/append/route.ts b/app/api/memory/append/route.ts new file mode 100644 index 0000000..7ec33b8 --- /dev/null +++ b/app/api/memory/append/route.ts @@ -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 }); + } +} diff --git a/app/api/memory/list/route.ts b/app/api/memory/list/route.ts new file mode 100644 index 0000000..2347e7e --- /dev/null +++ b/app/api/memory/list/route.ts @@ -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 }); + } +} diff --git a/app/api/morning/route.ts b/app/api/morning/route.ts index dd2b367..6cdb517 100644 --- a/app/api/morning/route.ts +++ b/app/api/morning/route.ts @@ -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(); diff --git a/app/api/tts-proxy/route.ts b/app/api/tts-proxy/route.ts new file mode 100644 index 0000000..857d060 --- /dev/null +++ b/app/api/tts-proxy/route.ts @@ -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 }); + } +} diff --git a/app/components/CleopatraVoiceWidget.tsx b/app/components/CleopatraVoiceWidget.tsx new file mode 100644 index 0000000..62616fe --- /dev/null +++ b/app/components/CleopatraVoiceWidget.tsx @@ -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("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(null); + const audioRef = useRef(null); + const streamRef = useRef(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 ( +
+ {/* Language Toggle */} +
+ +
+ + {/* Header */} +
+
👑
+

{t.title}

+

{t.subtitle}

+
+ + {/* Status */} +
+
+ {isListening ? t.status.listening : + isSpeaking ? t.status.speaking : + isThinking ? t.status.thinking : + t.status.ready} +
+
+ + {/* Visualizer */} +
+ {[...Array(16)].map((_, i) => ( +
+ ))} +
+ + {/* Transcript */} + {transcript && ( +
+

{t.youSaid}

+

{transcript}

+
+ )} + + {/* Reply */} + {lastReply && ( +
+

{t.cleopatra}

+

{lastReply}

+
+ )} + + {/* Text Input */} +
+
+ 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" + /> + +
+
+ + {/* Controls */} +
+ + + {(isSpeaking || isThinking) && ( + + )} +
+ + {/* Hint */} +

+ {isListening ? t.speakingHint : t.micHint} +

+
+ ); +} diff --git a/app/demos/cleopatra-voice/page.tsx b/app/demos/cleopatra-voice/page.tsx new file mode 100644 index 0000000..6be1b64 --- /dev/null +++ b/app/demos/cleopatra-voice/page.tsx @@ -0,0 +1,82 @@ +import CleopatraVoiceWidget from "@/app/components/CleopatraVoiceWidget"; + +export default function CleopatraVoicePage() { + return ( +
+
+
+

+ 👑 Cleopatra Voice AI +

+

+ Premium Voice Agent - Built for Human-Like Conversations +

+
+ +
+
+

🎤 Características

+
    +
  • + + Respuesta menos de 1 segundo +
  • +
  • + + Detección de voz (VAD) +
  • +
  • + + Palabras de relleno naturales +
  • +
  • + + Manejo de interrupciones +
  • +
  • + + Multilingüe +
  • +
+
+ +
+

🧠 Tecnología

+
    +
  • + + STT: Whisper (preciso) +
  • +
  • + + LLM: MiniMax M2.7 +
  • +
  • + + TTS: ElevenLabs (75ms) +
  • +
  • + + Streaming de audio +
  • +
  • + + Personalidad Cleopatra +
  • +
+
+
+ +
+ +
+ +
+

+ 🚀 Construido por Horus + Cleopatra | HostPioneers +

+
+
+
+ ); +} diff --git a/app/mission-control/claude-chat/page.tsx b/app/mission-control/claude-chat/page.tsx new file mode 100644 index 0000000..171b3e3 --- /dev/null +++ b/app/mission-control/claude-chat/page.tsx @@ -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([]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const [apiKey, setApiKey] = useState(""); + const [showSettings, setShowSettings] = useState(false); + const messagesEndRef = useRef(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 ( +
+ {/* Header */} +
+
+

+ + Claude Code Chat +

+

Direct chat with Claude AI

+
+
+ + +
+
+ + {/* Messages */} +
+
+ {messages.length === 0 && !loading && ( +
+
💬
+

Chat with Claude

+

+ Send a message to start a conversation. Claude will respond directly. +

+ {!apiKey && ( + + )} +
+ )} + + {messages.map((msg, i) => ( +
+
+
+ {msg.content} +
+
+ {msg.timestamp.toLocaleTimeString()} +
+
+
+ ))} + + {loading && ( +
+
+
+
+ Claude is thinking... +
+
+
+ )} + +
+
+
+ + {/* Input */} +
+
+
+