|
|
import streamlit as st |
|
|
import os |
|
|
import requests |
|
|
import base64 |
|
|
from typing import List, Dict |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Biblioteca de Res煤menes de Libros", |
|
|
layout="wide", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
|
|
|
st.image('Bookbot.png', use_container_width=True) |
|
|
|
|
|
|
|
|
def get_image_base64(image_path): |
|
|
"""Carga una imagen y la convierte a base64 para mostrarla""" |
|
|
try: |
|
|
with open(image_path, "rb") as img_file: |
|
|
return base64.b64encode(img_file.read()).decode() |
|
|
except Exception: |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_custom_css(): |
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Forzar tema claro - esta es la parte m谩s importante */ |
|
|
.st-emotion-cache-ue6h4q { |
|
|
color-scheme: light !important; |
|
|
} |
|
|
body { |
|
|
color-scheme: light !important; |
|
|
} |
|
|
|
|
|
/* Estilos para forzar colores de modo claro */ |
|
|
html, body, [class*="css"] { |
|
|
color: #31333F !important; |
|
|
} |
|
|
|
|
|
[data-testid="stAppViewContainer"], |
|
|
[data-testid="stHeader"], |
|
|
[data-testid="stToolbar"] { |
|
|
background-color: #F8F9FA !important; |
|
|
} |
|
|
|
|
|
/* Contenedor principal - ajustes para m贸vil */ |
|
|
[data-testid="stAppViewContainer"] > .main { |
|
|
padding: 1rem !important; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
[data-testid="stAppViewContainer"] > .main { |
|
|
padding: 0.5rem !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* T铆tulo principal */ |
|
|
.main h1 { |
|
|
color: #1E3A8A !important; |
|
|
font-size: 2rem; |
|
|
font-weight: 600; |
|
|
margin-bottom: 1.5rem; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.main h1 { |
|
|
font-size: 1.5rem !important; |
|
|
margin-bottom: 1rem !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Subt铆tulos */ |
|
|
.main h2, .main h3 { |
|
|
color: #1E3A8A !important; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
/* Contenedor de chat */ |
|
|
.chat-container { |
|
|
background-color: white !important; |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
margin-top: 15px; |
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
/* Mensajes del usuario */ |
|
|
.user-message { |
|
|
background-color: #E7F3FF !important; |
|
|
border-radius: 15px 15px 0 15px; |
|
|
padding: 10px 15px; |
|
|
margin: 10px 0; |
|
|
max-width: 85%; |
|
|
align-self: flex-end; |
|
|
margin-left: auto; |
|
|
color: #31333F !important; |
|
|
} |
|
|
|
|
|
/* Mensajes del asistente */ |
|
|
.assistant-message { |
|
|
background-color: #F0F0F0 !important; |
|
|
border-radius: 15px 15px 15px 0; |
|
|
padding: 10px 15px; |
|
|
margin: 10px 0; |
|
|
max-width: 85%; |
|
|
color: #31333F !important; |
|
|
} |
|
|
|
|
|
/* Ajustes para m贸vil de los mensajes */ |
|
|
@media (max-width: 768px) { |
|
|
.user-message, .assistant-message { |
|
|
max-width: 95% !important; |
|
|
padding: 8px 12px !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Campo de entrada de texto */ |
|
|
.stTextInput input, .stTextArea textarea { |
|
|
border-radius: 20px; |
|
|
border: 1px solid #E0E0E0 !important; |
|
|
padding: 10px 15px; |
|
|
font-size: 1rem; |
|
|
background-color: white !important; |
|
|
color: #31333F !important; |
|
|
} |
|
|
|
|
|
/* Selectbox y dropdown */ |
|
|
.stSelectbox div[data-baseweb="select"] div[role="button"], |
|
|
.stMultiselect div[data-baseweb="select"] div[role="button"] { |
|
|
background-color: white !important; |
|
|
color: #31333F !important; |
|
|
border: 1px solid #E0E0E0 !important; |
|
|
} |
|
|
|
|
|
div[role="listbox"] { |
|
|
background-color: white !important; |
|
|
color: #31333F !important; |
|
|
} |
|
|
|
|
|
div[role="option"] { |
|
|
background-color: white !important; |
|
|
color: #31333F !important; |
|
|
} |
|
|
|
|
|
/* Separador */ |
|
|
hr { |
|
|
margin: 1rem 0 !important; |
|
|
border: 0; |
|
|
border-top: 1px solid #E0E0E0 !important; |
|
|
} |
|
|
|
|
|
/* Bot贸n de env铆o */ |
|
|
.stButton button { |
|
|
background-color: #1E3A8A !important; |
|
|
color: white !important; |
|
|
border-radius: 20px; |
|
|
padding: 5px 20px; |
|
|
font-weight: 500; |
|
|
border: none !important; |
|
|
transition: all 0.3s; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
/* Espec铆ficamente para el texto del bot贸n */ |
|
|
.stButton button p, .stButton button span, .stButton button div { |
|
|
color: white !important; |
|
|
} |
|
|
|
|
|
.stButton button:hover { |
|
|
background-color: #2563EB !important; |
|
|
box-shadow: 0 4px 8px rgba(37, 99, 235, 0.2); |
|
|
} |
|
|
|
|
|
/* Mejoras para visualizaci贸n m贸vil */ |
|
|
@media (max-width: 768px) { |
|
|
.stButton button { |
|
|
padding: 5px 15px !important; |
|
|
font-size: 0.9rem !important; |
|
|
} |
|
|
|
|
|
/* Ajustar columnas en m贸vil */ |
|
|
[data-testid="column"] { |
|
|
width: 100% !important; |
|
|
min-width: 100% !important; |
|
|
} |
|
|
|
|
|
/* Ajustar tama帽o de texto en m贸vil */ |
|
|
p, div, span, label { |
|
|
font-size: 0.9rem !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Captions y textos peque帽os */ |
|
|
.stMarkdown caption, .stMarkdown small { |
|
|
color: #4B5563 !important; |
|
|
} |
|
|
|
|
|
/* Asegurar que todos los textos sean oscuros */ |
|
|
p, div, span, label, li, a { |
|
|
color: #31333F !important; |
|
|
} |
|
|
|
|
|
/* Espec铆fico para los mensajes de chat */ |
|
|
div.user-message strong, div.assistant-message strong { |
|
|
color: inherit !important; |
|
|
} |
|
|
|
|
|
/* Deshabilitar el interruptor de tema oscuro */ |
|
|
button[kind="headerNoPadding"] { |
|
|
display: none !important; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
def cargar_resumenes_libros(carpeta_libros: str) -> List[Dict[str, str]]: |
|
|
""" |
|
|
Carga res煤menes de libros desde archivos de texto en una carpeta espec铆fica. |
|
|
|
|
|
Args: |
|
|
carpeta_libros (str): Ruta de la carpeta que contiene los res煤menes de libros. |
|
|
|
|
|
Returns: |
|
|
List[Dict[str, str]]: Lista de diccionarios con informaci贸n de los libros. |
|
|
""" |
|
|
resumenes = [] |
|
|
try: |
|
|
|
|
|
if not os.path.exists(carpeta_libros): |
|
|
st.error(f"La carpeta {carpeta_libros} no existe.") |
|
|
return resumenes |
|
|
|
|
|
|
|
|
archivos = [f for f in os.listdir(carpeta_libros) if f.endswith('.txt')] |
|
|
|
|
|
if not archivos: |
|
|
st.warning("No se encontraron archivos de res煤menes en la carpeta.") |
|
|
return resumenes |
|
|
|
|
|
|
|
|
for archivo in archivos: |
|
|
ruta_archivo = os.path.join(carpeta_libros, archivo) |
|
|
try: |
|
|
with open(ruta_archivo, 'r', encoding='utf-8') as f: |
|
|
|
|
|
titulo = os.path.splitext(archivo)[0] |
|
|
|
|
|
contenido = f.read().strip() |
|
|
|
|
|
|
|
|
breve_resumen = contenido[:638] + "..." if len(contenido) > 638 else contenido |
|
|
|
|
|
|
|
|
resumenes.append({ |
|
|
"titulo": titulo, |
|
|
"resumen": contenido, |
|
|
"breve_resumen": breve_resumen |
|
|
}) |
|
|
except Exception as e: |
|
|
st.error(f"Error al leer el archivo {archivo}: {str(e)}") |
|
|
|
|
|
except Exception as e: |
|
|
st.error(f"Error al cargar los res煤menes: {str(e)}") |
|
|
|
|
|
return resumenes |
|
|
|
|
|
|
|
|
API_URL = "https://api-inference.huggingface.co/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" |
|
|
|
|
|
|
|
|
if 'chat_history' not in st.session_state: |
|
|
st.session_state.chat_history = [] |
|
|
|
|
|
|
|
|
@st.cache_resource |
|
|
def get_huggingface_api_key(): |
|
|
"""Obtener la API key de Hugging Face desde secrets""" |
|
|
try: |
|
|
|
|
|
return st.secrets["HF_TOKEN"] |
|
|
except KeyError: |
|
|
|
|
|
try: |
|
|
return st.secrets["huggingface"]["api_key"] |
|
|
except: |
|
|
return None |
|
|
|
|
|
|
|
|
def query_huggingface(payload): |
|
|
"""Env铆a una solicitud a la API de Hugging Face y retorna la respuesta""" |
|
|
api_key = get_huggingface_api_key() |
|
|
if not api_key: |
|
|
st.error("No se proporcion贸 una API key de Hugging Face.") |
|
|
return None |
|
|
|
|
|
headers = {"Authorization": f"Bearer {api_key}"} |
|
|
try: |
|
|
|
|
|
response = requests.post(API_URL, headers=headers, json=payload, timeout=60, verify=False) |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
except requests.exceptions.Timeout: |
|
|
st.error("La solicitud a la API de Hugging Face ha excedido el tiempo de espera.") |
|
|
return None |
|
|
except requests.exceptions.HTTPError as e: |
|
|
st.error(f"Error HTTP: {e.response.status_code} - {e.response.text}") |
|
|
return None |
|
|
except Exception as e: |
|
|
st.error(f"Error al comunicarse con la API de Hugging Face: {str(e)}") |
|
|
return None |
|
|
|
|
|
|
|
|
def main(): |
|
|
|
|
|
apply_custom_css() |
|
|
|
|
|
|
|
|
carpeta_libros = "libros" |
|
|
resumenes = cargar_resumenes_libros(carpeta_libros) |
|
|
|
|
|
|
|
|
if not resumenes: |
|
|
st.warning("No se pudieron cargar los res煤menes de libros.") |
|
|
return |
|
|
|
|
|
|
|
|
st.header("馃摎 BookBot te gu铆a en tu camino profesional y personal") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
titulos_libros = [libro["titulo"] for libro in resumenes] |
|
|
|
|
|
|
|
|
col_select, col_info = st.columns([1, 2]) |
|
|
|
|
|
with col_select: |
|
|
libro_seleccionado = st.selectbox("Elige un libro para explorar", titulos_libros) |
|
|
|
|
|
with col_info: |
|
|
st.caption("Asistente potenciado por DeepSeek-R1-Distill-Qwen-32B") |
|
|
|
|
|
|
|
|
libro_actual = next( |
|
|
(libro for libro in resumenes if libro["titulo"] == libro_seleccionado), |
|
|
None |
|
|
) |
|
|
|
|
|
|
|
|
if libro_actual: |
|
|
|
|
|
col1, col2 = st.columns([1, 2]) |
|
|
|
|
|
def imagen_a_base64(imagen_path): |
|
|
"""Convierte una imagen local a formato Base64 para ser usada en HTML.""" |
|
|
with open(imagen_path, "rb") as img_file: |
|
|
return base64.b64encode(img_file.read()).decode() |
|
|
|
|
|
with col1: |
|
|
imagen_path = f"imagenes/{libro_seleccionado}.png" |
|
|
|
|
|
if os.path.exists(imagen_path): |
|
|
imagen_base64 = imagen_a_base64(imagen_path) |
|
|
|
|
|
st.markdown( |
|
|
f""" |
|
|
<style> |
|
|
.fixed-size-img {{ |
|
|
width: 300px; |
|
|
height: 400px; |
|
|
object-fit: contain; |
|
|
display: block; |
|
|
margin: auto; |
|
|
}} |
|
|
</style> |
|
|
<img src="data:image/png;base64,{imagen_base64}" class="fixed-size-img"> |
|
|
""", |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
else: |
|
|
st.error("Imagen no encontrada") |
|
|
|
|
|
with col2: |
|
|
|
|
|
st.subheader("Resumen") |
|
|
st.markdown(f"{libro_actual['breve_resumen'].replace(chr(10), '<br>')}", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
st.subheader("馃挰 Consulta a Bookbot sobre este libro") |
|
|
|
|
|
|
|
|
for message in st.session_state.chat_history: |
|
|
if message["role"] == "user": |
|
|
st.markdown(f"<div class='user-message'><strong>馃榾 T煤:</strong> {message['content']}</div>", unsafe_allow_html=True) |
|
|
else: |
|
|
st.markdown(f"<div class='assistant-message'><strong>馃 Asistente:</strong> {message['content']}</div>", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
user_input = st.text_input("", key="user_query", placeholder="Escribe tu pregunta sobre el libro...") |
|
|
|
|
|
|
|
|
col_button_l, col_button_c, col_button_r = st.columns([1, 1, 1]) |
|
|
with col_button_c: |
|
|
send_button = st.button("Enviar") |
|
|
|
|
|
|
|
|
if user_input and send_button: |
|
|
|
|
|
context = f""" |
|
|
CONTEXTO DEL LIBRO: |
|
|
T铆tulo: {libro_seleccionado} |
|
|
Resumen: {libro_actual['resumen']} |
|
|
|
|
|
PREGUNTA DEL USUARIO: |
|
|
{user_input} |
|
|
|
|
|
Responde de manera concisa y precisa, bas谩ndote en el resumen y contexto del libro. |
|
|
""" |
|
|
|
|
|
|
|
|
with st.spinner("馃 Pensando..."): |
|
|
|
|
|
payload = { |
|
|
"inputs": context, |
|
|
"parameters": { |
|
|
"max_new_tokens": 1000, |
|
|
"temperature": 0.7, |
|
|
"top_p": 0.9, |
|
|
"do_sample": True |
|
|
} |
|
|
} |
|
|
|
|
|
response = query_huggingface(payload) |
|
|
|
|
|
if response: |
|
|
|
|
|
if isinstance(response, list) and len(response) > 0: |
|
|
bot_response = response[0].get("generated_text", "") |
|
|
|
|
|
try: |
|
|
bot_response = bot_response.split("Responde de manera concisa y precisa")[-1] |
|
|
if "PREGUNTA DEL USUARIO:" in bot_response: |
|
|
bot_response = bot_response.split("PREGUNTA DEL USUARIO:")[-1] |
|
|
bot_response = bot_response.strip() |
|
|
except: |
|
|
|
|
|
pass |
|
|
else: |
|
|
bot_response = str(response) |
|
|
|
|
|
|
|
|
st.session_state.chat_history.append({"role": "user", "content": user_input}) |
|
|
st.session_state.chat_history.append({"role": "assistant", "content": bot_response}) |
|
|
|
|
|
|
|
|
st.rerun() |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |