From 4ec2c2d5332a03de9d3a9ae75b791aeafb99a21d Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Mon, 24 Nov 2025 16:17:49 -0600 Subject: [PATCH] Se agrega funcionalidades de crear conversacion, archivos y vectores ademas de MCP --- .env.local2 | 4 + src/components/ai/AIChatModal.jsx | 619 +++++++++++++------- src/components/planes/academic-sections.tsx | 2 +- 3 files changed, 401 insertions(+), 224 deletions(-) create mode 100644 .env.local2 diff --git a/.env.local2 b/.env.local2 new file mode 100644 index 0000000..08a9b4b --- /dev/null +++ b/.env.local2 @@ -0,0 +1,4 @@ +VITE_SUPABASE_URL=http://127.0.0.1:54321 +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEzNzg2MzIsImV4cCI6MjA1Njk1NDYzMn0.g1mBmsw-i6F6e-tPv5gWkHZacyPM2Y9X0fiKVYmVYKE +#VITE_BACK_ORIGIN=http://localhost:3001 +VITE_BACK_ORIGIN=http://localhost:3001 diff --git a/src/components/ai/AIChatModal.jsx b/src/components/ai/AIChatModal.jsx index b97769b..98077cd 100644 --- a/src/components/ai/AIChatModal.jsx +++ b/src/components/ai/AIChatModal.jsx @@ -1,45 +1,41 @@ import React, { useEffect, useRef, useState } from "react"; import { supabase } from "@/auth/supabase"; -// ---------------- UI MOCKS ---------------- // -// Puedes reemplazarlos por tus propios componentes UI +/* ---------- UI Mocks (sin cambios) ---------- */ const Paperclip = (props) => ( - + ); const Dialog = ({ open, onOpenChange, children }) => open ?
{children}
: null; - const DialogContent = ({ className, children }) => -
e.stopPropagation()}> - {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}
; -// ---------------- COMPONENTE ---------------- // -export default function AIChatModal({ open, onClose, context, onAccept }) { +/* ------------- COMPONENT ------------- */ +export default function AIChatModal({ open, onClose, context, onAccept,planId }) { const [vectorStores, setVectorStores] = useState([]); const [vectorFiles, setVectorFiles] = useState([]); + const [selectedVectorFile, setSelectedVectorFile] = useState(null); const [attachedFile, setAttachedFile] = useState(null); const [attachedPreview, setAttachedPreview] = useState(null); @@ -49,179 +45,368 @@ export default function AIChatModal({ open, onClose, context, onAccept }) { const [loading, setLoading] = useState(false); const [loadingFiles, setLoadingFiles] = useState(false); - const [selectedVector, setSelectedVector] = useState(null); + 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]); - // ------------------------------------ - // Reset al abrir o cerrar modal - // ------------------------------------ + 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(planId); + if (!open) { + // si ya existe una conversación la eliminamos + if (conversationId) { + deleteConversation(conversationId).catch((e) => console.error(e)); + } setMessages([]); setInput(""); + setSelectedVectorFile(null); setAttachedFile(null); setAttachedPreview(null); - setVectorStores([]); - setVectorFiles([]); - setSelectedVector(null); + setConversationId(null); return; } + // inyectar contexto como system message if (context) { setMessages([ { role: "system", - content: `Contexto: ${context.section}\nTexto original:\n${context.originalText || "—"}`, - }, + content: `Contexto académico:\n${context.section || "—"}\n\nTexto original:\n${context.originalText || "—"}` + } ]); + } else { + setMessages(prev => prev); // no hacer nada si no hay contexto } - }, [open, context]); - // ------------------------------------ - // Cargar vector stores - // ------------------------------------ - useEffect(() => { - if (!open) return; + // crear conversación y esperar a que termine antes de permitir enviar + (async () => { + await createConversation(); + // tras crear podemos también cargar vector stores + fetchVectorStores(); + })(); - 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 - // ------------------------------------ + // --------- 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("modal-conversation", { + headers: { Authorization: `Bearer ${token}` }, + body: { action: "start" } + }); + + 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("modal-conversation", { + 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 }) => { + 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 = []; + console.log(attachedFile); + if (attachedFile) { + const base64 = await fileToBase64(attachedFile); + console.log(attachedFile); + + filesInput.push({ + type: "input_file", + filename: attachedFile.name, + file_data: `data:application/pdf;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", + conversationId, + vectorStoreId: selectedVectorFile?.vector_store_id ?? null, + fileIds: selectedVectorFile ? [selectedVectorFile.id] : [], + input: [ + { + role: "user", + content: [ + { type: "input_text", text }, + ...filesInput + ] + } + ] + }; + + console.log("handleConversation -> payload:", payload); + + const { data: invokeData, error } = await supabase.functions.invoke( + "modal-conversation", + { + 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."; + + console.log("💬 Respuesta detectada:", assistantText); + + setMessages(prev => [...prev, { role: "assistant", content: assistantText }]); + + setAttachedFile(null); + setAttachedPreview(null); + + } 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 }, - }, - } - ); + 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 : []); + setVectorFiles(Array.isArray(data) ? data : (data?.data ?? [])); } catch (err) { - console.error("Error al obtener archivos del vector store:", err); + console.error("Error loading vector files:", err); setVectorFiles([]); } finally { setLoadingFiles(false); } }; - // ------------------------------------ - // Adjuntar archivo - // ------------------------------------ + // ---------- UI helpers ---------- const handleAttach = (e) => { const file = e.target.files?.[0]; + console.log(file); + 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); + const handleSelectVectorFile = (file) => { + setSelectedVectorFile(file); + }; + + // ---------- Send flow ---------- + const handleSend = async () => { + if (!input.trim() && !attachedFile && !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(); + } }; - // ------------------------------------ - // UI - // ------------------------------------ return ( - + Asistente Inteligente @@ -229,82 +414,85 @@ Responde con una versión mejorada en texto directo, sin explicaciones.
- {/* LEFT: VECTOR STORES */} - - + {/* Left: vectors */} + +

