Se agrega modelo de respuestas y conversaciones archivos multiples y contexto de id plan de estudios
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user