This commit is contained in:
Rogelio
2025-10-13 18:16:25 +00:00
parent 739f087cef
commit 325f1ef439
415 changed files with 46870 additions and 0 deletions

View 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>
);
}

View 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}
/>
</>
);
}

View 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>
</>
);
}

View 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>
);
}

View 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);
}

View File

@@ -0,0 +1,4 @@
export * from "./components/FeedbackButton"
export * from "./components/ChatMessage"
export * from "./components/Chat"
export * from "./components/ChatSidebar"

View 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',
]
});