Vector Stores

- - {loading ? ( -

Cargando...

+ {loadingVectors ? ( +

Cargando vector stores...

) : vectorStores.length === 0 ? ( -

No hay vector stores

+

No hay vector stores.

) : (
    - {vectorStores.map(store => ( -
  • { - setSelectedVector(store); - loadFilesForVector(store.id); - }} - className={`border p-2 rounded-lg cursor-pointer - ${selectedVector?.id === store.id ? "bg-blue-100" : "bg-white"}`} + {vectorStores.map((vector) => ( +
  • loadFilesForVector(vector.id)} + className="border cursor-pointer hover:bg-blue-50 p-2 rounded-lg bg-white" > - {store.name || store.id} -

    {store.description}

    + {vector.name || vector.id} +

    {vector.description || vector.id}

  • ))}
)}
-

Archivos

- +
+

Archivos del Vector

{loadingFiles ? ( -

Cargando archivos...

- ) : vectorFiles.length === 0 ? ( -

No hay archivos

+

Cargando archivos...

+ ) : selectedVectorFile ? ( +
+ Seleccionado: {selectedVectorFile.filename ?? selectedVectorFile.id} +
) : ( -
    - {vectorFiles.map(f => ( -
  • - {f.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

+ {/* Right: Chat */} + + +

Chat con IA

-
+
{messages.length === 0 ? ( -

Inicia la conversación

+

Inicia una conversación...

) : ( - messages.map((msg, idx) => ( -
- {msg.role === "user" ? "Tú:" : "IA:"} -

{msg.content}

+ messages.map((m, i) => ( +
+ {m.role === "user" ? "Tú:" : m.role === "assistant" ? "IA:" : "Sistema:"}{" "} +
{m.content}
)) )} {loading && ( -
- La IA está respondiendo... +
+ + + + + La IA está respondiendo...
)} @@ -312,61 +500,46 @@ Responde con una versión mejorada en texto directo, sin explicaciones.
{attachedPreview && ( -
- - {attachedPreview} - +
+ + + {attachedPreview} + +
)} - {/* Input */} -
-