From a5ed36ef96b08b8cd4c3038a2ea091416277edca Mon Sep 17 00:00:00 2001 From: "Juan M. Ley" Date: Sat, 25 Apr 2026 15:20:23 -0600 Subject: [PATCH] first and real commit --- api/__pycache__/api.cpython-314.pyc | Bin 0 -> 727 bytes api/__pycache__/router.cpython-314.pyc | Bin 0 -> 219 bytes api/__pycache__/routes.cpython-314.pyc | Bin 0 -> 219 bytes api/api.py | 17 + api/routes/__pycache__/ai.cpython-314.pyc | Bin 0 -> 4029 bytes api/routes/__pycache__/root.cpython-314.pyc | Bin 0 -> 454 bytes api/routes/ai.py | 89 ++++ api/routes/root.py | 9 + frontend/README.md | 56 +++ frontend/__pycache__/config.cpython-314.pyc | Bin 0 -> 667 bytes frontend/config.py | 18 + frontend/main.py | 349 ++++++++++++++++ frontend/requirements.txt | 5 + html_client.html | 423 ++++++++++++++++++++ main.py | 12 + 15 files changed, 978 insertions(+) create mode 100644 api/__pycache__/api.cpython-314.pyc create mode 100644 api/__pycache__/router.cpython-314.pyc create mode 100644 api/__pycache__/routes.cpython-314.pyc create mode 100644 api/api.py create mode 100644 api/routes/__pycache__/ai.cpython-314.pyc create mode 100644 api/routes/__pycache__/root.cpython-314.pyc create mode 100644 api/routes/ai.py create mode 100644 api/routes/root.py create mode 100644 frontend/README.md create mode 100644 frontend/__pycache__/config.cpython-314.pyc create mode 100644 frontend/config.py create mode 100644 frontend/main.py create mode 100644 frontend/requirements.txt create mode 100644 html_client.html create mode 100644 main.py diff --git a/api/__pycache__/api.cpython-314.pyc b/api/__pycache__/api.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7d8859be910e05cbd534de3bf1db50a6a8f23e7 GIT binary patch literal 727 zcmZ`#PiqrF6n~T5Y&L(3hgwX)B3?{vcWtTEf)vDm38)RzTw@q^r)F@IS!O5Hdg|RT z;IYSg%ZKm-D2RrEfZ$2=5Q!ha*=*AE;)9v@d-Hzpz2BSLrD6`W{ht0}J1Kx4aj-CZ z4yGS9IE9yhQ3L8oqV*W38mYP=jTtr?W<4#fInHQouR|N%fSq{@nQdpAtBb}<-!`!6 zAe^4v7S1>+Z11L%yzY=~t}VF>)V6RI=XSN)*#671asDz37j8ne_%kl4B9(bUBKe@% z(6IFI03SYQz8}zIBB*|`1RqE$6son9@$_*^nazX4R@GER5(NCX!v%ZA`jNVlgr1;2 z?Mp_2Xu;@G;!|-E_NeS~KT=RliFi3>g@PWjH*XD>uou;W${cAqBxJ&fvupFn>0PLD zJT4+(#@Q9kb7l%7?YK-j=3 zQfAO(ehK1eGT!2J4DbxfFD*$e^3!C##h#W}T#{IjS;P#Ky~S1pmRQN~8Kn4@zJ5l2 zZmND!eoDT+OMY@`Zfaghv3^0NzE5UJNlq%zAbp@#pghPly@JYH95%W6DWy57c14^( f{ft0d%nu|!Ff%eT-er(|$SKq<-NaVJ4ip0b(T_Bd literal 0 HcmV?d00001 diff --git a/api/__pycache__/routes.cpython-314.pyc b/api/__pycache__/routes.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88daeb43780f263753086f8b152a548818e7b23d GIT binary patch literal 219 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 YFb2~;vRh#NmH5zcQG8Df9q*R^1(4%ydjJ3c literal 0 HcmV?d00001 diff --git a/api/routes/__pycache__/root.cpython-314.pyc b/api/routes/__pycache__/root.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3edc5371549cbea0195dd78319d9edd22cf93ec7 GIT binary patch literal 454 zcmYLEO-sW-5S`s58>?0PI0zM~;wcD?hk_RoL=P53O7L1Cw6P7e*|OOPy~UG)2m3et zJ>C{9iYG4;%^z?!ZRx!9z(dK_aSHEPa{8$`(l!g;Dnvgx<-~LDk(J9|W%89qwN6b}|Se z8N0)gdlgFA5AboznR)!MT-{J!s;dk**BgJ>iKG`=4GGJi7w zKRmGtehGJ%6ee&63L}HWU?$_SPxeVHW;4)W9)rRj)6-sLK;arJbv9Ox01Cfw{A=fk z!(4--mB5h?Z!9QCiC<8Nl3xkqfFj_a62YOfG!84#9mvLhdMmOF3HR(~TW{Ogu-;P1 zcMXrm!5qzc$Ltwn|2f8kJVJz%Dyl}9JmIPpg#sFLsA-~c)XSrxX?5wG*3i6U0n9Hx zfbnL@#JFb^s`8^(j&0G&?Q&Ki{NHsB?E~VohYsg~K-=^e1y$5*N{PfhC5eTSCh20i zSgpuqjU+ttR8-Z%iMV!C|GUM?OLbim>TkYl%PRJ2>8zG2}Z8eE`%cSD`!-B8Q6I%YRFxFCUN zeY-vC8PA<09pe4ya6r>7%P`Chq;FvBJA~iA{s!B(JZz>uBxn4`w7b23#a!=wPJYU{ z*(Yu+KN04s-Dz~zbd&oo#ODw8rs)}W?;pC5m}d^A)>YYk_|%0I9da*DyJ?EdV>=Up N8u6`nF(*Nn{sA`IwM+m2 literal 0 HcmV?d00001 diff --git a/frontend/config.py b/frontend/config.py new file mode 100644 index 0000000..20e0cdd --- /dev/null +++ b/frontend/config.py @@ -0,0 +1,18 @@ +from dotenv import load_dotenv +from os import getenv + +load_dotenv('.env_file') + +# Configuración de la aplicación frontend +API_BASE_URL = f"http://{getenv('HOST')}:{getenv('PORT')}" +API_HEALTH_ENDPOINT = f"{API_BASE_URL}/" +API_MESSAGE_ENDPOINT = f"{API_BASE_URL}/ai/" +API_MESSAGE_DOCS_ENDPOINT = f"{API_BASE_URL}/ai/using_docs" + +# Configuración de la ventana +WINDOW_WIDTH = 900 +WINDOW_HEIGHT = 700 +WINDOW_TITLE = "LittleAPI Frontend" + +# Configuración de timeout para requests +REQUEST_TIMEOUT = 30 diff --git a/frontend/main.py b/frontend/main.py new file mode 100644 index 0000000..32b7ac6 --- /dev/null +++ b/frontend/main.py @@ -0,0 +1,349 @@ +import sys +import requests +import markdown +from PyQt6.QtWidgets import ( + QApplication, + QMainWindow, + QWidget, + QVBoxLayout, + QHBoxLayout, + QTextEdit, + QTextBrowser, + QPushButton, + QLabel, + QRadioButton, + QButtonGroup, + QMessageBox, +) +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtGui import QFont +from config import ( + API_BASE_URL, + API_HEALTH_ENDPOINT, + API_MESSAGE_ENDPOINT, + API_MESSAGE_DOCS_ENDPOINT, + WINDOW_WIDTH, + WINDOW_HEIGHT, + WINDOW_TITLE, + REQUEST_TIMEOUT, +) + + +def markdown_to_html(text): + """Convierte markdown a HTML con soporte para tablas, títulos, negritas y cursivas""" + html = markdown.markdown( + text, + extensions=['tables', 'sane_lists', 'extra'] + ) + # Agregar CSS para mejorar la presentación + styled_html = f""" + + + + + + {html} + + + """ + return styled_html + + +class APIWorker(QThread): + """Thread worker para hacer requests a la API sin bloquear la UI""" + response_received = pyqtSignal(str) + error_occurred = pyqtSignal(str) + finished = pyqtSignal() + + def __init__(self, endpoint, message, use_streaming=True): + super().__init__() + self.endpoint = endpoint + self.message = message + self.use_streaming = use_streaming + + def run(self): + try: + if self.use_streaming: + self._handle_streaming_response() + else: + self._handle_normal_response() + except requests.exceptions.ConnectionError: + self.error_occurred.emit( + "Error de conexión: No se puede conectar a la API.\n" + f"Asegúrate de que el servidor esté corriendo en {API_BASE_URL}" + ) + except requests.exceptions.Timeout: + self.error_occurred.emit("Error de timeout: La API tardó demasiado en responder.") + except Exception as e: + self.error_occurred.emit(f"Error: {str(e)}") + finally: + self.finished.emit() + + def _handle_streaming_response(self): + """Maneja respuestas en streaming""" + try: + response = requests.post( + self.endpoint, + params={"message": self.message}, + stream=True, + timeout=REQUEST_TIMEOUT, + ) + response.raise_for_status() + + for chunk in response.iter_content(decode_unicode=True, chunk_size=10): + if chunk: + self.response_received.emit(chunk) + except Exception as e: + raise e + + def _handle_normal_response(self): + """Maneja respuestas normales""" + response = requests.post( + self.endpoint, + params={"message": self.message}, + timeout=REQUEST_TIMEOUT, + ) + response.raise_for_status() + self.response_received.emit(response.text) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle(WINDOW_TITLE) + self.setGeometry(100, 100, WINDOW_WIDTH, WINDOW_HEIGHT) + self.worker = None + self.api_status = False + self.response_buffer = "" # Buffer para acumular la respuesta + + self.initUI() + self.check_api_status() + + def initUI(self): + """Inicializa la interfaz de usuario""" + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QVBoxLayout() + + # Sección de estado + status_layout = QHBoxLayout() + self.status_label = QLabel("● Estado: Verificando...") + self.status_label.setFont(QFont("Arial", 10)) + status_layout.addWidget(self.status_label) + status_layout.addStretch() + refresh_btn = QPushButton("Actualizar estado") + refresh_btn.clicked.connect(self.check_api_status) + status_layout.addWidget(refresh_btn) + main_layout.addLayout(status_layout) + + # Selector de modo + mode_layout = QHBoxLayout() + mode_label = QLabel("Modo:") + mode_layout.addWidget(mode_label) + + self.mode_group = QButtonGroup() + self.radio_normal = QRadioButton("Mensaje normal") + self.radio_docs = QRadioButton("Con documentos") + self.radio_normal.setChecked(True) + self.mode_group.addButton(self.radio_normal) + self.mode_group.addButton(self.radio_docs) + mode_layout.addWidget(self.radio_normal) + mode_layout.addWidget(self.radio_docs) + mode_layout.addStretch() + main_layout.addLayout(mode_layout) + + # Área de entrada + input_label = QLabel("Mensaje:") + main_layout.addWidget(input_label) + self.input_text = QTextEdit() + self.input_text.setPlaceholderText("Escribe tu mensaje aquí...") + self.input_text.setMaximumHeight(120) + main_layout.addWidget(self.input_text) + + # Botones de acción + button_layout = QHBoxLayout() + self.send_btn = QPushButton("Enviar") + self.send_btn.clicked.connect(self.send_message) + self.clear_btn = QPushButton("Limpiar") + self.clear_btn.clicked.connect(self.clear_all) + button_layout.addWidget(self.send_btn) + button_layout.addWidget(self.clear_btn) + button_layout.addStretch() + main_layout.addLayout(button_layout) + + # Área de respuesta + response_label = QLabel("Respuesta:") + main_layout.addWidget(response_label) + self.response_text = QTextBrowser() + self.response_text.setMarkdown("La respuesta de la API aparecerá aquí...") + main_layout.addWidget(self.response_text) + + central_widget.setLayout(main_layout) + + def check_api_status(self): + """Verifica el estado del API""" + try: + response = requests.get(API_HEALTH_ENDPOINT, timeout=REQUEST_TIMEOUT) + if response.status_code == 200: + self.api_status = True + self.status_label.setText("● Estado: API conectada ✓") + self.status_label.setStyleSheet("color: green;") + return + except Exception: + pass + + self.api_status = False + self.status_label.setText("● Estado: API desconectada ✗") + self.status_label.setStyleSheet("color: red;") + + def send_message(self): + """Envía el mensaje a la API""" + if not self.api_status: + QMessageBox.warning(self, "Error", "La API no está conectada.") + return + + message = self.input_text.toPlainText().strip() + if not message: + QMessageBox.warning(self, "Advertencia", "Por favor escribe un mensaje.") + return + + # Determinar endpoint + use_docs = self.radio_docs.isChecked() + endpoint = API_MESSAGE_DOCS_ENDPOINT if use_docs else API_MESSAGE_ENDPOINT + + # Limpiar respuesta anterior + self.response_buffer = "" + self.response_text.clear() + + # Deshabilitar botón de envío + self.send_btn.setEnabled(False) + self.send_btn.setText("Procesando...") + + # Crear y ejecutar worker + self.worker = APIWorker(endpoint, message, use_streaming=True) + self.worker.response_received.connect(self.append_response) + self.worker.error_occurred.connect(self.handle_error) + self.worker.finished.connect(self.on_request_finished) + self.worker.start() + + def append_response(self, chunk): + """Acumula chunks de respuesta y los renderiza como markdown""" + self.response_buffer += chunk + # Renderizar el markdown con HTML + html = markdown_to_html(self.response_buffer) + self.response_text.setHtml(html) + # Auto-scroll al final + scrollbar = self.response_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def handle_error(self, error_message): + """Maneja errores""" + error_md = f"**Error:** {error_message}" + html = markdown_to_html(error_md) + self.response_text.setHtml(html) + QMessageBox.critical(self, "Error", error_message) + + def on_request_finished(self): + """Se ejecuta cuando la request termina""" + self.send_btn.setEnabled(True) + self.send_btn.setText("Enviar") + self.check_api_status() + + def clear_all(self): + """Limpia el mensaje y la respuesta""" + self.input_text.clear() + self.response_buffer = "" + self.response_text.clear() + self.input_text.setFocus() + + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/frontend/requirements.txt b/frontend/requirements.txt new file mode 100644 index 0000000..021ec45 --- /dev/null +++ b/frontend/requirements.txt @@ -0,0 +1,5 @@ +PyQt6==6.7.1 +PyQt6-Qt6==6.7.1 +PyQt6-sip==13.6.1 +requests==2.32.3 +markdown==3.5.2 diff --git a/html_client.html b/html_client.html new file mode 100644 index 0000000..493a780 --- /dev/null +++ b/html_client.html @@ -0,0 +1,423 @@ + + + + + + Claude API Chat + + + +
+
+

🤖 Claude API Chat

+

Interfaz de chat con tu API de FastAPI

+
+ + +
+
+ +
+ + +
+ +
+
+
+ ¡Hola! 👋 Estoy listo para ayudarte. Puedes cambiar entre modo normal y modo con documentos usando los botones superiores. +
+
+
+ +
+
+ + + +
+
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..277bc24 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +import uvicorn +from os import getenv; from dotenv import load_dotenv + +load_dotenv('.env_file') + +if __name__ == "__main__": + uvicorn.run( + app="api.api:application", + port=int(getenv('PORT')), + host=getenv('HOST'), + reload=True + ) \ No newline at end of file