Se agrega modelo de respuestas y conversaciones archivos multiples y contexto de id plan de estudios

This commit is contained in:
2025-11-25 11:34:00 -06:00
parent 6f97a83eb0
commit 93c79eee77
2 changed files with 53 additions and 47 deletions

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { supabase } from "@/auth/supabase"; import { supabase } from "@/auth/supabase";
import ReactMarkdown from "react-markdown"
/* ---------- UI Mocks (sin cambios) ---------- */ /* ---------- UI Mocks (sin cambios) ---------- */
const Paperclip = (props) => ( const Paperclip = (props) => (
@@ -32,13 +33,13 @@ const CardContent = ({ className, children }) => <div className={`p-4 ${classNam
const ScrollArea = ({ className, children }) => <div className={`overflow-y-auto ${className}`}>{children}</div>; const ScrollArea = ({ className, children }) => <div className={`overflow-y-auto ${className}`}>{children}</div>;
/* ------------- COMPONENT ------------- */ /* ------------- COMPONENT ------------- */
export default function AIChatModal({ open, onClose, context, onAccept,planId }) { export default function AIChatModal({ open, onClose, context, onAccept }) {
const [vectorStores, setVectorStores] = useState([]); const [vectorStores, setVectorStores] = useState([]);
const [vectorFiles, setVectorFiles] = useState([]); const [vectorFiles, setVectorFiles] = useState([]);
const [selectedVectorFile, setSelectedVectorFile] = useState(null); const [selectedVectorFile, setSelectedVectorFile] = useState(null);
const [attachedFile, setAttachedFile] = useState(null); const [attachedFiles, setAttachedFiles] = useState([]);
const [attachedPreview, setAttachedPreview] = useState(null); const [attachedPreviews, setAttachedPreviews] = useState([]);
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
@@ -79,7 +80,8 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
// Al abrir: reset o crear conversación // Al abrir: reset o crear conversación
useEffect(() => { useEffect(() => {
console.log(planId); console.log(context.cont_conversation);
console.log(context);
if (!open) { if (!open) {
// si ya existe una conversación la eliminamos // si ya existe una conversación la eliminamos
@@ -89,8 +91,8 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
setMessages([]); setMessages([]);
setInput(""); setInput("");
setSelectedVectorFile(null); setSelectedVectorFile(null);
setAttachedFile(null); setAttachedFiles([]);
setAttachedPreview(null); setAttachedPreviews([]);
setConversationId(null); setConversationId(null);
return; return;
} }
@@ -127,7 +129,7 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
// llamada // llamada
const resp = await supabase.functions.invoke("modal-conversation", { const resp = await supabase.functions.invoke("modal-conversation", {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
body: { action: "start" } body: { action: "start" , role:"system", content:context.cont_conversation, }
}); });
console.log("createConversation -> raw resp:", resp); console.log("createConversation -> raw resp:", resp);
@@ -221,17 +223,17 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
const token = session?.access_token; const token = session?.access_token;
let filesInput = []; let filesInput = [];
console.log(attachedFile);
if (attachedFile) {
const base64 = await fileToBase64(attachedFile);
console.log(attachedFile);
if (attachedFiles.length > 0) {
for (const file of attachedFiles) {
const base64 = await fileToBase64(file);
filesInput.push({ filesInput.push({
type: "input_file", type: "input_file",
filename: attachedFile.name, filename: file.name,
file_data: `data:application/pdf;base64,${base64}` file_data: `data:${file.type};base64,${base64}`
}); });
} }
}
if (selectedVectorFile) { if (selectedVectorFile) {
// si el archivo del vector viene sólo con id // si el archivo del vector viene sólo con id
@@ -297,8 +299,9 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
setMessages(prev => [...prev, { role: "assistant", content: assistantText }]); setMessages(prev => [...prev, { role: "assistant", content: assistantText }]);
setAttachedFile(null); setAttachedFiles([]);
setAttachedPreview(null); setAttachedPreviews([]);
} catch (err) { } catch (err) {
console.error("Error en handleConversation:", err); console.error("Error en handleConversation:", err);
@@ -357,13 +360,13 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
// ---------- UI helpers ---------- // ---------- UI helpers ----------
const handleAttach = (e) => { const handleAttach = (e) => {
const file = e.target.files?.[0]; const files = Array.from(e.target.files);
console.log(file); if (!files.length) return;
setAttachedFiles(prev => [...prev, ...files]);
setAttachedPreviews(prev => [...prev, ...files.map(f => f.name)]);
};
if (!file) return;
setAttachedFile(file);
setAttachedPreview(file.name);
};
const handleSelectVectorFile = (file) => { const handleSelectVectorFile = (file) => {
setSelectedVectorFile(file); setSelectedVectorFile(file);
@@ -371,7 +374,7 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
// ---------- Send flow ---------- // ---------- Send flow ----------
const handleSend = async () => { const handleSend = async () => {
if (!input.trim() && !attachedFile && !selectedVectorFile) return; if (!input.trim() && attachedFiles.length === 0 && !selectedVectorFile) return;
// esperar si aún se está creando la conversación // esperar si aún se está creando la conversación
if (creatingConversation) { if (creatingConversation) {
@@ -411,8 +414,8 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
<DialogTitle>Asistente Inteligente</DialogTitle> <DialogTitle>Asistente Inteligente</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto pt-4"> <div className="flex-1 pt-4 min-h-0">
<div className="flex gap-6 min-h-full"> <div className="flex gap-6 h-full min-h-0">
{/* Left: vectors */} {/* Left: vectors */}
<Card className="w-1/3 min-w-[250px] max-w-sm flex flex-col bg-muted/20 border border-gray-200 rounded-2xl"> <Card className="w-1/3 min-w-[250px] max-w-sm flex flex-col bg-muted/20 border border-gray-200 rounded-2xl">
@@ -471,9 +474,12 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
{/* Right: Chat */} {/* Right: Chat */}
<Card className="flex-1 flex flex-col min-w-[350px] bg-background border border-gray-200 rounded-2xl"> <Card className="flex-1 flex flex-col min-w-[350px] bg-background border border-gray-200 rounded-2xl">
<CardContent className="flex flex-col flex-1 p-4"> <CardContent className="flex flex-col flex-1 p-4 min-h-0">
<h3 className="font-semibold text-sm mb-3 flex-shrink-0">Chat con IA</h3> <h3 className="font-semibold text-sm mb-3 flex-shrink-0">Chat con IA</h3>
<div className="flex-1 flex flex-col min-h-0">
{/* CONTENEDOR SCROLL DE LOS MENSAJES */}
<div className="flex-1 overflow-y-auto min-h-0 border border-gray-200 rounded-lg p-3 space-y-3 bg-gray-50 break-words whitespace-pre-wrap"> <div className="flex-1 overflow-y-auto min-h-0 border border-gray-200 rounded-lg p-3 space-y-3 bg-gray-50 break-words whitespace-pre-wrap">
{messages.length === 0 ? ( {messages.length === 0 ? (
<p className="text-gray-400 text-sm text-center mt-10">Inicia una conversación...</p> <p className="text-gray-400 text-sm text-center mt-10">Inicia una conversación...</p>
@@ -481,7 +487,8 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
messages.map((m, i) => ( messages.map((m, i) => (
<div key={i} className={`break-words whitespace-pre-wrap p-3 rounded-xl shadow-sm max-w-[85%] ${m.role === "user" ? "bg-blue-50 text-blue-800 ml-auto" : m.role === "assistant" ? "bg-white text-gray-800 mr-auto border border-gray-200" : "bg-gray-100 text-gray-700 mr-auto"}`}> <div key={i} className={`break-words whitespace-pre-wrap p-3 rounded-xl shadow-sm max-w-[85%] ${m.role === "user" ? "bg-blue-50 text-blue-800 ml-auto" : m.role === "assistant" ? "bg-white text-gray-800 mr-auto border border-gray-200" : "bg-gray-100 text-gray-700 mr-auto"}`}>
<strong className="font-bold">{m.role === "user" ? "Tú:" : m.role === "assistant" ? "IA:" : "Sistema:"}</strong>{" "} <strong className="font-bold">{m.role === "user" ? "Tú:" : m.role === "assistant" ? "IA:" : "Sistema:"}</strong>{" "}
<div>{m.content}</div> <ReactMarkdown>{m.content}</ReactMarkdown>
</div> </div>
)) ))
)} )}
@@ -498,21 +505,20 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
{attachedPreview && (
<div className="flex items-center justify-between mt-2 p-3 border border-gray-300 rounded-xl text-sm bg-gray-100 shadow-inner flex-shrink-0">
<span className="truncate flex items-center gap-2 text-gray-700">
<Paperclip className="w-4 h-4 text-blue-500" />
{attachedPreview}
</span>
<Button variant="outline" className="text-red-500" onClick={() => { setAttachedFile(null); setAttachedPreview(null); }}>Quitar</Button>
</div> </div>
{attachedPreviews.length > 0 && (
<ul className="text-xs text-gray-600 mt-2">
{attachedPreviews.map((name, i) => (
<li key={i}>📄 {name}</li>
))}
</ul>
)} )}
<div className="flex gap-2 mt-4 items-end flex-shrink-0"> <div className="flex gap-2 mt-4 items-end flex-shrink-0">
<label className="cursor-pointer text-gray-600 hover:text-blue-600 self-center"> <label className="cursor-pointer text-gray-600 hover:text-blue-600 self-center">
<Paperclip className="w-5 h-5" /> <Paperclip className="w-5 h-5" />
<input type="file" accept=".pdf,.txt,.doc,.docx" className="hidden" onChange={handleAttach} /> <input type="file" accept=".pdf,.txt,.doc,.docx" multiple className="hidden" onChange={handleAttach} />
</label> </label>
<textarea <textarea
@@ -530,7 +536,7 @@ export default function AIChatModal({ open, onClose, context, onAccept,planId })
style={{ minHeight: "38px" }} style={{ minHeight: "38px" }}
/> />
<Button onClick={handleSend} disabled={loading || creatingConversation || (!input.trim() && !attachedFile && !selectedVectorFile)} className="shadow-md"> <Button onClick={handleSend} disabled={loading || creatingConversation || (!input.trim() && attachedFiles.length === 0 && !selectedVectorFile)} className="shadow-md">
{creatingConversation ? "Preparando..." : loading ? "Enviando..." : "Enviar"} {creatingConversation ? "Preparando..." : loading ? "Enviando..." : "Enviar"}
</Button> </Button>

View File

@@ -310,12 +310,12 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
<AIChatModal <AIChatModal
open={openModalIa} open={openModalIa}
planId={planId}
onClose={() => setopenModalIa(false)} onClose={() => setopenModalIa(false)}
context={{ context={{
section: iaContext?.title, section: iaContext?.title,
fieldKey: iaContext?.key, fieldKey: iaContext?.key,
originalText: iaContext?.content, originalText: iaContext?.content,
cont_conversation: `Eres un experto en craer planes de estudios basate en el id del plan ${planId} que se encuentra en la tabla plan_estudios con el mcp para realizar los cambios que se te soliciten`,
}} }}
onAccept={(newText: string) => { onAccept={(newText: string) => {
if (iaContext) { if (iaContext) {