import React, { useEffect, useRef, useState } from "react"; import { supabase } from "@/auth/supabase"; import ReactMarkdown from "react-markdown"; /* ---------- UI Mocks (sin cambios) ---------- */ const Paperclip = (props) => ( ); const Dialog = ({ open, onOpenChange, children }) => open ?
{children}
: null; const DialogContent = ({ className, children }) =>
e.stopPropagation()}>{children}
; const DialogHeader = ({ children }) =>
{children}
; const DialogTitle = ({ className, children }) =>

{children}

; const Button = ({ onClick, disabled, className, variant, children }) => ( ); const Card = ({ className, children }) =>
{children}
; const CardContent = ({ className, children }) =>
{children}
; const ScrollArea = ({ className, children }) =>
{children}
; /* ------------- COMPONENT ------------- */ export default function AIChatModal({ open, onClose, context, onAccept, plan_format }) { const [vectorStores, setVectorStores] = useState([]); const [vectorFiles, setVectorFiles] = useState([]); const [selectedVector, setSelectedVector] = useState(null); const [selectedFiles, setSelectedFiles] = useState([]); const [attachedFiles, setAttachedFiles] = useState([]); const [attachedPreviews, setAttachedPreviews] = useState([]); // chat const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); // loading states const [loading, setLoading] = useState(false); const [loadingFiles, setLoadingFiles] = useState(false); const [loadingVectors, setLoadingVectors] = useState(false); // conversation control const [conversationId, setConversationId] = useState(null); const [creatingConversation, setCreatingConversation] = useState(false); const messagesEndRef = useRef(null); const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); useEffect(scrollToBottom, [messages]); const normalizeInvokeResponse = (resp) => { if (!resp) return null; const raw = resp.data; if (typeof raw === "string") { try { return JSON.parse(raw); } catch (e) { console.warn("❗ No se pudo parsear resp.data:", raw); return null; } } if (typeof raw === "object" && raw !== null) return raw; return null; }; // Al abrir: reset o crear conversación useEffect(() => { if (!open) { if (conversationId) { deleteConversation(conversationId).catch((e) => console.error(e)); } setMessages([]); setInput(""); setSelectedFiles([]); setAttachedFiles([]); setAttachedPreviews([]); setConversationId(null); setSelectedVector(null); setVectorFiles([]); return; } if (context) { setMessages([ { role: "system", //content: `Contexto académico:\n${context.section || "—"}\n\nTexto original:\n${context.originalText || "—"}` content: `Contexto académico:\n${context.section || "—"}\n\nTexto original:\n${context.originalText || "—"}` } ]); } (async () => { await createConversation(); fetchVectorStores(); })(); }, [open]); // ---------- CREATE CONVERSATION ---------- const createConversation = async () => { try { setCreatingConversation(true); const { data: { session } } = await supabase.auth.getSession(); const token = session?.access_token; const resp = await supabase.functions.invoke("modal-conversation", { headers: { Authorization: `Bearer ${token}` }, body: { action: "start", role: "system", content: context?.cont_conversation ?? "" } }); let parsed = null; if (typeof resp?.data === "string") { try { parsed = JSON.parse(resp.data); } catch (e) { parsed = null; } } else if (typeof resp?.data === "object" && resp.data !== null) parsed = resp.data; else parsed = resp; const convId = parsed?.conversationId || parsed?.data?.conversationId || parsed?.data?.id || parsed?.id || parsed?.conversation_id || parsed?.data?.conversation_id; if (!convId) { setCreatingConversation(false); return; } setConversationId(convId); } catch (err) { console.error("Error creando conversación:", err); } finally { setCreatingConversation(false); } }; // ---------- DELETE CONVERSATION ---------- const deleteConversation = async (convIdParam) => { try { const convIdToUse = convIdParam ?? conversationId; if (!convIdToUse) return; const { data: { session } } = await supabase.auth.getSession(); const token = session?.access_token; await supabase.functions.invoke("modal-conversation", { headers: { Authorization: `Bearer ${token}` }, body: { action: "end", conversationId: convIdToUse } }); setConversationId(null); } catch (err) { console.error("Error eliminando conversación:", err); } }; // ---------- CONVERT FILE TO BASE64 ---------- const fileToBase64 = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = (e) => reject(e); reader.onload = () => resolve(reader.result.split(",")[1]); reader.readAsDataURL(file); }); // ---------- HANDLE CONVERSATION (envío) ---------- const handleConversation = async ({ text }) => { let contextText = ""; if (context?.originalText) contextText += `CONTEXTO DEL CAMPO:\n${context.originalText}\n`; if (!conversationId) { console.warn("No hay conversación activa todavía. conversationId:", conversationId); return; } try { setLoading(true); const { data: { session } } = await supabase.auth.getSession(); const token = session?.access_token; // archivos adjuntos (locales) -> base64 let filesInput = []; if (attachedFiles.length > 0) { for (const file of attachedFiles) { const base64 = await fileToBase64(file); filesInput.push({ type: "input_file", filename: file.name, file_data: `data:${file.type};base64,${base64}` }); } } // archivos seleccionados del vector (por id) if (selectedFiles.length > 0) { const filesFromVectors = selectedFiles.map(f => ({ type: "input_file", file_id: f.id })); filesInput = [...filesInput, ...filesFromVectors]; } const promptFinal = `${contextText}\nPREGUNTA DEL USUARIO:\n${text}`; const payload = { action: "message", format: plan_format, conversationId, vectorStoreId: selectedVector ?? null, fileIds: selectedFiles.length ? selectedFiles.map(f => f.id) : [], input: [ { role: "user", content: [ { type: "input_text", text: promptFinal }, ...filesInput ] } ] }; const { data: invokeData, error } = await supabase.functions.invoke( "modal-conversation", { headers: { Authorization: `Bearer ${token}` }, body: payload } ); if (error) throw error; const parsed = normalizeInvokeResponse({ data: invokeData }); // Extraer texto del assistant (robusto) let assistantText = null; if (parsed?.data?.output_text) assistantText = parsed.data.output_text; if (!assistantText && Array.isArray(parsed?.data?.output)) { const msgBlock = parsed.data.output.find(o => o.type === "message"); if (msgBlock?.content?.[0]?.text) assistantText = msgBlock.content[0].text; } assistantText = assistantText || "Sin respuesta del modelo."; setMessages(prev => [...prev, { role: "assistant", content: cleanAssistantResponse(assistantText) }]); // limpiar attachments locales (pero mantener seleccionados del vector si quieres — aquí los limpiamos) setAttachedFiles([]); setAttachedPreviews([]); // si quieres mantener los selectedFiles tras el envío, comenta la siguiente línea: setSelectedFiles([]); } catch (err) { console.error("Error en handleConversation:", err); setMessages(prev => [...prev, { role: "assistant", content: "Ocurrió un error al procesar tu mensaje." }]); } finally { setLoading(false); } }; // ---------- VECTORES ---------- const fetchVectorStores = async () => { try { setLoadingVectors(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 : (data?.data ?? [])); } catch (err) { console.error("Error loading vector stores:", err); setVectorStores([]); } finally { setLoadingVectors(false); } }; useEffect(() => { if (open) fetchVectorStores(); }, [open]); 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 : (data?.data ?? [])); } catch (err) { console.error("Error loading vector files:", err); setVectorFiles([]); } finally { setLoadingFiles(false); } }; // ---------- UI helpers ---------- const handleAttach = (e) => { const files = Array.from(e.target.files); if (!files.length) return; setAttachedFiles(prev => [...prev, ...files]); setAttachedPreviews(prev => [...prev, ...files.map(f => f.name)]); }; // Al hacer click en un vector: expandir (solo uno a la vez) y cargar sus archivos const handleVectorClick = async (vector) => { if (selectedVector === vector.id) { // colapsar setSelectedVector(null); setVectorFiles([]); setSelectedFiles([]); return; } setSelectedVector(vector.id); setSelectedFiles([]); await loadFilesForVector(vector.id); }; // Toggle selección de archivo (checkbox) const toggleFileSelection = (file) => { if (selectedFiles.some(f => f.id === file.id)) { setSelectedFiles(prev => prev.filter(f => f.id !== file.id)); } else { setSelectedFiles(prev => [...prev, file]); } }; const removeSelectedFile = (fileId) => { setSelectedFiles(prev => prev.filter(f => f.id !== fileId)); }; // ---------- Send flow ---------- const handleSend = async () => { // no permitir enviar si no hay nada if (!input.trim() && attachedFiles.length === 0 && selectedFiles.length === 0) return; if (creatingConversation) { // no bloqueo visible aquí por diseño; simplemente ignoramos el envío si aún creando return; } if (!conversationId) { await createConversation(); if (!conversationId) { setMessages(prev => [...prev, { role: "assistant", content: "No se pudo crear la conversación. Intenta de nuevo." }]); return; } } const userText = input.trim() || (selectedFiles.length ? `Consultar ${selectedFiles.length} archivo(s) del repositorio` : ""); setMessages(prev => [...prev, { role: "user", content: userText }]); setInput(""); await handleConversation({ text: userText }); }; function cleanAIResponse(text) { if (!text) return text; let cleaned = text; // ------------------------- // 1. Eliminar emojis // ------------------------- cleaned = cleaned.replace(/[\p{Emoji}\uFE0F]/gu, ""); // ------------------------- // 2. Eliminar separadores tipo --- // ------------------------- cleaned = cleaned.replace(/^---+$/gm, ""); // ------------------------- // 3. Eliminar saludos y frases meta // ------------------------- const metaPatterns = [ /^hola[!¡., ]*/i, /^buen(os|as) (días|tardes|noches)[!¡., ]*/i, /estoy aquí para ayudarte[.! ]*/gi, /aquí tienes[,:]*/gi, /claro[,:]*/gi, /como pediste[,:]*/gi, /como solicitaste[,:]*/gi, /el texto íntegro que compartiste.*$/gi, /te lo dejo a continuación.*$/gi, /¿te gustaría.*$/gi, /¿en qué más puedo.*$/gi, /si necesitas algo más.*$/gi, /con gusto.*$/gi, ]; metaPatterns.forEach(p => { cleaned = cleaned.replace(p, "").trim(); }); // ------------------------- // 4. Extraer solo contenido útil // ------------------------- const startMarker = "CONTEXTO DEL CAMPO"; const startIndex = cleaned.indexOf(startMarker); if (startIndex !== -1) { cleaned = cleaned.substring(startIndex).trim(); } // ------------------------- // 5. Eliminar líneas vacías múltiples // ------------------------- cleaned = cleaned.replace(/\n{2,}/g, "\n\n"); // ------------------------- // 6. Quitar numeraciones de cortesía (opcional) // Ejemplo: “1. ” al inicio de líneas // ------------------------- cleaned = cleaned.replace(/^\s*\d+\.\s+/gm, ""); return cleaned.trim(); } const handleApply = () => { const last = [...messages].reverse().find(m => m.role === "assistant"); if (last && onAccept) { const cleaned = cleanAIResponse(last.content); onAccept(cleaned); onClose(); } }; const cleanAssistantResponse = (text) => { if (!text) return text; const patterns = [/^claro[, ]*/i, /^por supuesto[, ]*/i, /^aquí tienes[, ]*/i, /^con gusto[, ]*/i, /^hola[, ]*/i, /^perfecto[, ]*/i, /^entendido[, ]*/i, /^muy bien[, ]*/i, /^ok[, ]*/i]; let cleaned = text.trim(); for (const p of patterns) cleaned = cleaned.replace(p, "").trim(); return cleaned; }; return ( Asistente Inteligente
{/* Left: vectors */}

Repositorio de archivos

{loadingVectors ? (

Cargando Repositorio de archivos...

) : vectorStores.length === 0 ? (

No hay Repositorio de archivos.

) : (
{vectorStores.map((vector) => (
{/* VECTOR */}
handleVectorClick(vector)} className={`p-3 rounded-lg border cursor-pointer transition flex items-center justify-between ${selectedVector === vector.id ? "bg-blue-50 border-blue-400 shadow" : "bg-white border-gray-300"}`} >
{vector.name || vector.id}

{vector.description || ""}

{selectedVector === vector.id ? "▼" : "▶"}
{/* ARCHIVOS cuando está expandido */} {selectedVector === vector.id && (
{loadingFiles ? (

Cargando archivos...

) : vectorFiles.length === 0 ? (

No hay archivos en este repositorio

) : ( vectorFiles.map((file) => ( )) )}
)}
))}
)}
{/* Resumen de archivos seleccionados (de vectores) */}

Archivos seleccionados

{selectedFiles.length === 0 ? (

No has seleccionado archivos del repositorio

) : (
    {selectedFiles.map((f) => (
  • {f.filename ?? f.name ?? f.id}
    {f.id}
    {/* optionally show vector id */}
  • ))}
)}
{/*
*/}
{/* Right: Chat */}

Chat con IA

{/* CONTENEDOR SCROLL DE LOS MENSAJES */}
{messages.length === 0 ? (

Inicia una conversación...

) : ( messages.map((m, i) => (
{m.role === "user" ? "Tú:" : m.role === "assistant" ? "IA:" : "Sistema:"}{" "} {m.content}
)) )} {loading && (
La IA está respondiendo...
)}
{attachedPreviews.length > 0 && (
    {attachedPreviews.map((name, i) => (
  • 📄 {name}
  • ))}
)}