Add Horus Chat to MC - connects to AI agent
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
const MESSAGE_FILE = "/tmp/horus-mc-messages.json";
|
||||||
|
const TELEGRAM_CHAT_ID = "382315644"; // Haitham's Telegram ID
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string;
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
timestamp: number;
|
||||||
|
processed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessages(): Message[] {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(MESSAGE_FILE)) {
|
||||||
|
return JSON.parse(fs.readFileSync(MESSAGE_FILE, "utf-8"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error reading messages:", e);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMessages(messages: Message[]) {
|
||||||
|
fs.writeFileSync(MESSAGE_FILE, JSON.stringify(messages, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendToTelegram(text: string) {
|
||||||
|
try {
|
||||||
|
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
|
if (!token) {
|
||||||
|
console.log("No Telegram token, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: TELEGRAM_CHAT_ID,
|
||||||
|
text: `💬 MC Chat:\n${text}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Telegram error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { message, locale = "es" } = body;
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return NextResponse.json({ error: "Message required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = getMessages();
|
||||||
|
|
||||||
|
// Add user message
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: `msg_${Date.now()}`,
|
||||||
|
role: "user",
|
||||||
|
content: message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
processed: false,
|
||||||
|
};
|
||||||
|
messages.push(userMessage);
|
||||||
|
saveMessages(messages);
|
||||||
|
|
||||||
|
// Forward to Telegram (where Horus is listening)
|
||||||
|
await sendToTelegram(message);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: userMessage.id,
|
||||||
|
status: "sent",
|
||||||
|
message: "Message sent to Horus"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const lastId = searchParams.get("after");
|
||||||
|
|
||||||
|
const messages = getMessages();
|
||||||
|
|
||||||
|
// Get messages after lastId
|
||||||
|
let filtered = messages;
|
||||||
|
if (lastId) {
|
||||||
|
const idx = messages.findIndex(m => m.id === lastId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
filtered = messages.slice(idx + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return only assistant messages that haven't been fetched
|
||||||
|
const response = filtered.filter(m => m.role === "assistant" && !m.processed);
|
||||||
|
|
||||||
|
// Mark as processed
|
||||||
|
if (response.length > 0) {
|
||||||
|
const processedIds = new Set(response.map(r => r.id));
|
||||||
|
const updated = messages.map(m =>
|
||||||
|
processedIds.has(m.id) ? { ...m, processed: true } : m
|
||||||
|
);
|
||||||
|
saveMessages(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ messages: response });
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string;
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VoiceChat() {
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [messages, setMessages] = useState<Message[]>([
|
||||||
|
{
|
||||||
|
id: "welcome",
|
||||||
|
role: "assistant",
|
||||||
|
content: "👁️ ¡Hola! Soy Horus. Chatea conmigo aquí o en Telegram. ¿En qué puedo ayudarte?",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!input.trim() || isProcessing) return;
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: `user_${Date.now()}`,
|
||||||
|
role: "user",
|
||||||
|
content: input,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, userMessage]);
|
||||||
|
setInput("");
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send to SiteMente AI agent
|
||||||
|
const res = await fetch("/api/chat/agent", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: userMessage.content,
|
||||||
|
locale: "es"
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const botMessage: Message = {
|
||||||
|
id: `bot_${Date.now()}`,
|
||||||
|
role: "assistant",
|
||||||
|
content: data.response || "Entendido.",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, botMessage]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Chat error:", e);
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
id: `error_${Date.now()}`,
|
||||||
|
role: "assistant",
|
||||||
|
content: "Error de conexión. Prueba en Telegram.",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}]);
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-white/5 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">💬</span>
|
||||||
|
<span className="font-semibold">Horus Chat</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isProcessing && (
|
||||||
|
<span className="text-xs text-brand-pink animate-pulse">Escribiendo...</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="p-4 space-y-3 min-h-[280px] max-h-[400px] overflow-y-auto">
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<motion.div
|
||||||
|
key={msg.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[85%] px-4 py-2 rounded-2xl text-sm ${
|
||||||
|
msg.role === "user"
|
||||||
|
? "bg-brand-pink text-white rounded-br-md"
|
||||||
|
: "bg-white/10 text-white/90 rounded-bl-md"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.content}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="p-3 border-t border-white/10">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="Escribe un mensaje..."
|
||||||
|
className="flex-1 bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink"
|
||||||
|
disabled={isProcessing}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!input.trim() || isProcessing}
|
||||||
|
className="px-4 py-2 bg-brand-pink hover:bg-[#ff7bc0] disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-sm font-medium transition"
|
||||||
|
>
|
||||||
|
Enviar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-white/40 mt-2 text-center">
|
||||||
|
O chatea directamente en Telegram
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useMissionControl } from "@/lib/mission-control/store";
|
import { useMissionControl } from "@/lib/mission-control/store";
|
||||||
import { TaskStatus } from "@/lib/mission-control/types";
|
import { TaskStatus } from "@/lib/mission-control/types";
|
||||||
import VoiceChat from "./VoiceChat";
|
import VoiceChat from "./HorusChat";
|
||||||
import MondayBoard from "./MondayBoard";
|
import MondayBoard from "./MondayBoard";
|
||||||
import { TaskCardsPanel } from "./TaskCardsPanel";
|
import { TaskCardsPanel } from "./TaskCardsPanel";
|
||||||
import { TaskHistoryPanel } from "./TaskHistoryPanel";
|
import { TaskHistoryPanel } from "./TaskHistoryPanel";
|
||||||
|
|||||||
Reference in New Issue
Block a user