diff --git a/src/components/ai/AIChatModal.jsx b/src/components/ai/AIChatModal.jsx
new file mode 100644
index 0000000..b97769b
--- /dev/null
+++ b/src/components/ai/AIChatModal.jsx
@@ -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) => (
+
+);
+
+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}
;
+
+// ---------------- 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 (
+
+ );
+}
diff --git a/src/components/ai/AIChatModal.tsx b/src/components/ai/AIChatModal.tsx
deleted file mode 100644
index e7955f1..0000000
--- a/src/components/ai/AIChatModal.tsx
+++ /dev/null
@@ -1,371 +0,0 @@
-import React, { useEffect, useState } from "react"
-import { supabase } from "@/auth/supabase"
-import type { PlanTextFields } from "../planes/academic-sections";
-
-// 🔹 SIMULACIÓN DE ICONO LUCIDE-REACT
-const Paperclip = (props) => (
-
-);
-
-// 🔹 SIMULACIÓN DE SHADCN/UI
-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}
;
-// ====================================================================
-
-
-type AIChatModalProps = {
- open: boolean
- onClose: () => void
- edgeFunctionUrl: string
- context?: {
- section?: string
- fieldKey?: keyof PlanTextFields
- originalText?: string
- }
- onAccept?: (newText: string) => void
-}
-
-export default function AIChatModal({ open, onClose, edgeFunctionUrl, context, onAccept }: AIChatModalProps) {
- const [files, setFiles] = useState([])
- const [attachedFile, setAttachedFile] = useState(null)
- const [attachedPreview, setAttachedPreview] = useState(null)
- const [messages, setMessages] = useState<{ role: string; content: string }[]>([])
- const [input, setInput] = useState("")
- const [loading, setLoading] = useState(false)
-
- // Referencia para desplazar al final del chat
- const messagesEndRef = React.useRef(null);
-
- const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
- useEffect(scrollToBottom, [messages])
-
- useEffect(() => {
- if (!open) {
- // 🧹 Limpia mensajes y archivos al cerrar
- setMessages([])
- setInput("")
- setAttachedFile(null)
- setAttachedPreview(null)
- } else if (context) {
- // 🧩 Muestra el contexto inicial al abrir
- setMessages([
- {
- role: "system",
- content: `Contexto: ${context.section}\nTexto original:\n${context.originalText || "—"}`,
- },
- ])
- }
-}, [open, context])
-
-
- // 🔹 Obtener lista de archivos del Vector Store (Lógica de API original)
- useEffect(() => {
- if (!open) return
- const fetchVectorFiles = async () => {
- // Nota: La verificación de Supabase ahora pasa por el mock.
- if (typeof supabase === 'undefined' || !supabase.auth) {
- console.error("Supabase no está disponible (Simulación). Saltando fetch de archivos.");
- return;
- }
-
- try {
- setLoading(true)
- const { data: { session } } = await supabase.auth.getSession()
- const token = session?.access_token
-
- // 🟢 TU LÓGICA DE FETCH ORIGINAL
- const res = await fetch(`${edgeFunctionUrl}?action=list_files`, {
- headers: { Authorization: `Bearer ${token}` },
- })
- const data = await res.json()
- if (data.files) setFiles(data.files)
- else console.warn("No se encontraron archivos en el vector store.")
- } catch (err) {
- console.error("Error al cargar archivos del vector store:", err)
- } finally {
- setLoading(false)
- }
- }
- fetchVectorFiles()
- }, [open, edgeFunctionUrl])
-
- // 📎 Adjuntar archivo
- const handleAttach = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0]
- if (!file) return
- setAttachedFile(file)
- setAttachedPreview(file.name)
- }
-
- // 🚀 Enviar prompt o archivo al Edge Function (Lógica de API original)
-const handleSend = async () => {
- if (!input.trim() && !attachedFile) return
-
- // 🧩 Crear el mensaje visible del usuario
- let userMessageContent = input.trim()
- if (attachedFile) {
- userMessageContent += (userMessageContent ? " | " : "") + `Adjunto: ${attachedFile.name}`
- }
- if (!userMessageContent && attachedFile) {
- userMessageContent = `Consulta de archivo: ${attachedFile.name}`
- }
-
- setMessages((prev) => [...prev, { role: "user", content: userMessageContent }])
- setInput("")
- setLoading(true)
-
- try {
- if (typeof supabase === "undefined" || !supabase.functions) {
- throw new Error("Supabase no está disponible o no soporta Edge Functions.")
- }
-
- const formData = new FormData()
-
- // 🧠 Construimos un prompt limpio con el contexto del campo
- const contextText = context?.originalText || "Sin texto original"
- const section = context?.section ? `Sección: ${context.section}` : ""
- const field = context?.fieldKey ? `Campo: ${context.fieldKey}` : ""
-
- const fullPrompt = `
-${section}
-${field}
-
-Texto original:
-${contextText}
-
-Solicitud del usuario:
-${input}
-
-Responde con una versión mejorada del texto, sin agregar frases como “Aquí tienes” ni explicaciones.
- `.trim()
-
- formData.append("prompt", fullPrompt)
- if (attachedFile) formData.append("file", attachedFile)
-
- // 🟢 Llamada a la Edge Function
- const { data, error } = await supabase.functions.invoke("simple-chat", {
- body: formData,
- })
-
- if (error) throw error
-
- setMessages((prev) => [
- ...prev,
- { role: "assistant", content: data?.text || "Sin respuesta del modelo." },
- ])
- } catch (err: any) {
- console.error("Error al enviar prompt:", err)
- setMessages((prev) => [
- ...prev,
- { role: "assistant", content: "Ocurrió un error al conectar con la API." },
- ])
- } finally {
- setLoading(false)
- setAttachedFile(null)
- setAttachedPreview(null)
- }
-}
-
-
-
- return (
-
- )
-}
\ No newline at end of file
diff --git a/src/components/planes/academic-sections.tsx b/src/components/planes/academic-sections.tsx
index e76a9f5..dd62b41 100644
--- a/src/components/planes/academic-sections.tsx
+++ b/src/components/planes/academic-sections.tsx
@@ -8,7 +8,8 @@ import { supabase,useSupabaseAuth } from "@/auth/supabase"
import { toast } from "sonner"
import ReactMarkdown from 'react-markdown'
import { HistorialCambiosModal } from "../historico/HistorialCambiosModal"
-import AIChatModal from "../ai/AIChatModal"
+// @ts-ignore
+import AIChatModal from "../ai/AIChatModal"
/* =====================================================
@@ -306,6 +307,7 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
updateField.mutate({ key, value })
}}
/>
+
setopenModalIa(false)}
diff --git a/tsconfig.json b/tsconfig.json
index 7920df9..dda04bb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,5 @@
{
- "include": ["**/*.ts", "**/*.tsx"],
+ "include": ["**/*.ts", "**/*.tsx", "src/components/ai/AIChatModal.jsx", "src/components/ai/AIChatModal.js"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",