|
|
|
@@ -1,6 +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"
|
|
|
|
import ReactMarkdown from "react-markdown";
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- UI Mocks (sin cambios) ---------- */
|
|
|
|
/* ---------- UI Mocks (sin cambios) ---------- */
|
|
|
|
const Paperclip = (props) => (
|
|
|
|
const Paperclip = (props) => (
|
|
|
|
@@ -33,23 +33,28 @@ 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 }) {
|
|
|
|
export default function AIChatModal({ open, onClose, context, onAccept, plan_format }) {
|
|
|
|
|
|
|
|
|
|
|
|
const [vectorStores, setVectorStores] = useState([]);
|
|
|
|
const [vectorStores, setVectorStores] = useState([]);
|
|
|
|
const [vectorFiles, setVectorFiles] = useState([]);
|
|
|
|
const [vectorFiles, setVectorFiles] = useState([]);
|
|
|
|
const [selectedVectorFile, setSelectedVectorFile] = useState(null);
|
|
|
|
const [selectedVector, setSelectedVector] = useState(null);
|
|
|
|
|
|
|
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
|
|
|
|
|
|
|
|
|
|
const [attachedFiles, setAttachedFiles] = useState([]);
|
|
|
|
const [attachedFiles, setAttachedFiles] = useState([]);
|
|
|
|
const [attachedPreviews, setAttachedPreviews] = useState([]);
|
|
|
|
const [attachedPreviews, setAttachedPreviews] = useState([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// chat
|
|
|
|
const [messages, setMessages] = useState([]);
|
|
|
|
const [messages, setMessages] = useState([]);
|
|
|
|
const [input, setInput] = useState("");
|
|
|
|
const [input, setInput] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// loading states
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [loadingFiles, setLoadingFiles] = useState(false);
|
|
|
|
const [loadingFiles, setLoadingFiles] = useState(false);
|
|
|
|
const [loadingVectors, setLoadingVectors] = useState(false);
|
|
|
|
const [loadingVectors, setLoadingVectors] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// conversation control
|
|
|
|
const [conversationId, setConversationId] = useState(null);
|
|
|
|
const [conversationId, setConversationId] = useState(null);
|
|
|
|
const [creatingConversation, setCreatingConversation] = useState(false); // control para esperar
|
|
|
|
const [creatingConversation, setCreatingConversation] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const messagesEndRef = useRef(null);
|
|
|
|
const messagesEndRef = useRef(null);
|
|
|
|
const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
@@ -57,103 +62,66 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
|
|
|
|
|
|
|
|
const normalizeInvokeResponse = (resp) => {
|
|
|
|
const normalizeInvokeResponse = (resp) => {
|
|
|
|
if (!resp) return null;
|
|
|
|
if (!resp) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// cuando invocas funciones, Supabase siempre regresa:
|
|
|
|
|
|
|
|
// { data: "...string...", error: null, response: {} }
|
|
|
|
|
|
|
|
const raw = resp.data;
|
|
|
|
const raw = resp.data;
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof raw === "string") {
|
|
|
|
if (typeof raw === "string") {
|
|
|
|
try {
|
|
|
|
try { return JSON.parse(raw); } catch (e) { console.warn("❗ No se pudo parsear resp.data:", raw); return null; }
|
|
|
|
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;
|
|
|
|
if (typeof raw === "object" && raw !== null) return raw;
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Al abrir: reset o crear conversación
|
|
|
|
// Al abrir: reset o crear conversación
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
console.log(context.cont_conversation);
|
|
|
|
|
|
|
|
console.log(context);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
if (!open) {
|
|
|
|
// si ya existe una conversación la eliminamos
|
|
|
|
|
|
|
|
if (conversationId) {
|
|
|
|
if (conversationId) {
|
|
|
|
deleteConversation(conversationId).catch((e) => console.error(e));
|
|
|
|
deleteConversation(conversationId).catch((e) => console.error(e));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setMessages([]);
|
|
|
|
setMessages([]);
|
|
|
|
setInput("");
|
|
|
|
setInput("");
|
|
|
|
setSelectedVectorFile(null);
|
|
|
|
setSelectedFiles([]);
|
|
|
|
setAttachedFiles([]);
|
|
|
|
setAttachedFiles([]);
|
|
|
|
setAttachedPreviews([]);
|
|
|
|
setAttachedPreviews([]);
|
|
|
|
setConversationId(null);
|
|
|
|
setConversationId(null);
|
|
|
|
|
|
|
|
setSelectedVector(null);
|
|
|
|
|
|
|
|
setVectorFiles([]);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// inyectar contexto como system message
|
|
|
|
|
|
|
|
if (context) {
|
|
|
|
if (context) {
|
|
|
|
setMessages([
|
|
|
|
setMessages([
|
|
|
|
{
|
|
|
|
{
|
|
|
|
role: "system",
|
|
|
|
role: "system",
|
|
|
|
|
|
|
|
//content: `Contexto académico:\n${context.section || "—"}\n\nTexto original:\n${context.originalText || "—"}`
|
|
|
|
content: `Contexto académico:\n${context.section || "—"}\n\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
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// crear conversación y esperar a que termine antes de permitir enviar
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
(async () => {
|
|
|
|
await createConversation();
|
|
|
|
await createConversation();
|
|
|
|
// tras crear podemos también cargar vector stores
|
|
|
|
|
|
|
|
fetchVectorStores();
|
|
|
|
fetchVectorStores();
|
|
|
|
})();
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
}, [open]);
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
|
|
// --------- CREATE CONVERSATION (robusto) ----------
|
|
|
|
// ---------- CREATE CONVERSATION ----------
|
|
|
|
const createConversation = async () => {
|
|
|
|
const createConversation = async () => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
setCreatingConversation(true);
|
|
|
|
setCreatingConversation(true);
|
|
|
|
|
|
|
|
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
const token = session?.access_token;
|
|
|
|
const token = session?.access_token;
|
|
|
|
|
|
|
|
|
|
|
|
// 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" , role:"system", content:context.cont_conversation, }
|
|
|
|
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;
|
|
|
|
let parsed = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof resp?.data === "string") {
|
|
|
|
if (typeof resp?.data === "string") {
|
|
|
|
try {
|
|
|
|
try { parsed = JSON.parse(resp.data); } catch (e) { parsed = null; }
|
|
|
|
parsed = JSON.parse(resp.data);
|
|
|
|
} else if (typeof resp?.data === "object" && resp.data !== null) parsed = resp.data;
|
|
|
|
} catch (e) {
|
|
|
|
else parsed = resp;
|
|
|
|
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 =
|
|
|
|
const convId =
|
|
|
|
parsed?.conversationId ||
|
|
|
|
parsed?.conversationId ||
|
|
|
|
parsed?.data?.conversationId ||
|
|
|
|
parsed?.data?.conversationId ||
|
|
|
|
@@ -162,24 +130,16 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
parsed?.conversation_id ||
|
|
|
|
parsed?.conversation_id ||
|
|
|
|
parsed?.data?.conversation_id;
|
|
|
|
parsed?.data?.conversation_id;
|
|
|
|
|
|
|
|
|
|
|
|
if (!convId) {
|
|
|
|
if (!convId) { setCreatingConversation(false); return; }
|
|
|
|
console.warn("No se encontró conversationId en la respuesta parseada:", parsed);
|
|
|
|
|
|
|
|
setCreatingConversation(false);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setConversationId(convId);
|
|
|
|
setConversationId(convId);
|
|
|
|
console.log("🟢 Conversación creada y guardada:", convId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error creando conversación:", err);
|
|
|
|
console.error("Error creando conversación:", err);
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
setCreatingConversation(false);
|
|
|
|
setCreatingConversation(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- DELETE CONVERSATION ----------
|
|
|
|
// --------- DELETE CONVERSATION (robusto) ----------
|
|
|
|
|
|
|
|
const deleteConversation = async (convIdParam) => {
|
|
|
|
const deleteConversation = async (convIdParam) => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const convIdToUse = convIdParam ?? conversationId;
|
|
|
|
const convIdToUse = convIdParam ?? conversationId;
|
|
|
|
@@ -187,13 +147,11 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
const token = session?.access_token;
|
|
|
|
const token = session?.access_token;
|
|
|
|
|
|
|
|
|
|
|
|
// algunas implementations esperan { action: "end", conversationId }, otras { action: "end", id }
|
|
|
|
await supabase.functions.invoke("modal-conversation", {
|
|
|
|
const { data, error } = await supabase.functions.invoke("modal-conversation", {
|
|
|
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
body: { action: "end", conversationId: convIdToUse }
|
|
|
|
body: { action: "end", conversationId: convIdToUse }
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("deleteConversation -> response:", data);
|
|
|
|
|
|
|
|
setConversationId(null);
|
|
|
|
setConversationId(null);
|
|
|
|
} catch (err) {
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error eliminando conversación:", err);
|
|
|
|
console.error("Error eliminando conversación:", err);
|
|
|
|
@@ -209,11 +167,13 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- SEND MESSAGE (usa conversationId) ----------
|
|
|
|
// ---------- HANDLE CONVERSATION (envío) ----------
|
|
|
|
const handleConversation = async ({ text }) => {
|
|
|
|
const handleConversation = async ({ text }) => {
|
|
|
|
|
|
|
|
let contextText = "";
|
|
|
|
|
|
|
|
if (context?.originalText) contextText += `CONTEXTO DEL CAMPO:\n${context.originalText}\n`;
|
|
|
|
|
|
|
|
|
|
|
|
if (!conversationId) {
|
|
|
|
if (!conversationId) {
|
|
|
|
console.warn("No hay conversación activa todavía. conversationId:", 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;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -222,8 +182,8 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
const token = session?.access_token;
|
|
|
|
const token = session?.access_token;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// archivos adjuntos (locales) -> base64
|
|
|
|
let filesInput = [];
|
|
|
|
let filesInput = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (attachedFiles.length > 0) {
|
|
|
|
if (attachedFiles.length > 0) {
|
|
|
|
for (const file of attachedFiles) {
|
|
|
|
for (const file of attachedFiles) {
|
|
|
|
const base64 = await fileToBase64(file);
|
|
|
|
const base64 = await fileToBase64(file);
|
|
|
|
@@ -233,31 +193,35 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
file_data: `data:${file.type};base64,${base64}`
|
|
|
|
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
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// archivos seleccionados del vector (por id)
|
|
|
|
|
|
|
|
if (selectedFiles.length > 0) {
|
|
|
|
|
|
|
|
const filesFromVectors = selectedFiles.map(f => ({
|
|
|
|
|
|
|
|
type: "input_file",
|
|
|
|
|
|
|
|
file_id: f.id
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
filesInput = [...filesInput, ...filesFromVectors];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const promptFinal = `${contextText}\nPREGUNTA DEL USUARIO:\n${text}`;
|
|
|
|
const payload = {
|
|
|
|
const payload = {
|
|
|
|
action: "message",
|
|
|
|
action: "message",
|
|
|
|
|
|
|
|
format: plan_format,
|
|
|
|
conversationId,
|
|
|
|
conversationId,
|
|
|
|
vectorStoreId: selectedVectorFile?.vector_store_id ?? null,
|
|
|
|
vectorStoreId: selectedVector ?? null,
|
|
|
|
fileIds: selectedVectorFile ? [selectedVectorFile.id] : [],
|
|
|
|
fileIds: selectedFiles.length ? selectedFiles.map(f => f.id) : [],
|
|
|
|
input: [
|
|
|
|
input: [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
role: "user",
|
|
|
|
role: "user",
|
|
|
|
content: [
|
|
|
|
content: [
|
|
|
|
{ type: "input_text", text },
|
|
|
|
{ type: "input_text", text: promptFinal },
|
|
|
|
...filesInput
|
|
|
|
...filesInput
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
]
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { data: invokeData, error } = await supabase.functions.invoke(
|
|
|
|
const { data: invokeData, error } = await supabase.functions.invoke(
|
|
|
|
"modal-conversation",
|
|
|
|
"modal-conversation",
|
|
|
|
{
|
|
|
|
{
|
|
|
|
@@ -267,40 +231,24 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("handleConversation -> RAW invokeData:", invokeData);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const parsed = normalizeInvokeResponse({ data: invokeData });
|
|
|
|
const parsed = normalizeInvokeResponse({ data: invokeData });
|
|
|
|
console.log("handleConversation -> PARSED:", parsed);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 EXTRACTOR DEFINITIVO
|
|
|
|
// Extraer texto del assistant (robusto)
|
|
|
|
let assistantText = null;
|
|
|
|
let assistantText = null;
|
|
|
|
|
|
|
|
if (parsed?.data?.output_text) assistantText = parsed.data.output_text;
|
|
|
|
// 1) directo
|
|
|
|
|
|
|
|
if (parsed?.data?.output_text) {
|
|
|
|
|
|
|
|
assistantText = parsed.data.output_text;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 2) buscar el message
|
|
|
|
|
|
|
|
if (!assistantText && Array.isArray(parsed?.data?.output)) {
|
|
|
|
if (!assistantText && Array.isArray(parsed?.data?.output)) {
|
|
|
|
const msgBlock = parsed.data.output.find(o => o.type === "message");
|
|
|
|
const msgBlock = parsed.data.output.find(o => o.type === "message");
|
|
|
|
if (msgBlock?.content?.[0]?.text) {
|
|
|
|
if (msgBlock?.content?.[0]?.text) assistantText = msgBlock.content[0].text;
|
|
|
|
assistantText = msgBlock.content[0].text;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 3) fallback
|
|
|
|
|
|
|
|
assistantText = assistantText || "Sin respuesta del modelo.";
|
|
|
|
assistantText = assistantText || "Sin respuesta del modelo.";
|
|
|
|
|
|
|
|
|
|
|
|
setMessages(prev => [
|
|
|
|
setMessages(prev => [...prev, { role: "assistant", content: cleanAssistantResponse(assistantText) }]);
|
|
|
|
...prev,
|
|
|
|
|
|
|
|
{ role: "assistant", content: cleanAssistantResponse(assistantText) }
|
|
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// limpiar attachments locales (pero mantener seleccionados del vector si quieres — aquí los limpiamos)
|
|
|
|
setAttachedFiles([]);
|
|
|
|
setAttachedFiles([]);
|
|
|
|
setAttachedPreviews([]);
|
|
|
|
setAttachedPreviews([]);
|
|
|
|
|
|
|
|
// si quieres mantener los selectedFiles tras el envío, comenta la siguiente línea:
|
|
|
|
|
|
|
|
setSelectedFiles([]);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error en handleConversation:", err);
|
|
|
|
console.error("Error en handleConversation:", err);
|
|
|
|
@@ -361,29 +309,49 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
const handleAttach = (e) => {
|
|
|
|
const handleAttach = (e) => {
|
|
|
|
const files = Array.from(e.target.files);
|
|
|
|
const files = Array.from(e.target.files);
|
|
|
|
if (!files.length) return;
|
|
|
|
if (!files.length) return;
|
|
|
|
|
|
|
|
|
|
|
|
setAttachedFiles(prev => [...prev, ...files]);
|
|
|
|
setAttachedFiles(prev => [...prev, ...files]);
|
|
|
|
setAttachedPreviews(prev => [...prev, ...files.map(f => f.name)]);
|
|
|
|
setAttachedPreviews(prev => [...prev, ...files.map(f => f.name)]);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Al hacer click en un vector: expandir (solo uno a la vez) y cargar sus archivos
|
|
|
|
|
|
|
|
const handleVectorClick = async (vector) => {
|
|
|
|
|
|
|
|
if (selectedVector === vector.id) {
|
|
|
|
|
|
|
|
// colapsar
|
|
|
|
|
|
|
|
setSelectedVector(null);
|
|
|
|
|
|
|
|
setVectorFiles([]);
|
|
|
|
|
|
|
|
setSelectedFiles([]);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectVectorFile = (file) => {
|
|
|
|
setSelectedVector(vector.id);
|
|
|
|
setSelectedVectorFile(file);
|
|
|
|
setSelectedFiles([]);
|
|
|
|
|
|
|
|
await loadFilesForVector(vector.id);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle selección de archivo (checkbox)
|
|
|
|
|
|
|
|
const toggleFileSelection = (file) => {
|
|
|
|
|
|
|
|
if (selectedFiles.some(f => f.id === file.id)) {
|
|
|
|
|
|
|
|
setSelectedFiles(prev => prev.filter(f => f.id !== file.id));
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
setSelectedFiles(prev => [...prev, file]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const removeSelectedFile = (fileId) => {
|
|
|
|
|
|
|
|
setSelectedFiles(prev => prev.filter(f => f.id !== fileId));
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Send flow ----------
|
|
|
|
// ---------- Send flow ----------
|
|
|
|
const handleSend = async () => {
|
|
|
|
const handleSend = async () => {
|
|
|
|
if (!input.trim() && attachedFiles.length === 0 && !selectedVectorFile) return;
|
|
|
|
// no permitir enviar si no hay nada
|
|
|
|
|
|
|
|
if (!input.trim() && attachedFiles.length === 0 && selectedFiles.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// esperar si aún se está creando la conversación
|
|
|
|
|
|
|
|
if (creatingConversation) {
|
|
|
|
if (creatingConversation) {
|
|
|
|
console.log("Esperando a que se cree la conversación...");
|
|
|
|
// no bloqueo visible aquí por diseño; simplemente ignoramos el envío si aún creando
|
|
|
|
// opcional: podrías mostrar un toast; aquí simplemente retornamos
|
|
|
|
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!conversationId) {
|
|
|
|
if (!conversationId) {
|
|
|
|
console.warn("No hay conversationId — intentaremos crear una ahora.");
|
|
|
|
|
|
|
|
await createConversation();
|
|
|
|
await createConversation();
|
|
|
|
if (!conversationId) {
|
|
|
|
if (!conversationId) {
|
|
|
|
setMessages(prev => [...prev, { role: "assistant", content: "No se pudo crear la conversación. Intenta de nuevo." }]);
|
|
|
|
setMessages(prev => [...prev, { role: "assistant", content: "No se pudo crear la conversación. Intenta de nuevo." }]);
|
|
|
|
@@ -391,122 +359,197 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const userText = input.trim() || (selectedVectorFile ? `Consultar archivo vector: ${selectedVectorFile.filename || selectedVectorFile.id}` : "");
|
|
|
|
const userText = input.trim() || (selectedFiles.length ? `Consultar ${selectedFiles.length} archivo(s) del repositorio` : "");
|
|
|
|
setMessages(prev => [...prev, { role: "user", content: userText }]);
|
|
|
|
setMessages(prev => [...prev, { role: "user", content: userText }]);
|
|
|
|
setInput("");
|
|
|
|
setInput("");
|
|
|
|
|
|
|
|
|
|
|
|
await handleConversation({ text: userText });
|
|
|
|
await handleConversation({ text: userText });
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleApply = () => {
|
|
|
|
function cleanAIResponse(text) {
|
|
|
|
const last = [...messages].reverse().find(m => m.role === "assistant");
|
|
|
|
|
|
|
|
if (last && onAccept) {
|
|
|
|
|
|
|
|
onAccept(last.content);
|
|
|
|
|
|
|
|
onClose();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const cleanAssistantResponse = (text) => {
|
|
|
|
|
|
|
|
if (!text) return text;
|
|
|
|
if (!text) return text;
|
|
|
|
|
|
|
|
|
|
|
|
// Frases que quieres eliminar (puedes agregar más)
|
|
|
|
let cleaned = text;
|
|
|
|
const patterns = [
|
|
|
|
|
|
|
|
/^claro[, ]*/i,
|
|
|
|
// -------------------------
|
|
|
|
/^por supuesto[, ]*/i,
|
|
|
|
// 1. Eliminar emojis
|
|
|
|
/^aquí tienes[, ]*/i,
|
|
|
|
// -------------------------
|
|
|
|
/^con gusto[, ]*/i,
|
|
|
|
cleaned = cleaned.replace(/[\p{Emoji}\uFE0F]/gu, "");
|
|
|
|
/^hola[, ]*/i,
|
|
|
|
|
|
|
|
/^perfecto[, ]*/i,
|
|
|
|
// -------------------------
|
|
|
|
/^entendido[, ]*/i,
|
|
|
|
// 2. Eliminar separadores tipo ---
|
|
|
|
/^muy bien[, ]*/i,
|
|
|
|
// -------------------------
|
|
|
|
/^ok[, ]*/i,
|
|
|
|
cleaned = cleaned.replace(/^---+$/gm, "");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
// 3. Eliminar saludos y frases meta
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
const metaPatterns = [
|
|
|
|
|
|
|
|
/^hola[!¡., ]*/i,
|
|
|
|
|
|
|
|
/^buen(os|as) (días|tardes|noches)[!¡., ]*/i,
|
|
|
|
|
|
|
|
/estoy aquí para ayudarte[.! ]*/gi,
|
|
|
|
|
|
|
|
/aquí tienes[,:]*/gi,
|
|
|
|
|
|
|
|
/claro[,:]*/gi,
|
|
|
|
|
|
|
|
/como pediste[,:]*/gi,
|
|
|
|
|
|
|
|
/como solicitaste[,:]*/gi,
|
|
|
|
|
|
|
|
/el texto íntegro que compartiste.*$/gi,
|
|
|
|
|
|
|
|
/te lo dejo a continuación.*$/gi,
|
|
|
|
|
|
|
|
/¿te gustaría.*$/gi,
|
|
|
|
|
|
|
|
/¿en qué más puedo.*$/gi,
|
|
|
|
|
|
|
|
/si necesitas algo más.*$/gi,
|
|
|
|
|
|
|
|
/con gusto.*$/gi,
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
let cleaned = text.trim();
|
|
|
|
metaPatterns.forEach(p => {
|
|
|
|
|
|
|
|
|
|
|
|
for (const p of patterns) {
|
|
|
|
|
|
|
|
cleaned = cleaned.replace(p, "").trim();
|
|
|
|
cleaned = cleaned.replace(p, "").trim();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
// 4. Extraer solo contenido útil
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
const startMarker = "CONTEXTO DEL CAMPO";
|
|
|
|
|
|
|
|
const startIndex = cleaned.indexOf(startMarker);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (startIndex !== -1) {
|
|
|
|
|
|
|
|
cleaned = cleaned.substring(startIndex).trim();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
// 5. Eliminar líneas vacías múltiples
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
cleaned = cleaned.replace(/\n{2,}/g, "\n\n");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
// 6. Quitar numeraciones de cortesía (opcional)
|
|
|
|
|
|
|
|
// Ejemplo: “1. ” al inicio de líneas
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
cleaned = cleaned.replace(/^\s*\d+\.\s+/gm, "");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return cleaned.trim();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleApply = () => {
|
|
|
|
|
|
|
|
const last = [...messages].reverse().find(m => m.role === "assistant");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (last && onAccept) {
|
|
|
|
|
|
|
|
const cleaned = cleanAIResponse(last.content);
|
|
|
|
|
|
|
|
onAccept(cleaned);
|
|
|
|
|
|
|
|
onClose();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cleanAssistantResponse = (text) => {
|
|
|
|
|
|
|
|
if (!text) return text;
|
|
|
|
|
|
|
|
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 cleaned;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
|
|
<Dialog open={open} onOpenChange={onClose} >
|
|
|
|
<DialogContent className="max-w-6xl w-[95vw] h-[85vh] p-6 flex flex-col relative">
|
|
|
|
<DialogContent className="max-w-6xl w-[95vw] h-[85vh] p-6 flex flex-col relative"
|
|
|
|
{/* Botón siempre visible */}
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
|
|
className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition z-50"
|
|
|
|
|
|
|
|
>
|
|
|
|
>
|
|
|
|
✕
|
|
|
|
<button onClick={onClose} className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition z-50">✕</button>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<DialogHeader>
|
|
|
|
<DialogHeader>
|
|
|
|
<DialogTitle>Asistente Inteligente</DialogTitle>
|
|
|
|
<DialogTitle>Asistente Inteligente</DialogTitle>
|
|
|
|
</DialogHeader>
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 pt-4 min-h-0">
|
|
|
|
<div className="flex-1 pt-4 min-h-0">
|
|
|
|
<div className="flex gap-6 h-full min-h-0">
|
|
|
|
<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">
|
|
|
|
<CardContent className="flex flex-col flex-1 p-4">
|
|
|
|
<CardContent className="flex flex-col flex-1 p-4">
|
|
|
|
<h3 className="font-semibold text-sm mb-3">Vector Stores</h3>
|
|
|
|
<h3 className="font-semibold text-sm mb-3">Repositorio de archivos</h3>
|
|
|
|
<ScrollArea className="flex-1">
|
|
|
|
<ScrollArea className="flex-1">
|
|
|
|
{loadingVectors ? (
|
|
|
|
{loadingVectors ? (
|
|
|
|
<p className="text-gray-500 text-sm text-center mt-10">Cargando vector stores...</p>
|
|
|
|
<p className="text-gray-500 text-sm text-center mt-10">Cargando Repositorio de archivos...</p>
|
|
|
|
) : vectorStores.length === 0 ? (
|
|
|
|
) : vectorStores.length === 0 ? (
|
|
|
|
<p className="text-gray-500 text-sm text-center mt-10">No hay vector stores.</p>
|
|
|
|
<p className="text-gray-500 text-sm text-center mt-10">No hay Repositorio de archivos.</p>
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<ul className="space-y-2">
|
|
|
|
<div className="space-y-3">
|
|
|
|
{vectorStores.map((vector) => (
|
|
|
|
{vectorStores.map((vector) => (
|
|
|
|
<li key={vector.id}
|
|
|
|
<div key={vector.id}>
|
|
|
|
onClick={() => loadFilesForVector(vector.id)}
|
|
|
|
{/* VECTOR */}
|
|
|
|
className="border cursor-pointer hover:bg-blue-50 p-2 rounded-lg bg-white"
|
|
|
|
<div
|
|
|
|
|
|
|
|
onClick={() => handleVectorClick(vector)}
|
|
|
|
|
|
|
|
className={`p-3 rounded-lg border cursor-pointer transition flex items-center justify-between
|
|
|
|
|
|
|
|
${selectedVector === vector.id ? "bg-blue-50 border-blue-400 shadow" : "bg-white border-gray-300"}`}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<strong className="truncate">{vector.name || vector.id}</strong>
|
|
|
|
<div className="truncate">
|
|
|
|
<p className="text-xs text-gray-400 truncate">{vector.description || vector.id}</p>
|
|
|
|
<strong className="block truncate">{vector.name || vector.id}</strong>
|
|
|
|
</li>
|
|
|
|
<p className="text-xs text-gray-400 truncate">{vector.description || ""}</p>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</ul>
|
|
|
|
<div className="text-xs text-gray-500">{selectedVector === vector.id ? "▼" : "▶"}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* ARCHIVOS cuando está expandido */}
|
|
|
|
|
|
|
|
{selectedVector === vector.id && (
|
|
|
|
|
|
|
|
<div className="ml-4 mt-2 mb-2 space-y-2">
|
|
|
|
|
|
|
|
{loadingFiles ? (
|
|
|
|
|
|
|
|
<p className="text-gray-400 text-sm">Cargando archivos...</p>
|
|
|
|
|
|
|
|
) : vectorFiles.length === 0 ? (
|
|
|
|
|
|
|
|
<p className="text-gray-400 text-sm">No hay archivos en este repositorio</p>
|
|
|
|
|
|
|
|
) : (
|
|
|
|
|
|
|
|
vectorFiles.map((file) => (
|
|
|
|
|
|
|
|
<label key={file.id} className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 cursor-pointer">
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
|
|
checked={selectedFiles.some(f => f.id === file.id)}
|
|
|
|
|
|
|
|
onChange={() => toggleFileSelection(file)}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<div className="text-sm">
|
|
|
|
|
|
|
|
<div className="font-medium">{file.filename ?? file.name ?? file.id}</div>
|
|
|
|
|
|
|
|
<div className="text-xs text-gray-400">{file.id}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
))
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
</ScrollArea>
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Resumen de archivos seleccionados (de vectores) */}
|
|
|
|
<div className="mt-4">
|
|
|
|
<div className="mt-4">
|
|
|
|
<h4 className="font-semibold text-sm mb-2">Archivos del Vector</h4>
|
|
|
|
<h4 className="font-semibold text-sm mb-2">Archivos seleccionados</h4>
|
|
|
|
{loadingFiles ? (
|
|
|
|
{selectedFiles.length === 0 ? (
|
|
|
|
<p className="text-sm text-gray-500">Cargando archivos...</p>
|
|
|
|
<p className="text-sm text-gray-500">No has seleccionado archivos del repositorio</p>
|
|
|
|
) : selectedVectorFile ? (
|
|
|
|
|
|
|
|
<div className="text-sm text-gray-700 mb-2">
|
|
|
|
|
|
|
|
Seleccionado: <strong>{selectedVectorFile.filename ?? selectedVectorFile.id}</strong>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<p className="text-sm text-gray-500">Selecciona un archivo del vector</p>
|
|
|
|
<ul className="space-y-2 max-h-40 overflow-auto">
|
|
|
|
)}
|
|
|
|
{selectedFiles.map((f) => (
|
|
|
|
|
|
|
|
<li key={f.id} className="flex items-center justify-between p-2 rounded-md border bg-white">
|
|
|
|
<ul className="space-y-2 max-h-40 overflow-auto mt-2">
|
|
|
|
<div className="text-sm">
|
|
|
|
{vectorFiles.map((file) => (
|
|
|
|
<div className="font-medium">{f.filename ?? f.name ?? f.id}</div>
|
|
|
|
<li key={file.id}
|
|
|
|
<div className="text-xs text-gray-400 truncate">{f.id}</div>
|
|
|
|
onClick={() => handleSelectVectorFile(file)}
|
|
|
|
</div>
|
|
|
|
className={`p-2 rounded-lg cursor-pointer border ${selectedVectorFile?.id === file.id ? "bg-blue-50 border-blue-300" : "bg-white"}`}
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
>
|
|
|
|
<span className="text-xs text-gray-500">{/* optionally show vector id */}</span>
|
|
|
|
<div className="text-sm font-medium">{file.filename}</div>
|
|
|
|
<button onClick={() => removeSelectedFile(f.id)} className="text-sm text-red-500 hover:underline">Quitar</button>
|
|
|
|
<div className="text-xs text-gray-400">{file.id}</div>
|
|
|
|
</div>
|
|
|
|
</li>
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="mt-4 flex-shrink-0">
|
|
|
|
{/* <div className="mt-4 flex-shrink-0">
|
|
|
|
<Button variant="outline" className="w-full" onClick={() => alert("Funcionalidad Subir a vector store no implementada aquí")}>Subir archivo (vector)</Button>
|
|
|
|
<Button variant="outline" className="w-full" onClick={() => alert("Funcionalidad Subir a vector store no implementada aquí")}>Subir archivo (vector)</Button>
|
|
|
|
</div>
|
|
|
|
</div> */}
|
|
|
|
</CardContent>
|
|
|
|
</CardContent>
|
|
|
|
</Card>
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 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 min-h-0">
|
|
|
|
<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">
|
|
|
|
<div className="flex-1 flex flex-col min-h-0">
|
|
|
|
@@ -519,7 +562,6 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
<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>{" "}
|
|
|
|
<ReactMarkdown>{m.content}</ReactMarkdown>
|
|
|
|
<ReactMarkdown>{m.content}</ReactMarkdown>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))
|
|
|
|
))
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
@@ -567,7 +609,7 @@ export default function AIChatModal({ open, onClose, context, onAccept }) {
|
|
|
|
style={{ minHeight: "38px" }}
|
|
|
|
style={{ minHeight: "38px" }}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Button onClick={handleSend} disabled={loading || creatingConversation || (!input.trim() && attachedFiles.length === 0 && !selectedVectorFile)} className="shadow-md">
|
|
|
|
<Button onClick={handleSend} disabled={loading || creatingConversation || (!input.trim() && attachedFiles.length === 0 && selectedFiles.length === 0)} className="shadow-md">
|
|
|
|
{creatingConversation ? "Preparando..." : loading ? "Enviando..." : "Enviar"}
|
|
|
|
{creatingConversation ? "Preparando..." : loading ? "Enviando..." : "Enviar"}
|
|
|
|
</Button>
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
|