forked from innovacion/Mayacontigo
ic
This commit is contained in:
123
packages/chat-ui/src/components/Chat.tsx
Normal file
123
packages/chat-ui/src/components/Chat.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { FormEvent, useState, useEffect } from "react";
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import { useRef } from "react";
|
||||
|
||||
export { Chat };
|
||||
|
||||
interface Message {
|
||||
user: boolean;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ChatProps {
|
||||
assistant: string;
|
||||
messages: Message[];
|
||||
pushMessage: (message: Message) => void;
|
||||
conversationId: string;
|
||||
setConversationId: (id: string) => void;
|
||||
setAssistantName: (name: string) => void;
|
||||
receivingMsg: boolean;
|
||||
setReceivingMsg: (receiving: boolean) => void;
|
||||
onStartConversation: (user: string, assistant: string) => Promise<string>;
|
||||
sendIcon: string;
|
||||
userAvatar: string;
|
||||
botAvatar: string;
|
||||
onFeedback?: (key: string, rating: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function Chat({
|
||||
assistant,
|
||||
messages,
|
||||
pushMessage,
|
||||
conversationId,
|
||||
setConversationId,
|
||||
setAssistantName,
|
||||
receivingMsg,
|
||||
setReceivingMsg,
|
||||
onStartConversation,
|
||||
sendIcon,
|
||||
userAvatar,
|
||||
botAvatar,
|
||||
onFeedback
|
||||
}: ChatProps) {
|
||||
const [input, setInput] = useState("");
|
||||
const bottomRef = useRef(null);
|
||||
|
||||
async function startConversation() {
|
||||
const newId = await onStartConversation("user", assistant);
|
||||
setConversationId(newId);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setAssistantName(assistant);
|
||||
startConversation();
|
||||
}, []);
|
||||
|
||||
function changeInput(e: FormEvent<HTMLInputElement>) {
|
||||
e.preventDefault();
|
||||
setInput(e.currentTarget.value);
|
||||
}
|
||||
|
||||
async function clearInput(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
// Validar que el input no esté vacío
|
||||
const trimmedInput = input.trim();
|
||||
if (!trimmedInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
pushMessage({ user: true, content: trimmedInput });
|
||||
setInput("");
|
||||
pushMessage({ user: false, content: trimmedInput });
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
// @ts-expect-error idk
|
||||
bottomRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center bg-slate-100 h-screen">
|
||||
<div className="mt-5 w-3/5 flex-1 overflow-y-auto scrollbar min-h-0">
|
||||
{messages.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={index}
|
||||
isUser={message.user}
|
||||
content={message.content}
|
||||
event={scrollToBottom}
|
||||
conversationId={conversationId}
|
||||
setReceivingMsg={setReceivingMsg}
|
||||
userAvatar={userAvatar}
|
||||
botAvatar={botAvatar}
|
||||
onFeedback={onFeedback}
|
||||
/>
|
||||
))}
|
||||
<div ref={bottomRef}></div>
|
||||
</div>
|
||||
<form
|
||||
className="flex-shrink-0 ml-5 my-5 flex w-3/4 items-center justify-center mr-5"
|
||||
onSubmit={clearInput}
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={changeInput}
|
||||
disabled={receivingMsg}
|
||||
placeholder="¡Pregúntame algo!"
|
||||
className="input input-bordered focus:input-primary w-[90%] p-7 rounded-3xl"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn-error ml-4 hover:border-red-200 hover:opacity-80 ${
|
||||
!input.trim() ? "opacity-50" : ""
|
||||
}`}
|
||||
disabled={receivingMsg || !input.trim()}
|
||||
>
|
||||
<img src={sendIcon} alt="Send" className="h-14 w-14" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
393
packages/chat-ui/src/components/ChatMessage.tsx
Normal file
393
packages/chat-ui/src/components/ChatMessage.tsx
Normal file
@@ -0,0 +1,393 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { FeedbackButton } from "./FeedbackButton";
|
||||
import { PDFModal } from "./PDFModal";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import { SSE } from "sse.js";
|
||||
|
||||
export { ChatMessage };
|
||||
|
||||
interface ChatMessageProps {
|
||||
isUser: boolean;
|
||||
content: string;
|
||||
event: CallableFunction;
|
||||
conversationId: string;
|
||||
setReceivingMsg: (receiving: boolean) => void;
|
||||
userAvatar: string;
|
||||
botAvatar: string;
|
||||
onFeedback?: (key: string, rating: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function ChatMessage({
|
||||
isUser,
|
||||
content,
|
||||
event,
|
||||
conversationId,
|
||||
setReceivingMsg,
|
||||
userAvatar,
|
||||
botAvatar,
|
||||
onFeedback
|
||||
}: ChatMessageProps) {
|
||||
const [buff, setBuff] = useState("");
|
||||
const [responseId, setResponseId] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const [acceptFeedback, setAcceptFeedback] = useState(false);
|
||||
const [streamIndex, setStreamIndex] = useState(0);
|
||||
const [fullResponse, setFullResponse] = useState("");
|
||||
const [pendingReferences, setPendingReferences] = useState<Array<any>>([]);
|
||||
const [streamingComplete, setStreamingComplete] = useState(false);
|
||||
|
||||
const [pdfModal, setPdfModal] = useState({
|
||||
isOpen: false,
|
||||
filename: '',
|
||||
page: undefined as number | undefined
|
||||
});
|
||||
|
||||
|
||||
|
||||
const closePdfModal = () => {
|
||||
setPdfModal({
|
||||
isOpen: false,
|
||||
filename: '',
|
||||
page: undefined
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && pdfModal.isOpen) {
|
||||
closePdfModal();
|
||||
}
|
||||
};
|
||||
|
||||
if (pdfModal.isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
}, [pdfModal.isOpen]);
|
||||
|
||||
const nextImage = () => {
|
||||
if (currentImageIndex < images.length - 1) {
|
||||
setCurrentImageIndex((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevImage = () => {
|
||||
if (currentImageIndex > 0) {
|
||||
setCurrentImageIndex((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (fullResponse && streamIndex < fullResponse.length) {
|
||||
setLoading(false);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setBuff((prev) => prev + fullResponse[streamIndex]);
|
||||
setStreamIndex((prev) => prev + 1);
|
||||
event();
|
||||
}, 3);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
} else if (fullResponse && streamIndex === fullResponse.length) {
|
||||
setReceivingMsg(false);
|
||||
setStreamingComplete(true);
|
||||
setBuff(fullResponse);
|
||||
}
|
||||
}, [fullResponse, streamIndex]);
|
||||
|
||||
async function getStream() {
|
||||
const payload = JSON.stringify({
|
||||
prompt: content,
|
||||
conversation_id: conversationId,
|
||||
});
|
||||
|
||||
const url = "/api/v1/message?stream=True";
|
||||
const eventSource = new SSE(url, {
|
||||
withCredentials: true,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
payload: payload,
|
||||
});
|
||||
|
||||
eventSource.onmessage = async (event) => {
|
||||
console.log(event.data);
|
||||
const ResponseChunk = JSON.parse(event.data);
|
||||
|
||||
if (ResponseChunk["type"] === "text") {
|
||||
const content = ResponseChunk["content"];
|
||||
setFullResponse((prev) => prev + content);
|
||||
} else if (ResponseChunk["type"] === "reference") {
|
||||
setPendingReferences(ResponseChunk["content"]);
|
||||
} else if (ResponseChunk["type"] === "end") {
|
||||
setResponseId(ResponseChunk["content"]);
|
||||
eventSource.close();
|
||||
} else if (ResponseChunk["type"] === "image") {
|
||||
const newImages = ResponseChunk.content.slice(0, 3);
|
||||
setImages((prev) => {
|
||||
const combinedImages = [...prev, ...newImages];
|
||||
return combinedImages.slice(0, 3);
|
||||
});
|
||||
} else if (ResponseChunk["type"] == "tool") {
|
||||
setAcceptFeedback(true);
|
||||
} else if (ResponseChunk["type"] === "error") {
|
||||
setFullResponse((prev) => prev + "\n\n" + ResponseChunk["content"]);
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
eventSource.onerror = async (e) => {
|
||||
console.log("error" + e);
|
||||
setReceivingMsg(false);
|
||||
setLoading(false);
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUser) {
|
||||
setLoading(true);
|
||||
setReceivingMsg(true);
|
||||
getStream();
|
||||
} else {
|
||||
setBuff(content);
|
||||
event();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const Metadata = ({ metadatas }: { metadatas: any[] }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (!metadatas || metadatas.length === 0) return null;
|
||||
|
||||
// Mapeo de archivos a URLs públicas (mismo que en el backend)
|
||||
const PDF_PUBLIC_URLS: { [key: string]: string } = {
|
||||
"Disposiciones de carácter general aplicables a las casas de bolsa.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20casas%20de%20bolsa.pdf",
|
||||
"Disposiciones de carácter general aplicables a las instituciones de crédito.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20instituciones%20de%20cr%C3%A9dito.pdf",
|
||||
"Disposiciones de carácter general aplicables a las sociedades controladoras de grupos financieros y subcontroladoras que regulan las materias que corresponden de manera conjunta a las Comisio.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20sociedades%20controladoras%20de%20grupos%20financieros%20y%20subcontroladoras%20que%20regulan%20las%20materias%20que%20corresponden%20de%20manera%20conjunta%20a%20las%20Comisiones%20Nacionales%20Supervisoras.pdf",
|
||||
"Disposiciones de carácter general aplicables a los fondos de inversión y a las personas que les prestan servicios.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20los%20fondos%20de%20inversi%C3%B3n%20y%20a%20las%20personas%20que%20les%20prestan%20servicios.pdf",
|
||||
"Ley para la Transparencia y Ordenamiento de los Servicios Financieros.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20para%20la%20Transparencia%20y%20Ordenamiento%20de%20los%20Servicios%20Financieros.pdf",
|
||||
"circular_servicios_de_inversion.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20entidades%20financieras%20y%20dem%C3%A1s%20personas%20que%20proporcionen%20servicios%20de.pdf",
|
||||
"circular_unica_de_auditores_externos.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20que%20establecen%20los%20requisitos%20que%20deber%C3%A1n%20cumplir%20los%20auditores%20y%20otros%20profesionales%20que.pdf",
|
||||
"ley_de_instituciones_de_Credito.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20de%20Instituciones%20de%20Cr%C3%A9dito.pdf",
|
||||
"circular_13_2007.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2007/cobro-intereses-por-adelantad.html",
|
||||
"circular_13_2011.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2011/%7BBA4CBC28-A468-16C9-6F17-9EA9D7B03318%7D.pdf",
|
||||
"circular_14_2007.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-14-2007/%7BFB726B6B-D523-56F5-F9B1-BE5B3B95A504%7D.pdf",
|
||||
"circular_17_2014.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-17-2014/%7BF36CEF03-9441-2DBE-082C-0DF274903782%7D.pdf",
|
||||
"circular_1_2005.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-1-2005/%7B5CA4BA75-FEA8-199C-F129-E8E6A73E84F3%7D.pdf",
|
||||
"circular_21_2009.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-21-2009/%7B29285862-EDE0-567A-BAFB-D261406641A3%7D.pdf",
|
||||
"circular_22_2008.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-22-2008/%7BF15C8A26-C92E-BE2B-9344-51EDAA3C9B68%7D.pdf",
|
||||
"circular_22_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-22-2010/%7B0D531F59-1001-4D67-D7B4-D5854DD07A58%7D.pdf",
|
||||
"circular_27_2008.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-27-2008/%7BBC4333FE-070F-E727-199E-CA6BCF2CBA66%7D.pdf",
|
||||
"circular_34_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-34-2010/%7B0C55B906-6DB4-6B88-FED0-67987E9FB3CC%7D.pdf",
|
||||
"circular_35_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-35-2010/%7B74C5641C-ED98-53C7-F08B-A3C7BAE0D480%7D.pdf",
|
||||
"circular_36_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-36-2010/%7B26C55DE6-CC3A-3368-34FC-1A6C50B11130%7D.pdf",
|
||||
"circular_3_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-3-2012/%7B4E0281A4-7AD8-1462-BC79-7F2925F3171D%7D.pdf",
|
||||
"circular_4_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-4-2012/%7B97C62974-1C94-19AE-AB5A-D0D949A36247%7D.pdf",
|
||||
"circular_unica_de_condusef.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
|
||||
"ley_para_regular_las_sociedades_de_informacion_crediticia.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
|
||||
"ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf",
|
||||
"reglamento_de_la_ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/regley/Reg_LFPDPPP.pdf",
|
||||
"Modificaciones Recursos Procedencia Ilícita jul 25 PLD.pdf": "https://gfbanorte.sharepoint.com/:w:/r/sites/Formatosyplantillas/Documentos%20compartidos/Otros/Modificaciones%20Recursos%20Procedencia%20Il%C3%ADcita%20jul%2025%20PLD.docx?d=w6a941e9e2c26403ea41c12de35536516&csf=1&web=1&e=EHtc9b",
|
||||
};
|
||||
|
||||
const handlePdfClick = (fileName: string, page?: number) => {
|
||||
const publicUrl = PDF_PUBLIC_URLS[fileName];
|
||||
|
||||
if (publicUrl) {
|
||||
// Abrir PDF público directamente
|
||||
let finalUrl = publicUrl;
|
||||
if (page) {
|
||||
finalUrl += `#page=${page}`;
|
||||
}
|
||||
window.open(finalUrl, '_blank');
|
||||
} else {
|
||||
// Fallback: usar tu modal para PDFs locales
|
||||
setPdfModal({
|
||||
isOpen: true,
|
||||
filename: fileName,
|
||||
page: page
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className="font-medium text-gray-700 mr-2">Fuentes</span>
|
||||
<span className="text-gray-500">
|
||||
{isExpanded ? '▲' : '▼'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-3 border border-gray-300 rounded-lg bg-gray-50 p-4">
|
||||
<div className="space-y-2">
|
||||
{metadatas.map((metadata, index) => {
|
||||
const fileName = metadata.file_name || metadata.file || 'Documento';
|
||||
const page = metadata.page;
|
||||
const displayText = page ? `${fileName} - Página ${page}` : fileName;
|
||||
const isPublic = PDF_PUBLIC_URLS[fileName];
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-start space-x-2 text-sm">
|
||||
<span className="text-gray-400 mt-1">
|
||||
{isPublic ? '🌐' : '📄'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handlePdfClick(fileName, page)}
|
||||
className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer text-left flex items-center"
|
||||
title={isPublic ? 'Documento público - Se abrirá en nueva pestaña' : 'Documento local'}
|
||||
>
|
||||
{displayText}
|
||||
{isPublic && (
|
||||
<span className="ml-1 text-xs text-gray-500">↗</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageViewer = () => {
|
||||
if (images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-5 space-y-4">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={images[currentImageIndex]}
|
||||
alt={`Generated image ${currentImageIndex + 1}`}
|
||||
className="w-full h-auto rounded-lg"
|
||||
/>
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<button
|
||||
onClick={prevImage}
|
||||
disabled={currentImageIndex === 0}
|
||||
className={`px-4 py-2 rounded ${
|
||||
currentImageIndex === 0
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<button
|
||||
onClick={nextImage}
|
||||
disabled={currentImageIndex === images.length - 1}
|
||||
className={`px-4 py-2 rounded ${
|
||||
currentImageIndex === images.length - 1
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-gray-600 mt-2 block text-center">
|
||||
Imagen {currentImageIndex + 1} de {images.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isUser ? (
|
||||
<div className="m-5 mr-5 flex flex-row-reverse items-start space-x-4">
|
||||
<div className="avatar placeholder mx-4 w-14 -mt-1">
|
||||
<img src={userAvatar} alt="user avatar icon" />
|
||||
</div>
|
||||
<div className="inline-block max-w-[82%] 2xl:max-w-[88%]">
|
||||
<div className="border border-slate-400 rounded-3xl bg-white p-4 text-gray-500">
|
||||
<div className="whitespace-pre-wrap text-left">
|
||||
{loading && (
|
||||
<span className="loading loading-dots loading-md"></span>
|
||||
)}
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{buff}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="m-5 flex items-start space-x-4 w-full">
|
||||
<div className="avatar placeholder mx-4 w-14 -mt-1 mr-2">
|
||||
<img src={botAvatar} alt="bot avatar icon" />
|
||||
</div>
|
||||
<div className="inline-block max-w-[82%] 2xl:max-w-[88%]">
|
||||
<div className="border-2 border-red-500 rounded-3xl bg-white p-4 text-gray-500 pl-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="text-left w-full">
|
||||
{loading && (
|
||||
<span className="loading loading-dots loading-md"></span>
|
||||
)}
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={{
|
||||
h1: ({ ...props }) => (
|
||||
<h1 className="text-2xl font-bold mb-4" {...props} />
|
||||
),
|
||||
h2: ({ ...props }) => (
|
||||
<h2 className="text-xl font-bold mb-3" {...props} />
|
||||
),
|
||||
h3: ({ ...props }) => (
|
||||
<h3 className="text-lg font-bold mb-2" {...props} />
|
||||
),
|
||||
p: ({ ...props }) => <p className="mb-4" {...props} />,
|
||||
ul: ({ ...props }) => (
|
||||
<ul
|
||||
className="list-disc pl-6 mb-4 space-y-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ ...props }) => (
|
||||
<ol
|
||||
className="list-decimal pl-6 mb-4 space-y-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||
a: ({ ...props }) => (
|
||||
<a
|
||||
className="text-blue-600 underline hover:text-blue-800"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
strong: ({ ...props }) => (
|
||||
<strong className="font-bold" {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{buff}
|
||||
</Markdown>
|
||||
{pendingReferences.length > 0 && <Metadata metadatas={pendingReferences} />}
|
||||
<ImageViewer />
|
||||
</div>
|
||||
{streamingComplete && acceptFeedback && onFeedback && (
|
||||
<FeedbackButton messageKey={responseId} onFeedback={onFeedback} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PDFModal
|
||||
isOpen={pdfModal.isOpen}
|
||||
onClose={closePdfModal}
|
||||
filename={pdfModal.filename}
|
||||
page={pdfModal.page}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
49
packages/chat-ui/src/components/ChatSidebar.tsx
Normal file
49
packages/chat-ui/src/components/ChatSidebar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
export { ChatSidebar };
|
||||
|
||||
interface ChatSidebarProps {
|
||||
assistant: string;
|
||||
logoSrc: string;
|
||||
sidebarImageSrc: string;
|
||||
assistantAvatarSrc: string;
|
||||
}
|
||||
|
||||
function ChatSidebar({ assistant, logoSrc, sidebarImageSrc, assistantAvatarSrc }: ChatSidebarProps) {
|
||||
return (
|
||||
<>
|
||||
<nav className="bg-[#1b0103] shadow-lg min-h-[641px] min-w-[250px] py-6 px-6 font-[sans-serif] flex flex-col overflow-auto w-[272px] 2xl:h-screen">
|
||||
<div className="flex flex-wrap items-center cursor-pointer">
|
||||
<div className="relative w-full mb-12 ">
|
||||
<div className="mx-5 w-3/4 -inset-3mt-2">
|
||||
<a href="/">
|
||||
<img className="h-10" src={logoSrc} alt="Logo" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative items-center text-center mx-auto -mt-5">
|
||||
<img src={assistantAvatarSrc} className="w-24 h-24 border-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-2">
|
||||
<h2 className="text-xl font-extrabold text-gray-300 uppercase">
|
||||
{assistant}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3 flex-1 mt-5 mb-10 pl-5">
|
||||
</ul>
|
||||
<ul className="w-full">
|
||||
<li className="w-full">
|
||||
<a
|
||||
href=""
|
||||
className="text-gray-300 hover:text-white text-base flex items-center rounded-md"
|
||||
>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="w-[272px] -p-6 -m-6">
|
||||
<img src={sidebarImageSrc} alt="Sidebar Image" className="w-[272px]" />
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
69
packages/chat-ui/src/components/FeedbackButton.tsx
Normal file
69
packages/chat-ui/src/components/FeedbackButton.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Icon } from "@iconify-icon/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export { FeedbackButton };
|
||||
|
||||
interface FeedbackButtonProps {
|
||||
messageKey: string;
|
||||
onFeedback: (key: string, rating: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function FeedbackButton({ messageKey, onFeedback }: FeedbackButtonProps) {
|
||||
const [likeStyle, setLikeStyle] = useState({ fontSize: "18px" });
|
||||
const [dislikeStyle, setDislikeStyle] = useState({ fontSize: "18px" });
|
||||
const [liked, setLiked] = useState(false);
|
||||
const [disliked, setDisliked] = useState(false);
|
||||
|
||||
async function sendFeedback(rating: string) {
|
||||
await onFeedback(messageKey, rating);
|
||||
console.log("Sent feedback:");
|
||||
console.log(rating);
|
||||
}
|
||||
|
||||
function Like() {
|
||||
if (!liked) {
|
||||
const colorStyle = { color: "green" };
|
||||
setLikeStyle({ fontSize: "18px", ...colorStyle });
|
||||
setLiked(true);
|
||||
|
||||
setDislikeStyle({ fontSize: "18px" });
|
||||
setDisliked(false);
|
||||
|
||||
sendFeedback("Good");
|
||||
} else {
|
||||
setLikeStyle({ fontSize: "18px" });
|
||||
setLiked(false);
|
||||
|
||||
sendFeedback("None");
|
||||
}
|
||||
}
|
||||
|
||||
function Dislike() {
|
||||
if (!disliked) {
|
||||
const colorStyle = { color: "red" };
|
||||
setDislikeStyle({ fontSize: "18px", ...colorStyle });
|
||||
setDisliked(true);
|
||||
|
||||
setLikeStyle({ fontSize: "18px" });
|
||||
setLiked(false);
|
||||
|
||||
sendFeedback("Bad");
|
||||
} else {
|
||||
setDislikeStyle({ fontSize: "18px" });
|
||||
setDisliked(false);
|
||||
|
||||
sendFeedback("None");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-end space-x-2 mt-3">
|
||||
<button className="btn btn-xs btn-ghost" onClick={Like}>
|
||||
<Icon style={likeStyle} icon="iconamoon:like" />
|
||||
</button>
|
||||
<button className="btn btn-xs btn-ghost" onClick={Dislike}>
|
||||
<Icon style={dislikeStyle} flip="horizontal" icon="iconamoon:dislike" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
381
packages/chat-ui/src/components/PDFModal.tsx
Normal file
381
packages/chat-ui/src/components/PDFModal.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
interface PDFModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
filename: string;
|
||||
page?: number;
|
||||
}
|
||||
|
||||
// Mapeo COMPLETO con URLs públicas directas - IDÉNTICO AL BACKEND
|
||||
const PDF_REFERENCES: Record<string, string> = {
|
||||
// Disposiciones de CNBV
|
||||
"Disposiciones de carácter general aplicables a las casas de bolsa.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20casas%20de%20bolsa.pdf",
|
||||
"Disposiciones de carácter general aplicables a las instituciones de crédito.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20instituciones%20de%20cr%C3%A9dito.pdf",
|
||||
"Disposiciones de carácter general aplicables a las sociedades controladoras de grupos financieros y subcontroladoras que regulan las materias que corresponden de manera conjunta a las Comisio.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20sociedades%20controladoras%20de%20grupos%20financieros%20y%20subcontroladoras%20que%20regulan%20las%20materias%20que%20corresponden%20de%20manera%20conjunta%20a%20las%20Comisiones%20Nacionales%20Supervisoras.pdf",
|
||||
"Disposiciones de carácter general aplicables a los fondos de inversión y a las personas que les prestan servicios.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20los%20fondos%20de%20inversi%C3%B3n%20y%20a%20las%20personas%20que%20les%20prestan%20servicios.pdf",
|
||||
"Ley para la Transparencia y Ordenamiento de los Servicios Financieros.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20para%20la%20Transparencia%20y%20Ordenamiento%20de%20los%20Servicios%20Financieros.pdf",
|
||||
|
||||
// Circulares CNBV adicionales
|
||||
"circular_servicios_de_inversion.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20entidades%20financieras%20y%20dem%C3%A1s%20personas%20que%20proporcionen%20servicios%20de.pdf",
|
||||
"circular_unica_de_auditores_externos.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20que%20establecen%20los%20requisitos%20que%20deber%C3%A1n%20cumplir%20los%20auditores%20y%20otros%20profesionales%20que.pdf",
|
||||
"ley_de_instituciones_de_Credito.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20de%20Instituciones%20de%20Cr%C3%A9dito.pdf",
|
||||
|
||||
// Circulares de Banxico
|
||||
"circular_13_2007.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2007/cobro-intereses-por-adelantad.html",
|
||||
"circular_13_2011.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2011/%7BBA4CBC28-A468-16C9-6F17-9EA9D7B03318%7D.pdf",
|
||||
"circular_14_2007.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-14-2007/%7BFB726B6B-D523-56F5-F9B1-BE5B3B95A504%7D.pdf",
|
||||
"circular_17_2014.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-17-2014/%7BF36CEF03-9441-2DBE-082C-0DF274903782%7D.pdf",
|
||||
"circular_1_2005.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-1-2005/%7B5CA4BA75-FEA8-199C-F129-E8E6A73E84F3%7D.pdf",
|
||||
"circular_21_2009.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-21-2009/%7B29285862-EDE0-567A-BAFB-D261406641A3%7D.pdf",
|
||||
"circular_22_2008.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-22-2008/%7BF15C8A26-C92E-BE2B-9344-51EDAA3C9B68%7D.pdf",
|
||||
"circular_22_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-22-2010/%7B0D531F59-1001-4D67-D7B4-D5854DD07A58%7D.pdf",
|
||||
"circular_27_2008.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-27-2008/%7BBC4333FE-070F-E727-199E-CA6BCF2CBA66%7D.pdf",
|
||||
"circular_34_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-34-2010/%7B0C55B906-6DB4-6B88-FED0-67987E9FB3CC%7D.pdf",
|
||||
"circular_35_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-35-2010/%7B74C5641C-ED98-53C7-F08B-A3C7BAE0D480%7D.pdf",
|
||||
"circular_36_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-36-2010/%7B26C55DE6-CC3A-3368-34FC-1A6C50B11130%7D.pdf",
|
||||
"circular_3_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-3-2012/%7B4E0281A4-7AD8-1462-BC79-7F2925F3171D%7D.pdf",
|
||||
"circular_4_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-4-2012/%7B97C62974-1C94-19AE-AB5A-D0D949A36247%7D.pdf",
|
||||
|
||||
// CONDUSEF
|
||||
"circular_unica_de_condusef.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
|
||||
"ley_para_regular_las_sociedades_de_informacion_crediticia.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
|
||||
|
||||
// Leyes federales
|
||||
"ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf",
|
||||
"reglamento_de_la_ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/regley/Reg_LFPDPPP.pdf",
|
||||
|
||||
// SharePoint Banorte
|
||||
"Modificaciones Recursos Procedencia Ilícita jul 25 PLD.pdf": "https://gfbanorte.sharepoint.com/:w:/r/sites/Formatosyplantillas/Documentos%20compartidos/Otros/Modificaciones%20Recursos%20Procedencia%20Il%C3%ADcita%20jul%2025%20PLD.docx?d=w6a941e9e2c26403ea41c12de35536516&csf=1&web=1&e=EHtc9b",
|
||||
};
|
||||
|
||||
// Función para determinar si es una URL externa
|
||||
const isExternalUrl = (url: string): boolean => {
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
};
|
||||
|
||||
// Función para resolver la URL del PDF - BYPASS completo del backend local
|
||||
const resolvePdfUrl = (filename: string): { directUrl: string; viewerUrl: string; isExternal: boolean } => {
|
||||
console.log(`Resolviendo PDF para: "${filename}"`);
|
||||
|
||||
// Buscar directamente en el mapeo de URLs públicas
|
||||
const publicUrl = PDF_REFERENCES[filename];
|
||||
|
||||
if (!publicUrl) {
|
||||
console.warn(`No se encontró URL pública para: "${filename}"`);
|
||||
return {
|
||||
directUrl: '',
|
||||
viewerUrl: '',
|
||||
isExternal: false
|
||||
};
|
||||
}
|
||||
|
||||
const external = isExternalUrl(publicUrl);
|
||||
|
||||
if (external) {
|
||||
console.log(`✅ BYPASS COMPLETO - URL pública directa: ${publicUrl}`);
|
||||
// PDF.js viewer apunta DIRECTAMENTE a la URL pública, sin pasar por tu API
|
||||
return {
|
||||
directUrl: publicUrl,
|
||||
viewerUrl: `https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent(publicUrl)}`,
|
||||
isExternal: true
|
||||
};
|
||||
} else {
|
||||
console.warn(`URL no es externa: ${publicUrl}`);
|
||||
return {
|
||||
directUrl: '',
|
||||
viewerUrl: '',
|
||||
isExternal: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function PDFModal({ isOpen, onClose, filename, page }: PDFModalProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
|
||||
// Resolver URLs del PDF
|
||||
const { directUrl: directPdfUrl, viewerUrl: pdfUrl, isExternal } = resolvePdfUrl(filename);
|
||||
|
||||
// Crear un elemento para el portal al montar el componente
|
||||
useEffect(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
let element = document.getElementById('pdf-modal-portal');
|
||||
|
||||
if (!element) {
|
||||
element = document.createElement('div');
|
||||
element.id = 'pdf-modal-portal';
|
||||
element.style.position = 'fixed';
|
||||
element.style.top = '0';
|
||||
element.style.left = '0';
|
||||
element.style.width = '100%';
|
||||
element.style.height = '100%';
|
||||
element.style.zIndex = '99999';
|
||||
element.style.pointerEvents = 'none';
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
|
||||
setPortalElement(element);
|
||||
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
if (element && !isOpen && document.body.contains(element)) {
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
document.body.style.overflow = 'auto';
|
||||
setPdfError(false);
|
||||
setIsLoading(true);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Manejar tecla ESC
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !portalElement) return null;
|
||||
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false);
|
||||
setPdfError(false);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setIsLoading(false);
|
||||
setPdfError(true);
|
||||
console.error(`Error loading PDF: ${filename}`, { directPdfUrl, pdfUrl, isExternal });
|
||||
};
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setPdfError(false);
|
||||
setIsLoading(true);
|
||||
const iframe = document.querySelector(`iframe[title="PDF Viewer - ${filename}"]`) as HTMLIFrameElement;
|
||||
if (iframe) {
|
||||
iframe.src = pdfUrl;
|
||||
}
|
||||
};
|
||||
|
||||
// Si no hay URL pública, mostrar mensaje de error
|
||||
if (!directPdfUrl) {
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4"
|
||||
style={{
|
||||
zIndex: 99999,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div
|
||||
className="bg-white flex flex-col p-8 rounded-xl max-w-lg w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-6xl mb-4">⚠️</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">PDF no disponible</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
No se encontró URL pública para: <strong>{filename}</strong>
|
||||
</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-6 py-2 bg-gray-600 text-white font-medium rounded-lg hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
portalElement
|
||||
);
|
||||
}
|
||||
|
||||
const modalContent = (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4"
|
||||
style={{
|
||||
zIndex: 99999,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div
|
||||
className="bg-white flex flex-col"
|
||||
style={{
|
||||
width: '85vw',
|
||||
height: '90vh',
|
||||
maxWidth: 'none',
|
||||
maxHeight: 'none',
|
||||
borderRadius: '16px',
|
||||
border: '3px solid #000000',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05)',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-gray-50 to-gray-100 border-b-2 border-gray-200">
|
||||
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
<div className="ml-4 min-w-0 flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
||||
📄 {filename}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-4 text-xs text-gray-500 mt-1">
|
||||
{page && <span>Página {page}</span>}
|
||||
<span className="px-2 py-1 rounded-full bg-blue-100 text-blue-700">
|
||||
Público
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded-full bg-green-100 text-green-700">
|
||||
Directo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-red-500 text-2xl font-bold w-10 h-10 flex items-center justify-center hover:bg-red-50 rounded-full transition-all duration-200 flex-shrink-0 ml-4"
|
||||
aria-label="Cerrar modal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* PDF Content */}
|
||||
<div className="flex-1 relative bg-gray-50">
|
||||
{isLoading && !pdfError && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white z-10">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-16 w-16 border-b-4 border-blue-600"></div>
|
||||
<p className="mt-6 text-gray-600 text-lg font-medium">Cargando PDF...</p>
|
||||
<p className="mt-2 text-gray-400 text-sm max-w-md break-words">{filename}</p>
|
||||
<p className="mt-1 text-xs text-gray-300">Fuente pública</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pdfError && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white z-10 p-8">
|
||||
<div className="text-center max-w-2xl">
|
||||
<div className="text-6xl mb-4">❌</div>
|
||||
<p className="text-red-600 text-lg font-medium mb-2">Error al cargar el PDF</p>
|
||||
<p className="text-gray-600 mb-4">
|
||||
No se pudo cargar: <strong className="break-words">{filename}</strong>
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg mb-4 text-left text-xs">
|
||||
<p className="font-medium text-gray-700 mb-2">Información:</p>
|
||||
<p className="text-gray-600 break-all mb-1">
|
||||
<strong>URL:</strong> {directPdfUrl}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
<strong>Tipo:</strong> Fuente pública
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-x-2">
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
<a
|
||||
href={directPdfUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Abrir en nueva pestaña
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!pdfError && (
|
||||
<iframe
|
||||
src={pdfUrl}
|
||||
className="w-full h-full border-0"
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
title={`PDF Viewer - ${filename}`}
|
||||
style={{
|
||||
minHeight: '100%',
|
||||
borderRadius: '0'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-gray-50 to-gray-100 border-t-2 border-gray-200">
|
||||
<div className="flex items-center space-x-4">
|
||||
<a
|
||||
href={directPdfUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center px-4 py-2 bg-black text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200"
|
||||
>
|
||||
🔗 Abrir en nueva pestaña
|
||||
</a>
|
||||
<span className="text-xs text-gray-500">
|
||||
ESC para cerrar • PDF.js Viewer • Fuente pública
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-6 py-2 bg-gray-600 text-white font-medium rounded-lg hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return createPortal(modalContent, portalElement);
|
||||
}
|
||||
4
packages/chat-ui/src/index.ts
Normal file
4
packages/chat-ui/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./components/FeedbackButton"
|
||||
export * from "./components/ChatMessage"
|
||||
export * from "./components/Chat"
|
||||
export * from "./components/ChatSidebar"
|
||||
166
packages/chat-ui/src/tailwind-plugin.js
Normal file
166
packages/chat-ui/src/tailwind-plugin.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import plugin from 'tailwindcss/plugin';
|
||||
|
||||
export default plugin(function({ addUtilities }) {
|
||||
// Add any custom utilities if needed
|
||||
}, {
|
||||
// Safelist all classes used in the chat-ui package
|
||||
safelist: [
|
||||
// Layout and flexbox
|
||||
'flex',
|
||||
'flex-1',
|
||||
'flex-col',
|
||||
'flex-row-reverse',
|
||||
'flex-wrap',
|
||||
'flex-shrink-0',
|
||||
'items-center',
|
||||
'items-start',
|
||||
'justify-center',
|
||||
'justify-between',
|
||||
'justify-end',
|
||||
'space-x-2',
|
||||
'space-x-4',
|
||||
'space-y-2',
|
||||
'space-y-3',
|
||||
'space-y-4',
|
||||
|
||||
// Positioning
|
||||
'relative',
|
||||
'absolute',
|
||||
'inset-3mt-2',
|
||||
'inline-block',
|
||||
|
||||
// Margins and padding
|
||||
'm-5',
|
||||
'mr-5',
|
||||
'ml-4',
|
||||
'ml-5',
|
||||
'my-5',
|
||||
'mx-4',
|
||||
'mx-5',
|
||||
'mx-auto',
|
||||
'mt-2',
|
||||
'mt-3',
|
||||
'mt-4',
|
||||
'mt-5',
|
||||
'-mt-1',
|
||||
'-mt-5',
|
||||
'mb-1',
|
||||
'mb-3',
|
||||
'mb-4',
|
||||
'mb-10',
|
||||
'mb-12',
|
||||
'mr-2',
|
||||
'p-4',
|
||||
'p-7',
|
||||
'px-4',
|
||||
'px-6',
|
||||
'py-2',
|
||||
'py-6',
|
||||
'pl-5',
|
||||
'pl-6',
|
||||
'-p-6',
|
||||
'-m-6',
|
||||
|
||||
// Width and height
|
||||
'w-full',
|
||||
'w-3/4',
|
||||
'w-3/5',
|
||||
'w-14',
|
||||
'w-24',
|
||||
'w-[90%]',
|
||||
'w-[272px]',
|
||||
'h-auto',
|
||||
'h-10',
|
||||
'h-14',
|
||||
'h-24',
|
||||
'h-screen',
|
||||
'min-h-0',
|
||||
'min-h-[641px]',
|
||||
'min-w-[250px]',
|
||||
'max-w-[82%]',
|
||||
|
||||
// Colors and backgrounds
|
||||
'bg-slate-100',
|
||||
'bg-white',
|
||||
'bg-[#1b0103]',
|
||||
'text-gray-300',
|
||||
'text-gray-500',
|
||||
'text-gray-600',
|
||||
'text-gray-700',
|
||||
'text-gray-400',
|
||||
'text-blue-600',
|
||||
'text-blue-700',
|
||||
'text-blue-800',
|
||||
'text-white',
|
||||
'text-base',
|
||||
'text-sm',
|
||||
'text-lg',
|
||||
'text-xl',
|
||||
'text-2xl',
|
||||
'border-white',
|
||||
'border-slate-400',
|
||||
'border-red-500',
|
||||
'border-2',
|
||||
'border',
|
||||
'hover:text-white',
|
||||
'hover:text-blue-800',
|
||||
'hover:bg-gray-100',
|
||||
'hover:border-red-200',
|
||||
'hover:opacity-80',
|
||||
|
||||
// Typography
|
||||
'font-bold',
|
||||
'font-extrabold',
|
||||
'font-[sans-serif]',
|
||||
'text-left',
|
||||
'text-center',
|
||||
'uppercase',
|
||||
'underline',
|
||||
'whitespace-pre-wrap',
|
||||
'list-disc',
|
||||
'list-decimal',
|
||||
|
||||
// Borders and rounding
|
||||
'rounded-lg',
|
||||
'rounded-md',
|
||||
'rounded-3xl',
|
||||
'rounded',
|
||||
|
||||
// Shadows and effects
|
||||
'shadow-lg',
|
||||
'opacity-50',
|
||||
|
||||
// Scrolling
|
||||
'overflow-auto',
|
||||
'overflow-y-auto',
|
||||
'scrollbar',
|
||||
|
||||
// Cursors
|
||||
'cursor-pointer',
|
||||
'cursor-not-allowed',
|
||||
|
||||
// Display
|
||||
'block',
|
||||
|
||||
// DaisyUI components
|
||||
'btn',
|
||||
'btn-xs',
|
||||
'btn-ghost',
|
||||
'btn-error',
|
||||
'input',
|
||||
'input-bordered',
|
||||
'focus:input-primary',
|
||||
'avatar',
|
||||
'placeholder',
|
||||
'loading',
|
||||
'loading-dots',
|
||||
'loading-md',
|
||||
|
||||
// Responsive variants
|
||||
'2xl:max-w-[88%]',
|
||||
'2xl:h-screen',
|
||||
|
||||
// Custom negative margins
|
||||
'-inset-3mt-2',
|
||||
]
|
||||
});
|
||||
Reference in New Issue
Block a user