Version estable conversacion normal

This commit is contained in:
2025-11-18 15:17:11 -06:00
parent 8c890d76e0
commit f7a29ad510
4 changed files with 379 additions and 373 deletions

View File

@@ -0,0 +1,375 @@
import React, { useEffect, useRef, useState } from "react";
import { supabase } from "@/auth/supabase";
// ---------------- UI MOCKS ---------------- //
// Puedes reemplazarlos por tus propios componentes UI
const Paperclip = (props) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
</svg>
);
const Dialog = ({ open, onOpenChange, children }) =>
open ? <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onOpenChange}>{children}</div> : null;
const DialogContent = ({ className, children }) =>
<div className={`bg-white rounded-xl shadow-2xl transform transition-all max-w-6xl w-[95vw] h-[85vh] p-6 flex flex-col ${className}`} onClick={(e) => e.stopPropagation()}>
{children}
</div>;
const DialogHeader = ({ children }) => <div className="pb-4 border-b border-gray-200">{children}</div>;
const DialogTitle = ({ className, children }) => <h2 className={`text-xl font-bold ${className}`}>{children}</h2>;
const Button = ({ onClick, disabled, className, variant, children }) => (
<button
onClick={onClick}
disabled={disabled}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors
${variant === "outline" ? "bg-white border border-gray-300 text-gray-700 hover:bg-gray-50" : "bg-blue-600 text-white hover:bg-blue-700"}
${disabled ? "opacity-50 cursor-not-allowed" : ""} ${className}`}
>
{children}
</button>
);
const Card = ({ className, children }) => <div className={`bg-white rounded-2xl shadow-md ${className}`}>{children}</div>;
const CardContent = ({ className, children }) => <div className={`p-4 ${className}`}>{children}</div>;
const ScrollArea = ({ className, children }) => <div className={`overflow-y-auto ${className}`}>{children}</div>;
// ---------------- COMPONENTE ---------------- //
export default function AIChatModal({ open, onClose, context, onAccept }) {
const [vectorStores, setVectorStores] = useState([]);
const [vectorFiles, setVectorFiles] = useState([]);
const [attachedFile, setAttachedFile] = useState(null);
const [attachedPreview, setAttachedPreview] = useState(null);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [loadingFiles, setLoadingFiles] = useState(false);
const [selectedVector, setSelectedVector] = useState(null);
const messagesEndRef = useRef(null);
const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
useEffect(scrollToBottom, [messages]);
// ------------------------------------
// Reset al abrir o cerrar modal
// ------------------------------------
useEffect(() => {
if (!open) {
setMessages([]);
setInput("");
setAttachedFile(null);
setAttachedPreview(null);
setVectorStores([]);
setVectorFiles([]);
setSelectedVector(null);
return;
}
if (context) {
setMessages([
{
role: "system",
content: `Contexto: ${context.section}\nTexto original:\n${context.originalText || "—"}`,
},
]);
}
}, [open, context]);
// ------------------------------------
// Cargar vector stores
// ------------------------------------
useEffect(() => {
if (!open) return;
const fetchVectorStores = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;
const { data, error } = await supabase.functions.invoke(
"files-and-vector-stores-api",
{
headers: { Authorization: `Bearer ${token}` },
body: { module: "vectorStores", action: "list" },
}
);
if (error) throw error;
setVectorStores(Array.isArray(data) ? data : []);
} catch (err) {
console.error("Error al obtener vector stores:", err);
setVectorStores([]);
} finally {
setLoading(false);
}
};
fetchVectorStores();
}, [open]);
// ------------------------------------
// Cargar archivos del vector seleccionado
// ------------------------------------
const loadFilesForVector = async (vectorStoreId) => {
try {
setLoadingFiles(true);
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;
const { data, error } = await supabase.functions.invoke(
"files-and-vector-stores-api",
{
headers: { Authorization: `Bearer ${token}` },
body: {
module: "vectorStoreFiles",
action: "list",
params: { vector_store_id: vectorStoreId },
},
}
);
if (error) throw error;
setVectorFiles(Array.isArray(data) ? data : []);
} catch (err) {
console.error("Error al obtener archivos del vector store:", err);
setVectorFiles([]);
} finally {
setLoadingFiles(false);
}
};
// ------------------------------------
// Adjuntar archivo
// ------------------------------------
const handleAttach = (e) => {
const file = e.target.files?.[0];
if (!file) return;
setAttachedFile(file);
setAttachedPreview(file.name);
};
// ------------------------------------
// handleSend — versión final para Supabase Edge Function
// ------------------------------------
const handleSend = async () => {
if (!input.trim() && !attachedFile) return;
// Construir texto del mensaje del usuario
const userMessage = input.trim()
? input.trim()
: attachedFile
? `Consulta sobre archivo: ${attachedFile.name}`
: "";
// Agregar mensaje al chat
setMessages(prev => [...prev, { role: "user", content: userMessage }]);
setInput("");
setLoading(true);
try {
const formData = new FormData();
const fullPrompt = `
${context?.section ? `Sección: ${context.section}` : ""}
${context?.fieldKey ? `Campo: ${context.fieldKey}` : ""}
Texto original:
${context?.originalText || "Sin texto original"}
Solicitud del usuario:
${userMessage}
Responde con una versión mejorada en texto directo, sin explicaciones.
`.trim();
formData.append("prompt", fullPrompt);
if (attachedFile) formData.append("file", attachedFile);
const { data, error } = await supabase.functions.invoke(
"simple-chat",
{ body: formData }
);
if (error) throw error;
// Respuesta de la IA
setMessages(prev => [
...prev,
{ role: "assistant", content: data?.text || "Sin respuesta del modelo." }
]);
} catch (err) {
console.error("Error enviando mensaje:", err);
setMessages(prev => [...prev, { role: "assistant", content: "Ocurrió un error al conectar con la IA." }]);
}
setLoading(false);
setAttachedFile(null);
setAttachedPreview(null);
};
// ------------------------------------
// UI
// ------------------------------------
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Asistente Inteligente</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto pt-4">
<div className="flex gap-6 min-h-full">
{/* LEFT: VECTOR STORES */}
<Card className="w-1/3 max-w-sm flex flex-col bg-muted/20 border border-gray-200">
<CardContent className="flex flex-col flex-1">
<h3 className="font-semibold text-sm mb-3">Vector Stores</h3>
<ScrollArea className="flex-1">
{loading ? (
<p className="text-center text-gray-400">Cargando...</p>
) : vectorStores.length === 0 ? (
<p className="text-center text-gray-400">No hay vector stores</p>
) : (
<ul className="space-y-2">
{vectorStores.map(store => (
<li
key={store.id}
onClick={() => {
setSelectedVector(store);
loadFilesForVector(store.id);
}}
className={`border p-2 rounded-lg cursor-pointer
${selectedVector?.id === store.id ? "bg-blue-100" : "bg-white"}`}
>
<strong>{store.name || store.id}</strong>
<p className="text-xs text-gray-500 truncate">{store.description}</p>
</li>
))}
</ul>
)}
</ScrollArea>
<h4 className="mt-4 font-semibold text-sm">Archivos</h4>
<ScrollArea className="mt-2 max-h-40">
{loadingFiles ? (
<p className="text-gray-400 text-sm">Cargando archivos...</p>
) : vectorFiles.length === 0 ? (
<p className="text-gray-400 text-sm">No hay archivos</p>
) : (
<ul className="space-y-1">
{vectorFiles.map(f => (
<li key={f.id} className="border bg-white p-2 rounded-lg text-sm">
{f.id}
</li>
))}
</ul>
)}
</ScrollArea>
</CardContent>
</Card>
{/* RIGHT: CHAT */}
<Card className="flex-1 flex flex-col border border-gray-200">
<CardContent className="flex flex-col flex-1">
<h3 className="font-semibold text-sm mb-3">Chat</h3>
<div className="flex-1 overflow-y-auto border p-3 rounded-lg bg-gray-50 space-y-3">
{messages.length === 0 ? (
<p className="text-gray-400 text-center text-sm">Inicia la conversación</p>
) : (
messages.map((msg, idx) => (
<div
key={idx}
className={`p-3 rounded-xl max-w-[85%] shadow-sm whitespace-pre-wrap
${msg.role === "user"
? "bg-blue-50 text-blue-800 ml-auto"
: "bg-white text-gray-700 border border-gray-200 mr-auto"
}`}
>
<strong>{msg.role === "user" ? "Tú:" : "IA:"}</strong>
<p>{msg.content}</p>
</div>
))
)}
{loading && (
<div className="p-3 bg-white border rounded-xl max-w-fit">
<span className="text-gray-600 text-sm">La IA está respondiendo...</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
{attachedPreview && (
<div className="flex items-center mt-2 p-2 border rounded-lg bg-gray-100 text-sm">
<Paperclip className="w-4 h-4 text-blue-500" />
<span className="ml-2">{attachedPreview}</span>
<button
onClick={() => { setAttachedFile(null); setAttachedPreview(null); }}
className="ml-auto text-red-500 text-xs"
>
Quitar
</button>
</div>
)}
{/* Input */}
<div className="flex gap-2 mt-4">
<label className="cursor-pointer text-gray-600 hover:text-blue-600">
<Paperclip className="w-5 h-5" />
<input type="file" className="hidden" onChange={handleAttach} />
</label>
<textarea
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Escribe tu pregunta..."
className="flex-1 border rounded-lg p-3 text-sm"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<Button
onClick={handleSend}
disabled={loading || (!input.trim() && !attachedFile)}
>
Enviar
</Button>
<Button
onClick={() => {
const last = messages[messages.length - 1];
if (last?.role === "assistant") {
onAccept(last.content);
onClose();
}
}}
disabled={!messages.some(m => m.role === "assistant")}
>
Aplicar
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</DialogContent>
</Dialog>
);
}