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 [selectedVectorFile, setSelectedVectorFile] = useState(null); const [attachedFiles, setAttachedFiles] = useState([]); const [attachedPreviews, setAttachedPreviews] = useState([]); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [loadingFiles, setLoadingFiles] = useState(false); const [loadingVectors, setLoadingVectors] = useState(false); const [conversationId, setConversationId] = useState(null); const [creatingConversation, setCreatingConversation] = useState(false); // control para esperar const messagesEndRef = useRef(null); const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); useEffect(scrollToBottom, [messages]); const normalizeInvokeResponse = (resp) => { if (!resp) return null; // cuando invocas funciones, Supabase siempre regresa: // { data: "...string...", error: null, response: {} } 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; } } // si ya viene como objeto if (typeof raw === "object" && raw !== null) return raw; return null; }; // Al abrir: reset o crear conversación useEffect(() => { console.log(context.cont_conversation); console.log(context); if (!open) { // si ya existe una conversación la eliminamos if (conversationId) { deleteConversation(conversationId).catch((e) => console.error(e)); } setMessages([]); setInput(""); setSelectedVectorFile(null); setAttachedFiles([]); setAttachedPreviews([]); setConversationId(null); return; } // inyectar contexto como system message if (context) { setMessages([ { role: "system", content: `Contexto académico:\n${context.section || "—"}\n\nTexto original:\n${context.originalText || "—"}` } ]); } else { setMessages(prev => prev); // no hacer nada si no hay contexto } // crear conversación y esperar a que termine antes de permitir enviar (async () => { await createConversation(); // tras crear podemos también cargar vector stores fetchVectorStores(); })(); }, [open]); // --------- CREATE CONVERSATION (robusto) ---------- const createConversation = async () => { try { setCreatingConversation(true); const { data: { session } } = await supabase.auth.getSession(); const token = session?.access_token; // llamada const resp = await supabase.functions.invoke("conversation-format", { headers: { Authorization: `Bearer ${token}` }, body: { action: "start" , role:"system", content:context.cont_conversation, } }); console.log("createConversation -> raw resp:", resp); // resp puede ser { data: "...json string..." } o { data: { ... } } let parsed = null; if (typeof resp?.data === "string") { try { parsed = JSON.parse(resp.data); } catch (e) { console.warn("No se pudo parsear resp.data como JSON:", e, resp.data); parsed = null; } } else if (typeof resp?.data === "object" && resp.data !== null) { parsed = resp.data; } else { // fallback: quizá la respuesta viene en resp (sin data) parsed = resp; } console.log("createConversation -> parsed payload:", parsed); // buscar el id en varios lugares (robusto) const convId = parsed?.conversationId || parsed?.data?.conversationId || parsed?.data?.id || parsed?.id || parsed?.conversation_id || parsed?.data?.conversation_id; if (!convId) { console.warn("No se encontró conversationId en la respuesta parseada:", parsed); setCreatingConversation(false); return; } setConversationId(convId); console.log("🟢 Conversación creada y guardada:", convId); } catch (err) { console.error("Error creando conversación:", err); } finally { setCreatingConversation(false); } }; // --------- DELETE CONVERSATION (robusto) ---------- const deleteConversation = async (convIdParam) => { try { const convIdToUse = convIdParam ?? conversationId; if (!convIdToUse) return; const { data: { session } } = await supabase.auth.getSession(); const token = session?.access_token; // algunas implementations esperan { action: "end", conversationId }, otras { action: "end", id } const { data, error } = await supabase.functions.invoke("conversation-format", { headers: { Authorization: `Bearer ${token}` }, body: { action: "end", conversationId: convIdToUse } }); console.log("deleteConversation -> response:", data); 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); }); // ---------- SEND MESSAGE (usa conversationId) ---------- const handleConversation = async ({ text }) => { console.log(plan_format); if (!conversationId) { console.warn("No hay conversación activa todavía. conversationId:", conversationId); // si no hay conv, opcionalmente intentar crear una sin que el usuario note return; } try { setLoading(true); const { data: { session } } = await supabase.auth.getSession(); const token = session?.access_token; 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}` }); } } if (selectedVectorFile) { // si el archivo del vector viene sólo con id filesInput.push({ type: "input_file", file_id: selectedVectorFile.id }); } const payload = { action: "message", format: plan_format, conversationId, vectorStoreId: selectedVectorFile?.vector_store_id ?? null, fileIds: selectedVectorFile ? [selectedVectorFile.id] : [], input: [ { role: "user", content: [ { type: "input_text", text }, ...filesInput ] } ] }; const { data: invokeData, error } = await supabase.functions.invoke( "conversation-format", { headers: { Authorization: `Bearer ${token}` }, body: payload } ); if (error) throw error; console.log("handleConversation -> RAW invokeData:", invokeData); const parsed = normalizeInvokeResponse({ data: invokeData }); console.log("handleConversation -> PARSED:", parsed); // 🔥 EXTRACTOR DEFINITIVO let assistantText = null; // 1) directo if (parsed?.data?.output_text) { assistantText = parsed.data.output_text; } // 2) buscar el message 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; } } // 3) fallback assistantText = assistantText || "Sin respuesta del modelo."; setMessages(prev => [ ...prev, { role: "assistant", content: cleanAssistantResponse(assistantText) } ]); setAttachedFiles([]); setAttachedPreviews([]); } 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)]); }; const handleSelectVectorFile = (file) => { setSelectedVectorFile(file); }; // ---------- Send flow ---------- const handleSend = async () => { if (!input.trim() && attachedFiles.length === 0 && !selectedVectorFile) return; // esperar si aún se está creando la conversación if (creatingConversation) { console.log("Esperando a que se cree la conversación..."); // opcional: podrías mostrar un toast; aquí simplemente retornamos return; } if (!conversationId) { console.warn("No hay conversationId — intentaremos crear una ahora."); 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() || (selectedVectorFile ? `Consultar archivo vector: ${selectedVectorFile.filename || selectedVectorFile.id}` : ""); setMessages(prev => [...prev, { role: "user", content: userText }]); setInput(""); await handleConversation({ text: userText }); }; const handleApply = () => { const last = [...messages].reverse().find(m => m.role === "assistant"); if (last && onAccept) { onAccept(last.content); onClose(); } }; const cleanAssistantResponse = (text) => { if (!text) return text; // Frases que quieres eliminar (puedes agregar más) 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 ( {/* Botón siempre visible */} Asistente Inteligente
{/* Left: vectors */}

Vector Stores

{loadingVectors ? (

Cargando vector stores...

) : vectorStores.length === 0 ? (

No hay vector stores.

) : (
    {vectorStores.map((vector) => (
  • loadFilesForVector(vector.id)} className="border cursor-pointer hover:bg-blue-50 p-2 rounded-lg bg-white" > {vector.name || vector.id}

    {vector.description || vector.id}

  • ))}
)}

Archivos del Vector

{loadingFiles ? (

Cargando archivos...

) : selectedVectorFile ? (
Seleccionado: {selectedVectorFile.filename ?? selectedVectorFile.id}
) : (

Selecciona un archivo del vector

)}
    {vectorFiles.map((file) => (
  • handleSelectVectorFile(file)} className={`p-2 rounded-lg cursor-pointer border ${selectedVectorFile?.id === file.id ? "bg-blue-50 border-blue-300" : "bg-white"}`} >
    {file.filename}
    {file.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}
  • ))}
)}