16 Commits

Author SHA1 Message Date
egutierrez fa33321ff1 feat: Refactor backend structure by moving main application and router files to backend_domains 2025-06-22 10:05:04 +02:00
egutierrez a2873e4c85 feat: Add .continue to .gitignore 2025-06-21 14:51:20 +02:00
egutierrez 0d97aa2d61 feat: Remove unused Login page component 2025-06-21 02:18:55 +02:00
egutierrez 23f25034ad Merge remote-tracking branch 'origin/cambios_llm' 2025-06-21 02:17:56 +02:00
egutierrez 8ad80defcf feat: Remove @tabler/icons-react dependency and add Login page component 2025-06-21 02:13:19 +02:00
egutierrez aef8791151 feat: Implement main application shell with navigation and color scheme toggle
- Added Appshell component with responsive navbar and main content area
- Integrated ColorSchemeToggle for light/dark mode switching
- Created Welcome component with styled title and introductory text
- Developed ChatPage for LLM interaction with WebSocket support
- Implemented Biblioteca for managing notes with rich text editor
- Added LoginPage for user authentication with error handling
- Introduced MessageList and MessageBubble components for chat messages
- Styled components with CSS modules for consistent design
2025-06-21 02:01:21 +02:00
egutierrez 3d5deef0fb feat: Implement user management functionality with login and CRUD operations 2025-06-21 01:40:19 +02:00
egutierrez 3438102dc0 feat: Add initial task list for project planning and user management 2025-06-17 00:38:00 +02:00
egutierrez 9ee8daa295 feat: Implement WebSocket support for chat functionality and refactor chat service
- Added WebSocket endpoint for real-time chat interactions.
- Refactored ChatPage component to utilize WebSocket for sending and receiving messages.
- Updated chat service to handle streaming responses from the LLM agent.
- Introduced error handling for WebSocket connections and message processing.
- Modified Editor_Test to include AppShellWithMenu for better layout.
- Adjusted file path in generar_tree.py for correct directory structure.
- Created llm_chat_endpoint_v1.py and llm_chat_srvc.py for handling chat requests and responses.
- Established logging for WebSocket interactions and errors.
2025-06-17 00:19:36 +02:00
egutierrez 6d6fab5634 feat: Add Chat LLM functionality with message input and display components 2025-06-16 23:18:18 +02:00
egutierrez 9c638fc3e5 Generacion de estructura ddd para backend
feat: Refactor API structure by consolidating endpoints and removing deprecated files
2025-06-16 22:22:41 +02:00
egutierrez 43f6fb03fe feat: Refactor CamaraNoir component layout and update content for live streaming 2025-06-16 22:12:28 +02:00
egutierrez ac83907e7c feat: Update routing paths and enhance navigation links for Biblioteca and CameraNoir 2025-06-11 23:19:23 +02:00
egutierrez 3cd267ee6e Gestionado todo el frontend para domain driven design
feat: Implement LlamadorAPI component for API interaction with dynamic request handling

feat: Create MetodoSelect component for selecting HTTP methods with visual feedback

feat: Add VisualizacionesRandom component to display various charts using ECharts

feat: Develop custom 404 Error page with holographic shader effect

feat: Create HoloShader component for dynamic background effects on 404 page

style: Add CSS styles for Appshell layout and navigation

feat: Build Appshell component to manage application layout and navigation

feat: Add ColorSchemeToggle component for switching between light and dark themes

feat: Create Plantilla component as a template for future pages

style: Define styles for Welcome component

feat: Implement Welcome component with introductory text and links

feat: Develop HomePage component to serve as the main entry point of the application

feat: Create Biblioteca component for managing notes and libraries with rich text editor

feat: Add Editor_Test component for testing rich text editor functionality

style: Define styles for the rich text editor in Biblioteca
2025-06-11 22:53:32 +02:00
egutierrez e1b756ac99 feat: Implement cookie extraction script for Chrome v20 and enhance browser interaction 2025-06-01 15:31:13 +02:00
egutierrez 628cddc3ae feat: Enhance logging and add chart endpoints
- Updated LoggerDB to remove all active sinks on initialization.
- Added a new PostgresCredencial setup in notas_mmr.py for database connection.
- Replaced print statements with logger calls for better logging in notas_mmr.py.
- Introduced new FastAPI endpoints for various chart types (bar, line, pie, scatter).
- Created Editor_biblioteca.css for styling the rich text editor.
- Implemented Editor_Test.tsx to test the rich text editor functionality.
2025-06-01 00:33:48 +02:00
129 changed files with 6890 additions and 2442 deletions
+1
View File
@@ -17,6 +17,7 @@ pruebas_conceptos/postgres_extensions/pgdata/*
*.env
config/.env
.continue
#Icon files
frontend/src/assets/icons/filled/** */
+6
View File
@@ -0,0 +1,6 @@
# Inicia el frontend en una nueva ventana de PowerShell
Start-Process powershell -ArgumentList "-NoExit", "-Command", 'cd ./frontend; npm run dev; pause'
# Inicia el backend en una nueva ventana de PowerShell
Start-Process powershell -ArgumentList "-NoExit", "-Command", 'cd ./backend; uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000; pause'
-8
View File
@@ -1,8 +0,0 @@
# backend/api/router.py
from fastapi import APIRouter
from backend.api.v1.endpoints import ping, text_manager_endpoint
router = APIRouter()
router.include_router(ping.router, prefix="/api/v1/ping")
router.include_router(text_manager_endpoint.router, prefix="/api/v1/text_manager")
@@ -0,0 +1,57 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/bar")
def get_bar_chart():
return {
"xAxis": {"type": "category", "data": ["Ene", "Feb", "Mar", "Abr"]},
"yAxis": {"type": "value"},
"series": [{"data": [5, 20, 36, 10], "type": "bar"}]
}
@router.get("/line")
def get_line_chart():
return {
"xAxis": {"type": "category", "data": ["Semana 1", "Semana 2", "Semana 3", "Semana 4"]},
"yAxis": {"type": "value"},
"series": [{"data": [15, 25, 18, 30], "type": "line"}]
}
@router.get("/pie")
def get_pie_chart():
return {
"tooltip": {"trigger": "item"},
"legend": {"top": "5%", "left": "center"},
"series": [{
"name": "Accesos",
"type": "pie",
"radius": ["40%", "70%"],
"avoidLabelOverlap": False,
"itemStyle": {"borderRadius": 10, "borderColor": "#fff", "borderWidth": 2},
"label": {"show": False, "position": "center"},
"emphasis": {
"label": {"show": True, "fontSize": 16, "fontWeight": "bold"}
},
"labelLine": {"show": False},
"data": [
{"value": 1048, "name": "Search"},
{"value": 735, "name": "Direct"},
{"value": 580, "name": "Email"},
{"value": 484, "name": "Union Ads"},
{"value": 300, "name": "Video Ads"}
]
}]
}
@router.get("/scatter")
def get_scatter_chart():
return {
"xAxis": {},
"yAxis": {},
"series": [{
"symbolSize": 20,
"data": [[10, 8], [20, 20], [30, 10], [40, 30], [50, 15]],
"type": "scatter"
}]
}
@@ -0,0 +1,44 @@
# backend/domains/llm/agent_endpoints.py
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from fastapi.concurrency import run_in_threadpool
from backend.backend_domains.llms.llm_chat_srvc import construir_agente_llm, responder, responder_stream
from domains.Logger.logger_db import LoggerDB, logger
from entrypoint.init_db import db_credencial
LoggerDB(db_credencial, "logger_llm", created_by="sistema")
router = APIRouter()
agente = construir_agente_llm() # inicializa el agente una vez
# 📥 Schema para entrada de prompt
class ChatInput(BaseModel):
prompt: str
# ✅ Endpoint de respuesta simple
@router.post("/chat", summary="Enviar prompt y obtener respuesta completa del agente")
async def chat_endpoint(data: ChatInput):
try:
return await responder(data.prompt, agente)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.exception("[ERROR] Fallo durante respuesta del agente:")
raise HTTPException(status_code=500, detail="Error interno al procesar la solicitud.")
# 🔁 Endpoint de streaming
@router.post("/chat-stream", summary="Enviar prompt y recibir respuesta del agente en streaming")
async def chat_stream_endpoint(data: ChatInput):
try:
return StreamingResponse(
responder_stream(data.prompt, agente),
media_type="text/plain"
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.exception("[ERROR] Fallo durante respuesta en streaming:")
raise HTTPException(status_code=500, detail="Error interno en el agente.")
@@ -0,0 +1,84 @@
# src/services/agent_service.py
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
from domains.ConexionSql.Postgres_conexion import PostgresConexion
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
from domains.Llms.Modelos.Openai_model import ModeloOpenAI
from domains.Llms.Agente import AgenteAI
from domains.Llms.Memory.postgres_MemoryConv import MemoryConvPostgres
from domains.Llms.MCPs.McpClient import MCPClient
from domains.Llms.MCPs.McpClient_Registry import ClientRegistry
from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_llm", created_by="sistema")
from typing import AsyncGenerator
# 🔧 Inicialización única del agente
def construir_agente_llm() -> AgenteAI:
logger.info("[INICIO] Inicializando agente LLM...")
conexion = PostgresConexion(db_credencial)
# Paso 1: Obtener credencial
repo = OpenAICredencialRepo(conexion)
credencial = repo.get_by_id("OPAK20250513-61b29978b7604031014")
if not credencial:
raise ValueError("No se encontró la credencial OpenAI")
logger.debug(f"[OK] Credencial OpenAI cargada: {credencial.titulo}")
# Paso 2: Crear cliente
cliente = OpenAICliente(credencial)
# Paso 3: Instanciar modelo
modelo = ModeloOpenAI(
cliente=cliente,
model="gpt-4o",
temperature=1
)
# Paso 4: Memoria en PostgreSQL
memoria = MemoryConvPostgres(
credencial=db_credencial,
nombre_tabla="memoria_conversacion_pruebas",
k=10
)
# Paso 5: Herramientas MCP (ej. archivos)
archivos = MCPClient.from_http(
name="files",
url="http://127.0.0.1:4201/fs"
)
registry = ClientRegistry()
registry.add("files", archivos)
# Paso 6: Agente
agente = AgenteAI(
modelo=modelo,
nombre="Asistente Inteligente",
descripcion="",
system_prompt="",
rol="asistente",
objetivos=[],
max_iterations=0,
memoria=memoria,
mcp=registry
)
logger.success("[OK] Agente LLM listo.")
return agente
# ⚡ Función simple
async def responder(prompt: str, agente: AgenteAI) -> str:
logger.info(f"[Petición] Prompt recibido: {prompt[:50]}...")
respuesta = await agente.interactuar_en_bucle(prompt=prompt, stream=False)
logger.debug(f"[Respuesta] {respuesta[:100]}...")
return respuesta
# 🔁 Función en streaming
async def responder_stream(prompt: str, agente: AgenteAI) -> AsyncGenerator[str, None]:
logger.info(f"[Streaming] Prompt recibido: {prompt[:50]}...")
async for token in agente.interactuar_en_bucle(prompt=prompt, stream=True):
yield token
@@ -0,0 +1,35 @@
from fastapi import WebSocket, APIRouter, WebSocketDisconnect
from backend.backend_domains.llms.llm_chat_srvc import construir_agente_llm
from domains.Logger.logger_db import LoggerDB, logger
from entrypoint.init_db import db_credencial
import json
LoggerDB(db_credencial, "logger_llm_ws", created_by="sistema")
router = APIRouter()
agente = construir_agente_llm()
@router.websocket("/ws/chat")
async def chat_ws(websocket: WebSocket):
await websocket.accept()
try:
data = await websocket.receive_text()
parsed = json.loads(data)
prompt = parsed.get("prompt")
if not prompt:
await websocket.send_text("⚠️ Prompt vacío.")
await websocket.close()
return
# ✅ Solución: hacer await antes de iterar
respuesta_gen = await agente.interactuar_en_bucle(prompt=prompt, stream=True)
async for token in respuesta_gen:
await websocket.send_text(token)
await websocket.close()
except WebSocketDisconnect:
logger.info("🔌 WebSocket desconectado por el cliente.")
except Exception as e:
logger.exception("❌ Error en WebSocket:")
await websocket.close()
@@ -2,7 +2,9 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from backend.api.v1.router import router
from backend.backend_domains.router_v1 import router
from backend.backend_domains.llms import llm_chat_ws_endpoint_v1
app = FastAPI(
title="Fitz Backend",
@@ -21,4 +23,5 @@ app.add_middleware(
# Incluye las rutas de tu API
app.include_router(router)
app.include_router(router, prefix="/api/v1", tags=["v1"])
app.include_router(llm_chat_ws_endpoint_v1.router)
+15
View File
@@ -0,0 +1,15 @@
# backend/api/router_v1.py
from fastapi import APIRouter
from backend.backend_domains.experiments import charts_examples_endpoint_v1 as charts
from backend.backend_domains.experiments import ping_endpoint_v1
from backend.backend_domains.text_manager import text_manager_endpoint_v1
from backend.backend_domains.llms import llm_chat_endpoint_v1
from backend.backend_domains.usuarios_endpoint_v1 import router as usuarios_router
router = APIRouter()
router.include_router(ping_endpoint_v1.router, prefix="/ping")
router.include_router(text_manager_endpoint_v1.router, prefix="/text_manager")
router.include_router(charts.router, prefix="/charts")
router.include_router(llm_chat_endpoint_v1.router, prefix="/llm", tags=["Agente LLM"])
router.include_router(usuarios_router, prefix="/usuarios", tags=["Usuarios"])
@@ -1,12 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import Path
from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput
from backend.backend_domains.text_manager.text_manager_schema import BibliotecaInput, NotaInput
from fastapi.concurrency import run_in_threadpool
from backend.db.conexion import get_conexion
from backend.services.text_manager_srvc import *
from src.ConexionSql.Postgres_conexion import PostgresConexion
from backend.backend_domains.text_manager.text_manager_srvc import *
from domains.ConexionSql.Postgres_conexion import PostgresConexion
from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
router = APIRouter()
@@ -1,53 +1,55 @@
from src.TextManager.biblioteca import Biblioteca
from src.TextManager.biblioteca_mmr import BibliotecaRepo
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
from src.ConexionSql.Postgres_conexion import PostgresConexion
from src.TextManager.nota import Nota
from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
from domains.TextManager.biblioteca import Biblioteca
from domains.TextManager.biblioteca_mmr import BibliotecaRepo
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
from domains.ConexionSql.Postgres_conexion import PostgresConexion
from domains.TextManager.nota import Nota
from domains.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
from sqlalchemy import MetaData
from backend.schemas.text_manager_schema import NotaInput
from backend.backend_domains.text_manager.text_manager_schema import NotaInput
from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None):
print("[INICIO] Creando biblioteca...")
logger.info("[INICIO] Creando biblioteca...")
try:
print("[Paso 1] Obteniendo credencial...")
logger.info("[Paso 1] Obteniendo credencial...")
cred_repo = OpenAICredencialRepo(conexion)
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None")
logger.debug(f"[OK] Credencial obtenida: {credencial.titulo if credencial else '❌ None'}")
print("[Paso 2] Instanciando embedder...")
logger.info("[Paso 2] Instanciando embedder...")
embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large")
print("[OK] Embedder instanciado")
logger.debug("[OK] Embedder instanciado")
print("[Paso 3] Instanciando biblioteca...")
logger.info("[Paso 3] Instanciando biblioteca...")
biblioteca = Biblioteca(
nombre=nombre_biblioteca,
embedder=embedder,
descripcion=descripcion
)
print(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
logger.debug(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
print("[Paso 4] Guardando en base de datos...")
logger.info("[Paso 4] Guardando en base de datos...")
repo = BibliotecaRepo(conexion)
repo.add(biblioteca=biblioteca)
print("[OK] Biblioteca guardada")
logger.success("[OK] Biblioteca guardada")
print("[Paso 5] Generando modelo de notas...")
logger.info("[Paso 5] Generando modelo de notas...")
biblioteca.generar_modelo_notas(conexion)
print("[OK] Modelo de notas generado")
logger.success("[OK] Modelo de notas generado")
print("[FIN] Biblioteca creada correctamente")
logger.success("[FIN] Biblioteca creada correctamente")
return {
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
"id": biblioteca.id
}
except Exception as e:
print("[ERROR] Ocurrió una excepción:", str(e))
logger.exception("[ERROR] Ocurrió una excepción:")
raise
@@ -64,6 +66,7 @@ def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
for b in bibliotecas
]
def agregar_nota_a_biblioteca(
conexion: PostgresConexion,
biblioteca_id: str,
@@ -73,25 +76,19 @@ def agregar_nota_a_biblioteca(
conexiones: list[str] = None,
resumen: str = ""
):
# Obtener la biblioteca
repo_biblioteca = BibliotecaRepo(conexion)
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
if biblioteca is None:
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
# Crear objeto Nota
nota = Nota(
titulo=titulo,
texto=texto,
tags=tags or [],
conexiones=conexiones or [],
resumen=resumen or "",
# vector=biblioteca.embedder.embed_text(texto),
# vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None
)
# Mostrar atributos seguros
print(
logger.debug(
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
f"texto_len={len(nota.texto)}, "
f"tags={len(nota.tags)}, "
@@ -99,7 +96,6 @@ def agregar_nota_a_biblioteca(
f"resumen_len={len(nota.resumen)}"
)
# Preparar tabla y modelo de nota
metadata = MetaData()
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
biblioteca.nombre,
@@ -108,7 +104,6 @@ def agregar_nota_a_biblioteca(
)
metadata.create_all(conexion.get_engine())
# Guardar la nota
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
nota_id = repo_nota.add(nota)
@@ -117,7 +112,7 @@ def agregar_nota_a_biblioteca(
"nota_id": nota_id
}
print(f"[SUCCESS] {resultado['mensaje']}")
logger.success(f"[SUCCESS] {resultado['mensaje']}")
return resultado
@@ -160,6 +155,7 @@ def eliminar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str)
fue_eliminada = repo_nota.delete_by_id(nota_id)
if fue_eliminada:
logger.success(f"Nota '{nota_id}' eliminada correctamente.")
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
@@ -186,6 +182,7 @@ def actualizar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str
fue_actualizada = repo_nota.update(nota_id, nota_actualizada)
if fue_actualizada:
logger.success(f"Nota '{nota_id}' actualizada correctamente.")
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
@@ -0,0 +1,35 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from domains.Usuario.usuario_mmr import UsuarioRepo, Usuario, UsuarioModel
from backend.db.conexion import get_conexion
router = APIRouter()
@router.post("/usuarios/", response_model=dict)
def crear_usuario(nombre: str, email: str, db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
usuario = Usuario(id=None, nombre=nombre, email=email)
usuario_id = repo.add(usuario)
return {"id": usuario_id}
@router.get("/usuarios/{usuario_id}", response_model=dict)
def obtener_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
usuario = repo.get_by_id(usuario_id)
if not usuario:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return {"id": usuario.id, "nombre": usuario.nombre, "email": usuario.email, "activo": usuario.activo}
@router.get("/usuarios/", response_model=list)
def listar_usuarios(db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
usuarios = repo.get_all()
return [{"id": u.id, "nombre": u.nombre, "email": u.email, "activo": u.activo} for u in usuarios]
@router.delete("/usuarios/{usuario_id}", response_model=dict)
def eliminar_usuario(usuario_id: int, db: Session = Depends(get_conexion)):
repo = UsuarioRepo(db)
exito = repo.delete_by_id(usuario_id)
if not exito:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return {"ok": True}
+1 -1
View File
@@ -1,6 +1,6 @@
# backend/db/conexion.py
from entrypoint.init_db import db_credencial
from src.ConexionSql.Postgres_conexion import PostgresConexion
from domains.ConexionSql.Postgres_conexion import PostgresConexion
def get_conexion():
conexion = PostgresConexion(db_credencial)
+26 -1016
View File
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Security.GenerarIDs import GeneradorIDUnico
class OpenAICredencial:
def __init__(self, titulo: str, api_key: str, organizacion: str = None, id: str = None):
@@ -3,17 +3,17 @@ import base64
from dotenv import load_dotenv
from sqlalchemy import Column, Integer, String
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.ApiKeys.openai_apikey import OpenAICredencial
from src.Security.Encriptar import Encriptar_fernet
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.ApiKeys.openai_apikey import OpenAICredencial
from domains.Security.Encriptar import Encriptar_fernet
from entrypoint import ENV_PATH
from src.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from sqlalchemy import Column, String
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
# ----------------------
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime
from src.ArquitectureLayer.Mapper import Mapper_base # Asegúrate de importar tu ABC base
from domains.ArquitectureLayer.Mapper import Mapper_base # Asegúrate de importar tu ABC base
TModelo = TypeVar("TModelo")
TDominio = TypeVar("TDominio")
@@ -1,5 +1,5 @@
import requests
from src.Credenciales.ollama_credencial import OllamaCredencial
from domains.Credenciales.ollama_credencial import OllamaCredencial
class OllamaCliente:
def __init__(self, credencial: OllamaCredencial):
@@ -1,5 +1,5 @@
from openai import OpenAI
from src.ApiKeys.openai_apikey import OpenAICredencial
from domains.ApiKeys.openai_apikey import OpenAICredencial
class OpenAICliente:
def __init__(self, credencial: OpenAICredencial):
@@ -4,8 +4,8 @@ from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.engine import Engine
from src.ConexionSql.Base_conexion import ConexionBase
from src.Credenciales.postgres_credencial import PostgresCredencial
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.Credenciales.postgres_credencial import PostgresCredencial
class PostgresConexion(ConexionBase):
def __init__(self, *args, **kwargs):
@@ -1,4 +1,4 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Security.GenerarIDs import GeneradorIDUnico
class OllamaCredencial:
def __init__(self, titulo: str, base_url: str = "http://localhost:11434", id: str = None):
@@ -1,4 +1,4 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Security.GenerarIDs import GeneradorIDUnico
class PostgresCredencial:
def __init__(self, titulo: str, host: str, port: int, dbname: str, user: str, password: str, id: str = None):
@@ -6,14 +6,14 @@ from sqlalchemy import DateTime, Text, func
import base64
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Credenciales.postgres_credencial import PostgresCredencial
from src.Security.Encriptar import Encriptar_fernet
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Credenciales.postgres_credencial import PostgresCredencial
from domains.Security.Encriptar import Encriptar_fernet
# ----------------------
# Cargar clave maestra
@@ -1,13 +1,13 @@
from src.Llms.Modelos.Base_model import ModeloABC
from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
from src.Llms.MCPs.McpClient_Registry import ClientRegistry
from domains.Llms.Modelos.Base_model import ModeloABC
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
from domains.Llms.MCPs.McpClient_Registry import ClientRegistry
from datetime import datetime
from typing import Optional, List, Union, AsyncGenerator
import re
import json
from entrypoint.init_db import db_credencial
from src.Logger.logger_db import LoggerDB, logger
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_agentes", created_by="sistema")
@@ -1,8 +1,8 @@
from typing import List
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
from src.ApiKeys.openai_apikey import OpenAICredencial
from src.ConexionApis.OpenAi_conexion import OpenAICliente
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
from domains.ApiKeys.openai_apikey import OpenAICredencial
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
from domains.Security.GenerarIDs import GeneradorIDUnico
class OpenAIEmbedder(EmbedderABC):
def __init__(self, credencial: OpenAICredencial,
@@ -3,15 +3,15 @@ from dotenv import load_dotenv
from sqlalchemy import Column, String
from sqlalchemy import Column, String, ForeignKey
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from src.ApiKeys.openai_apikey import OpenAICredencial
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from domains.ApiKeys.openai_apikey import OpenAICredencial
# ----------------------
# Cargar configuración desde .env si se requiere
@@ -1,4 +1,4 @@
from src.Llms.MCPs.McpClient import MCPClient
from domains.Llms.MCPs.McpClient import MCPClient
from typing import Any
class ClientRegistry:
@@ -1,9 +1,9 @@
from sqlalchemy import Table, Column, Integer, String, MetaData, insert, select, delete
from typing import Literal
from src.Credenciales.postgres_credencial import PostgresCredencial
from src.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
from domains.Credenciales.postgres_credencial import PostgresCredencial
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
class MemoryConvPostgres(MemoryConvABC):
@@ -1,7 +1,7 @@
from src.Llms.Modelos.Base_model import ModeloABC
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Modelos.Base_model import ModeloABC
from domains.Security.GenerarIDs import GeneradorIDUnico
from typing import AsyncGenerator, Union
from src.ConexionApis.Ollama_cliente import OllamaCliente # Asegúrate de importar correctamente
from domains.ConexionApis.Ollama_cliente import OllamaCliente # Asegúrate de importar correctamente
import asyncio
class ModeloOllama(ModeloABC):
@@ -1,6 +1,6 @@
from src.Llms.Modelos.Base_model import ModeloABC
from src.ConexionApis.OpenAi_conexion import OpenAICliente
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Modelos.Base_model import ModeloABC
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
from domains.Security.GenerarIDs import GeneradorIDUnico
import asyncio
from typing import AsyncGenerator, Union
@@ -2,15 +2,15 @@ import os
from dotenv import load_dotenv
from sqlalchemy import Column, Integer, String, Float, Boolean
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from typing import Optional
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
# ----------------------
# Cargar clave maestra
@@ -3,17 +3,17 @@ from sqlalchemy import Column, Integer, String, Text, TIMESTAMP
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import SQLAlchemyError
from src.ArquitectureLayer.Model import Model_base
from src.ConexionSql.Postgres_conexion import PostgresConexion
from src.Credenciales.postgres_credencial import PostgresCredencial
from domains.ArquitectureLayer.Model import Model_base
from domains.ConexionSql.Postgres_conexion import PostgresConexion
from domains.Credenciales.postgres_credencial import PostgresCredencial
class LoggerDB:
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
if not LoggerDB._sink_removido:
logger.remove() # 🧹 elimina impresión en terminal
LoggerDB._sink_removido = True
# 🔥 Elimina todos los sinks activos, incluso los automáticos
logger.remove()
self.conexion = PostgresConexion(credencial)
self.engine = self.conexion.get_engine()
@@ -28,6 +28,8 @@ class LoggerDB:
def _generar_modelo_logger(self):
class LoggerTable(Model_base):
__tablename__ = self.nombre_tabla
__table_args__ = {'extend_existing': True} # 👈 Esta línea evita el error
id = Column(Integer, primary_key=True)
nivel = Column(String, nullable=False)
mensaje = Column(Text, nullable=False)
+190
View File
@@ -0,0 +1,190 @@
from typing import TYPE_CHECKING, Optional
import random
import asyncio
import json
if TYPE_CHECKING:
from .Tab import Tab
class ElementoWeb:
def __init__(self, tab: "Tab", object_id: Optional[str]):
self.tab = tab
self.object_id = object_id
self._node_id = None # Lazy resolved
@classmethod
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
inst = cls(tab, object_id=None)
inst._node_id = node_id
return inst
async def _asegurar_object_id(self):
if not self.object_id and self._node_id:
try:
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
self.object_id = resolved["object"]["objectId"]
except Exception as e:
print(f"⚠️ No se pudo resolver objectId desde nodeId: {e}")
async def scroll_into_view(self):
try:
await self._asegurar_object_id()
await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
"awaitPromise": True
})
if self.tab.verbose:
print("📜 Elemento desplazado a la vista.")
except Exception as e:
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
async def click(self):
try:
await self.scroll_into_view()
await self._asegurar_object_id()
if not self.object_id:
raise ValueError("No se puede obtener objectId del elemento para hacer click.")
# Intenta obtener coordenadas del nodo
node_result = await self.tab._enviar("DOM.describeNode", {
"objectId": self.object_id
})
node_id = node_result["node"]["nodeId"]
try:
box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id})
content = box_model["model"]["content"]
x = (content[0] + content[2]) / 2
y = (content[1] + content[5]) / 2
except:
quads_result = await self.tab._enviar("DOM.getContentQuads", {"nodeId": node_id})
quad = quads_result["quads"][0]
x = (quad[0] + quad[4]) / 2
y = (quad[1] + quad[5]) / 2
# 🧠 Enfocar el elemento antes de clickear
await self.tab._enviar("DOM.focus", {
"objectId": self.object_id
})
# 🎯 Movimiento humanoide opcional
start_x, start_y = x + random.uniform(-100, 100), y + random.uniform(-100, 100)
steps = random.randint(5, 12)
for i in range(1, steps + 1):
curr_x = start_x + (x - start_x) * i / steps + random.uniform(-1, 1)
curr_y = start_y + (y - start_y) * i / steps + random.uniform(-1, 1)
await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mouseMoved",
"x": curr_x,
"y": curr_y,
})
await asyncio.sleep(random.uniform(0.01, 0.05))
# 👆 Mouse Down
await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mousePressed",
"x": x,
"y": y,
"button": "left",
"clickCount": 1
})
await asyncio.sleep(random.uniform(0.05, 0.15))
# 👇 Mouse Up
await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mouseReleased",
"x": x,
"y": y,
"button": "left",
"clickCount": 1
})
await asyncio.sleep(random.uniform(0.01, 0.05))
# 🖱️ Click manual adicional
await self.tab._enviar("Input.dispatchMouseEvent", {
"type": "mouseClicked",
"x": x,
"y": y,
"button": "left",
"clickCount": 1
})
if self.tab.verbose:
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})")
except Exception as e:
print(f"⚠️ Error al hacer click físico: {e}")
print("🧪 Intentando fallback con JavaScript click()...")
await self.click_js()
async def click_js(self):
try:
await self._asegurar_object_id()
if not self.object_id:
print("⚠️ No se puede hacer click JS: objectId no disponible.")
return
await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": "function() { this.click(); }",
"awaitPromise": True
})
if self.tab.verbose:
print("🖱️ Click simulado por JavaScript (element.click())")
except Exception as e:
print(f"⚠️ Error al ejecutar click en JS: {e}")
async def obtener_texto(self) -> Optional[str]:
try:
await self._asegurar_object_id()
result = await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": "function() { return this.textContent; }",
"returnByValue": True
})
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al obtener texto del elemento: {e}")
return None
async def escribir_texto(self, texto: str):
try:
await self._asegurar_object_id()
await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": f"function() {{ this.value = {json.dumps(texto)}; this.dispatchEvent(new Event('input')); }}",
"awaitPromise": True
})
if self.tab.verbose:
print(f"⌨️ Texto escrito en elemento: '{texto}'")
except Exception as e:
print(f"⚠️ Error al escribir texto: {e}")
async def encontrar_hijo_clickeable(self) -> Optional["ElementoWeb"]:
try:
await self._asegurar_object_id()
resultado = await self.tab._enviar("Runtime.callFunctionOn", {
"objectId": self.object_id,
"functionDeclaration": """
function() {
const candidatos = this.querySelectorAll("span, div, a, button");
for (const el of candidatos) {
const style = window.getComputedStyle(el);
const visible = style.display !== "none" && style.visibility !== "hidden";
const interactivo = style.pointerEvents !== "none";
if (visible && interactivo) return el;
}
return this;
}
""",
"returnByValue": False
})
if "result" in resultado and "objectId" in resultado["result"]:
return ElementoWeb(self.tab, resultado["result"]["objectId"])
except Exception as e:
print(f"⚠️ No se pudo encontrar hijo clickeable: {e}")
return None
@@ -87,9 +87,9 @@ class Navegador:
f"--user-data-dir={self.user_data_dir}",
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-web-security",
# "--disable-web-security",
# "--disable-extensions",
"--disable-dev-shm-usage",
# "--disable-dev-shm-usage",
"--disable-infobars",
"--disable-popup-blocking",
"--disable-default-apps",
+138
View File
@@ -0,0 +1,138 @@
import aiohttp
import websockets
import json
import asyncio
from .Tab import Tab
from typing import Optional
class Scrapper:
def __init__(self, debugging_url: str = "http://127.0.0.1:9222"):
self.debugging_url = debugging_url
self.tabs: list[Tab] = []
async def _crear_tab_websocket_url(self, target_url: str = "about:blank") -> str:
"""
Crea una nueva pestaña usando el método oficial Target.createTarget
y devuelve su WebSocketDebuggerUrl.
"""
# 1. Obtener el WebSocket general del browser (root)
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.debugging_url}/json/version") as resp:
if resp.status != 200:
raise RuntimeError("No se pudo obtener información del navegador")
data = await resp.json()
browser_ws_url = data["webSocketDebuggerUrl"]
# 2. Conectarse al WebSocket del browser
async with websockets.connect(browser_ws_url) as websocket:
# 3. Enviar comando para crear target
msg_id = 1
await websocket.send(json.dumps({
"id": msg_id,
"method": "Target.createTarget",
"params": {
"url": target_url,
"newWindow": False
}
}))
# 4. Esperar respuesta con el targetId
while True:
respuesta = await websocket.recv()
data = json.loads(respuesta)
if data.get("id") == msg_id:
target_id = data["result"]["targetId"]
break
# 5. Esperar a que el target aparezca en /json
for _ in range(30): # máximo ~3 segundos
await asyncio.sleep(0.1)
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.debugging_url}/json") as resp:
if resp.status == 200:
tabs = await resp.json()
for tab in tabs:
if tab.get("id") == target_id:
return tab["webSocketDebuggerUrl"]
raise RuntimeError("No se pudo obtener el WebSocket de la nueva pestaña")
async def nueva_tab(self, url: str = "", wait_time: float = 5.0) -> Tab:
websocket_url = await self._crear_tab_websocket_url()
tab = await Tab.crear_desde_websocket(websocket_url)
self.tabs.append(tab)
if url:
print(f"🌍 Navegando a: {url}")
await tab.navegar(url, wait_time)
else:
print("⚠️ No se especificó URL. La pestaña se creó pero no se navegó a ninguna página.")
return tab
async def cerrar_todos(self):
for tab in list(self.tabs):
await tab.cerrar()
self.tabs.clear()
def get_tab(self, identifier: str) -> Optional[Tab]:
"""
Devuelve una instancia de Tab según su WebSocket URL o su ID final (extraído del WebSocket URL).
Acepta:
- ws_url completo: ws://127.0.0.1:9222/devtools/page/XYZ
- id directo: XYZ
"""
for tab in self.tabs:
# Comparar directamente contra ws_url
if tab.ws_url == identifier:
return tab
# Comparar contra el ID extraído
ws_id = tab.ws_url.rsplit("/", 1)[-1]
if ws_id == identifier:
return tab
return None
async def obtener_tabs_existentes(self) -> list[Tab]:
"""
Recupera todas las pestañas de tipo 'page' que no están ya en self.tabs,
las conecta y devuelve como lista. Muestra resumen limpio por consola.
"""
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.debugging_url}/json") as resp:
if resp.status != 200:
raise RuntimeError("No se pudo obtener la lista de pestañas")
tabs_info = await resp.json()
print("\n🧾 Pestañas activas (filtradas: solo 'type': 'page'):\n")
nuevas_tabs = []
for idx, tab_info in enumerate(tabs_info, start=1):
tipo = tab_info.get("type")
if tipo != "page":
continue # Filtrar todo lo que no sea página visible
ws_url = tab_info.get("webSocketDebuggerUrl")
tab_id = tab_info.get("id")
title = tab_info.get("title", "<Sin título>")
url = tab_info.get("url", "<Sin URL>")
# Verifica si ya la tienes cargada
if any(t.ws_url == ws_url for t in self.tabs):
continue
# Conectar
try:
tab = await Tab.crear_desde_websocket(ws_url)
self.tabs.append(tab)
nuevas_tabs.append(tab)
except Exception as e:
print(f"⚠️ No se pudo conectar a pestaña {tab_id}: {e}")
if not nuevas_tabs:
print("⚠️ No se encontraron nuevas pestañas para agregar.\n")
return nuevas_tabs
@@ -2,21 +2,29 @@ import asyncio
import json
import base64
import websockets
from typing import Optional
from typing import List
from src.ScrappingWeb.ElementoWeb import ElementoWeb
from typing import Optional, List
from .ElementoWeb import ElementoWeb
import os
class Tab:
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str):
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str, verbose: bool = True):
self.websocket = websocket
self.ws_url = ws_url
self._message_id = 0
self._pending = {}
self._load_event = asyncio.Event()
self.verbose = verbose
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
await self.cerrar()
@classmethod
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
websocket = await websockets.connect(ws_url)
websocket = await websockets.connect(ws_url, max_size=10 * 1024 * 1024)
tab = cls(websocket, ws_url)
asyncio.create_task(tab._recibir_eventos())
await tab._enviar("Page.enable")
@@ -28,11 +36,14 @@ class Tab:
data = json.loads(mensaje)
if "id" in data and data["id"] in self._pending:
future = self._pending.pop(data["id"])
future.set_result(data.get("result"))
if "result" in data:
future.set_result(data["result"])
elif "error" in data:
future.set_exception(Exception(data["error"]))
elif data.get("method") == "Page.loadEventFired":
self._load_event.set()
async def _enviar(self, metodo: str, parametros: Optional[dict] = None) -> dict:
async def _enviar(self, metodo: str, parametros: Optional[dict] = None, timeout: float = 10.0) -> dict:
self._message_id += 1
msg_id = self._message_id
mensaje = {
@@ -44,15 +55,17 @@ class Tab:
future = asyncio.get_event_loop().create_future()
self._pending[msg_id] = future
await self.websocket.send(json.dumps(mensaje))
return await future
return await asyncio.wait_for(future, timeout=timeout)
async def navegar(self, url: str, wait_time: float = 5.0):
self._load_event.clear()
print(f"🌍 Navegando a: {url}")
if self.verbose:
print(f"🌍 Navegando a: {url}")
await self._enviar("Page.navigate", {"url": url})
try:
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
print("✅ Página cargada correctamente.")
if self.verbose:
print("✅ Página cargada correctamente.")
except asyncio.TimeoutError:
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
@@ -62,11 +75,40 @@ class Tab:
"expression": js_code,
"returnByValue": True
})
return result["result"]["value"]
if "exceptionDetails" in result:
raise Exception(result["exceptionDetails"])
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al ejecutar JS: {e}")
return None
async def inyectar_archivo_js(self, ruta_archivo: str, reemplazos: dict = None) -> Optional[str]:
if not os.path.exists(ruta_archivo):
print(f"❌ Archivo JS no encontrado: {ruta_archivo}")
return None
with open(ruta_archivo, "r", encoding="utf-8") as f:
js_code = f.read()
if reemplazos:
for key, value in reemplazos.items():
js_code = js_code.replace(f"{{{{{key}}}}}", str(value))
# 🔧 Eliminamos el `return` externo
js_code_final = f"(async () => {{\n{js_code}\n}})();"
try:
result = await self._enviar("Runtime.evaluate", {
"expression": js_code_final,
"returnByValue": True
})
if "exceptionDetails" in result:
raise Exception(result["exceptionDetails"])
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al inyectar JS desde {ruta_archivo}: {e}")
return None
async def obtener_user_agent(self) -> Optional[str]:
return await self.evaluar_js("navigator.userAgent")
@@ -76,66 +118,57 @@ class Tab:
data = result["data"]
with open(output_path, "wb") as f:
f.write(base64.b64decode(data))
print(f"📸 Screenshot guardado como {output_path}")
if self.verbose:
print(f"📸 Screenshot guardado como {output_path}")
except Exception as e:
print(f"⚠️ Error al capturar screenshot: {e}")
async def cerrar(self):
try:
await self.websocket.close()
print("🛑 WebSocket cerrado.")
if not self.websocket.closed:
await self.websocket.close()
if self.verbose:
print("🛑 WebSocket cerrado.")
except Exception as e:
print(f"⚠️ Error al cerrar pestaña: {e}")
async def obtener_html_completo(self) -> Optional[str]:
"""
Devuelve el HTML completo de la página actual.
"""
try:
result = await self._enviar("Runtime.evaluate", {
"expression": "document.documentElement.outerHTML",
"returnByValue": True
})
html = result["result"]["value"]
print("📄 HTML completo obtenido.")
return html
return result.get("result", {}).get("value")
except Exception as e:
print(f"⚠️ Error al obtener HTML: {e}")
return None
async def obtener_dominio(self) -> Optional[str]:
"""
Devuelve el dominio (hostname) de la página actual, por ejemplo: 'example.com'.
"""
try:
dominio = await self.evaluar_js("window.location.hostname")
print(f"🌐 Dominio actual: {dominio}")
if self.verbose and dominio:
print(f"🌐 Dominio actual: {dominio}")
return dominio
except Exception as e:
print(f"⚠️ Error al obtener dominio: {e}")
return None
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
try:
# Obtener nodo raíz del documento
doc = await self._enviar("DOM.getDocument")
root_node_id = doc["root"]["nodeId"]
# Buscar el nodo desde el DOM (más confiable que Runtime.evaluate)
result = await self._enviar("DOM.querySelector", {
"nodeId": root_node_id,
"selector": selector
})
node_id = result["nodeId"]
node_id = result.get("nodeId")
if not node_id:
print(f"⚠️ Nodo no encontrado con selector: {selector}")
return None
return ElementoWeb.from_node(self, node_id=node_id)
except Exception as e:
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
return None
@@ -157,8 +190,17 @@ class Tab:
for prop in props["result"]:
if "value" in prop and "objectId" in prop["value"]:
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
if self.verbose:
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
return elementos
except Exception as e:
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
return []
return []
async def enfocar(self):
try:
await self._enviar("Page.bringToFront")
if self.verbose:
print("🪟 Pestaña enfocada (bringToFront).")
except Exception as e:
print(f"⚠️ Error al enfocar pestaña: {e}")
@@ -1,11 +1,11 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
from domains.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
from typing import List, Optional
from src.ConexionSql.Base_conexion import ConexionBase
from domains.ConexionSql.Base_conexion import ConexionBase
from sqlalchemy import MetaData # Asegúrate de importar esto
from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
from domains.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
from sqlalchemy import inspect
from src.base import Base
from domains.base import Base
class Biblioteca:
@@ -3,16 +3,16 @@ import base64
from dotenv import load_dotenv
from sqlalchemy import Column, String, Integer
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base
from src.Security.Encriptar import Encriptar_fernet
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Base_Embedder import EmbedderABC
from src.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
from domains.ConexionSql.Base_conexion import ConexionBase
from domains.base import Base
from domains.Security.Encriptar import Encriptar_fernet
from domains.Security.GenerarIDs import GeneradorIDUnico
from domains.Llms.Embedders.Base_Embedder import EmbedderABC
from domains.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
# ----------------------
# Cargar clave maestra
@@ -1,4 +1,4 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from domains.Security.GenerarIDs import GeneradorIDUnico
from typing import List
class Nota:
@@ -3,18 +3,42 @@ from dotenv import load_dotenv
from sqlalchemy import Table, Column, String, Text, MetaData
from pgvector.sqlalchemy import Vector
from sqlalchemy.orm import registry, Session
from src.TextManager.nota import Nota
from src.ConexionSql.Base_conexion import ConexionBase
from domains.TextManager.nota import Nota
from domains.ConexionSql.Base_conexion import ConexionBase
from typing import Tuple
import re
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Repo import Repo_base
from domains.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
from src.base import Base # Este es tu declarative_base()
titulo = os.getenv('DB_TITLE')
usuario = os.getenv('DB_USER')
passwrd = os.getenv('DB_PASSWORD')
host = os.getenv('DB_HOST')
port = os.getenv('DB_PORT')
db_name = os.getenv('DB_NAME')
db_credencial = PostgresCredencial(
titulo=titulo,
user=usuario,
password=passwrd,
host=host,
port=port,
dbname=db_name
)
# from entrypoint.init_db import db_credencial
from domains.Logger.logger_db import LoggerDB, logger
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
from domains.base import Base # Este es tu declarative_base()
# ----------------------
# Cargar .env
@@ -37,11 +61,11 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema.
"""
try:
print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
logger.info(f"Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
# Nombre SQL-safe
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
print(f"[DEBUG] Nombre de tabla SQL-safe: '{nombre_tabla}'")
logger.debug(f"Nombre de tabla SQL-safe: '{nombre_tabla}'")
# Modelo ORM dinámico
class NotaModel(Base, Model_base):
@@ -57,15 +81,15 @@ def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int,
vector = Column(Vector(vector_dim), nullable=True)
vector_resumen = Column(Vector(vector_dim), nullable=True)
print(f"[INFO] Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
logger.info(f"Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
logger.debug(f"Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
logger.debug(f"Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
logger.debug(f"Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
return NotaModel.__table__, NotaModel
except Exception as e:
print(f"[ERROR] Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
logger.error(f"Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
raise
# ----------------------
+15
View File
@@ -0,0 +1,15 @@
class Usuario:
def __init__(self, id: int, nombre: str, email: str, activo: bool = True):
self.id = id
self.nombre = nombre
self.email = email
self.activo = activo
def activar(self):
self.activo = True
def desactivar(self):
self.activo = False
def __repr__(self):
return f"Usuario(id={self.id}, nombre='{self.nombre}', email='{self.email}', activo={self.activo})"
+82
View File
@@ -0,0 +1,82 @@
from sqlalchemy import Column, Integer, String, Boolean
from domains.ArquitectureLayer.Model import Model_base
from domains.ArquitectureLayer.Mapper import Mapper_base
from domains.ArquitectureLayer.Repo import Repo_base
from domains.Usuario.usuario import Usuario
# ----------------------
# MODELO (SQLAlchemy)
# ----------------------
class UsuarioModel(Model_base):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True, autoincrement=True)
nombre = Column(String, nullable=False)
email = Column(String, unique=True, nullable=False)
activo = Column(Boolean, default=True, nullable=False)
# ----------------------
# MAPPER
# ----------------------
class UsuarioMapper(Mapper_base[Usuario, UsuarioModel]):
@staticmethod
def to_model(obj: Usuario) -> UsuarioModel:
return UsuarioModel(
id=obj.id,
nombre=obj.nombre,
email=obj.email,
activo=obj.activo
)
@staticmethod
def from_model(model: UsuarioModel) -> Usuario:
return Usuario(
id=model.id,
nombre=model.nombre,
email=model.email,
activo=model.activo
)
@staticmethod
def to_dict(obj: Usuario) -> dict:
return {
'id': obj.id,
'nombre': obj.nombre,
'email': obj.email,
'activo': obj.activo
}
@staticmethod
def from_dict(data: dict) -> Usuario:
return Usuario(
id=data['id'],
nombre=data['nombre'],
email=data['email'],
activo=data.get('activo', True)
)
@staticmethod
def from_model_list(models: list[UsuarioModel]) -> list[Usuario]:
return [UsuarioMapper.from_model(m) for m in models]
# ----------------------
# REPO
# ----------------------
class UsuarioRepo(Repo_base[UsuarioModel, Usuario]):
def __init__(self, session):
super().__init__(
session=session,
modelo=UsuarioModel,
mapper=UsuarioMapper
)
def get_by_email(self, email: str) -> Usuario | None:
model = (
self.session.query(self.Modelo)
.filter_by(email=email, sys_deleted_at=None)
.first()
)
return self.Mapper.from_model(model) if model else None
View File
+8 -8
View File
@@ -1,14 +1,14 @@
# entrypoint/init_db.py
from src.base import Base
from src.ConexionSql.Postgres_conexion import PostgresConexion # Asegúrate de tener esta clase implementada correctamente
from src.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
from domains.base import Base
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Asegúrate de tener esta clase implementada correctamente
from domains.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
from src.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
from src.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
from src.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
from src.TextManager.biblioteca_mmr import BibliotecaModel
from domains.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
from domains.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
from domains.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
from domains.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
from domains.TextManager.biblioteca_mmr import BibliotecaModel
from dotenv import load_dotenv
+2931 -35
View File
File diff suppressed because it is too large Load Diff
+12 -4
View File
@@ -20,18 +20,25 @@
"storybook:build": "storybook build"
},
"dependencies": {
"@mantine/core": "8.0.0",
"@mantine/hooks": "8.0.0",
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mantine/tiptap": "^8.0.1",
"@react-three/fiber": "^9.1.2",
"@tabler/icons": "^3.31.0",
"@tabler/icons-react": "^3.31.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@uiw/react-markdown-preview": "^5.1.4",
"@uiw/react-md-editor": "^4.0.7",
"axios": "^1.9.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"marked": "^15.0.12",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-rnd": "^10.5.2",
"react-router-dom": "^7.4.0"
"react-router-dom": "^7.4.0",
"turndown": "^7.2.0"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
@@ -46,6 +53,7 @@
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/three": "^0.176.0",
"@types/turndown": "^5.0.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.23.0",
"eslint-config-mantine": "^4.0.3",
+66 -20
View File
@@ -1,39 +1,85 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { HomePage } from './pages/Home.page';
import { Consulta_API } from './pages/Consulta_api';
import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta
import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta
import { Biblioteca } from './pages/Biblioteca';
import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
import { Camara_noir } from './pages/Camaras_noir';
import { HomePage } from './frontend_domains/Home/Home.page';
import { Consulta_API } from './frontend_domains/Experiments/Consulta_api';
import { Error_404 } from './frontend_domains/FitzStudio/404/404'; // Ajusta si está en otra carpeta
import { Grid_Dashboard } from './frontend_domains/Experiments/Grid_dashboard'; // Ajusta si está en otra carpeta
import { Biblioteca } from './frontend_domains/TextEditor/Biblioteca';
import { VisualizacionesRandom } from './frontend_domains/Experiments/Visualizaciones_Random';
import { Camara_noir } from './frontend_domains/CamaraNoir/Camaras_noir';
import EditorTest from "./frontend_domains/TextEditor/Editor_Test";
import { ChatPage } from './frontend_domains/Llms/Chat/ChatPage';
import { LoginPage } from './frontend_domains/Usuarios/Login.page';
const router = createBrowserRouter([
// Home Principal
{
path: '/',
element: <HomePage />,
},
// Biblioteca
{
path: '/Consulta_API',
element: <Consulta_API />,
},
{
path: '/Grid_Dashboard',
element: <Grid_Dashboard />,
},
{
path: '/Biblioteca',
path: '/bibliot/Biblioteca',
element: <Biblioteca />,
},
{
path: '/analytics/Visualizaciones_Random',
element: <VisualizacionesRandom />,
path: '/bibliot/editortest',
element: <EditorTest />,
},
// Chat LLM
{
path: '/analytics/Camaras',
path: '/llms/chat',
element: <ChatPage />,
},
// CamaraNoir
{
path: '/camara/principal',
element: <Camara_noir />,
},
// Experimentos
{
path: '/experiments/Consulta_API',
element: <Consulta_API />,
},
{
path: '/experiments/Grid_Dashboard',
element: <Grid_Dashboard />,
},
{
path: '/experiments/Visualizaciones_Random',
element: <VisualizacionesRandom />,
},
// Login
{
path: '/login',
element: <LoginPage />,
},
// FitzStudio Pages -------------------------------------------------------
// Error 404
{
path: '*',
element: <Error_404 />,
+6 -1
View File
@@ -1,3 +1,5 @@
// https://tabler.io/icons
// OUTLINED
export { default as IconArrowLeft } from './outlined/arrow-left.svg?react';
export { default as IconHomeOutline } from './outlined/home.svg?react';
@@ -11,7 +13,10 @@ export { default as IconSettings } from './outlined/settings.svg?react';
export { default as IconArrowBarLeft } from './outlined/arrow-bar-left.svg?react';
export { default as IconArrowBarRight } from './outlined/arrow-bar-right.svg?react';
export { default as IconCheck } from './outlined/check.svg?react';
export { default as CameraPlus } from './outlined/camera-plus.svg?react';
export { default as Flask } from './outlined/flask.svg?react';
export { default as Users } from './outlined/users.svg?react';
export { default as IconNotebook } from './outlined/notebook.svg?react';
// FILLED
export { default as IconHomeFilled } from './filled/home.svg?react';
@@ -1,26 +0,0 @@
// Archivo: components/CanvasDisplay.tsx
import { Box } from '@mantine/core';
import { RefObject } from 'react';
interface CanvasDisplayProps {
canvasRef: RefObject<HTMLCanvasElement>;
}
export function CanvasDisplay({ canvasRef }: CanvasDisplayProps) {
return (
<Box style={{ position: 'relative', width: '100%', height: 480 }}>
<canvas
ref={canvasRef}
width={640}
height={480}
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
borderRadius: 8,
backgroundColor: '#000',
}}
/>
</Box>
);
}
@@ -1,74 +0,0 @@
// Archivo: components/CaptureGrid.tsx
import { useCallback } from 'react';
import { SimpleGrid } from '@mantine/core';
import { FrameCard } from './FrameCard';
interface CaptureGridProps {
totalSlots: number;
capturas: string[][];
setCapturas: (value: string[][]) => void;
frameIndices: number[];
setFrameIndices: (value: number[]) => void;
fijados: boolean[];
setFijados: (value: boolean[]) => void;
frameTemporal: number | null;
onScroll: (e: React.WheelEvent, index: number) => void;
onSliderChange: (val: number) => void;
onSliderEnd: (index: number, val: number) => void;
numColumnas: number;
}
export function CaptureGrid({
totalSlots,
capturas,
setCapturas,
frameIndices,
setFrameIndices,
fijados,
setFijados,
frameTemporal,
onScroll,
onSliderChange,
onSliderEnd,
numColumnas,
}: CaptureGridProps) {
const handleImageClick = useCallback((index: number, dataUrl: string) => {
const nuevasCapturas = [...capturas];
nuevasCapturas[index] = [dataUrl];
setCapturas(nuevasCapturas);
const nuevosFijados = [...fijados];
nuevosFijados[index] = true;
setFijados(nuevosFijados);
const nuevosIndices = [...frameIndices];
nuevosIndices[index] = 0;
setFrameIndices(nuevosIndices);
}, [capturas, fijados, frameIndices, setCapturas, setFijados, setFrameIndices]);
return (
<SimpleGrid cols={numColumnas} style={{ flex: 1, columnGap: 12, rowGap: 4 }}>
{Array.from({ length: totalSlots }).map((_, i) => {
const frames = capturas[i] ?? [];
const currentIndex =
!fijados[i] && frameTemporal !== null && frames.length > 0
? Math.max(0, Math.min(frames.length - 1, frameTemporal))
: frameIndices[i];
return (
<FrameCard
key={i}
index={i}
frames={frames}
currentIndex={currentIndex}
fijado={fijados[i]}
onScroll={onScroll}
onSliderChange={onSliderChange}
onSliderEnd={onSliderEnd}
onImageClick={handleImageClick}
/>
);
})}
</SimpleGrid>
);
}
@@ -1,25 +0,0 @@
// Archivo: components/ControlPanel.tsx
import { Button, Group } from '@mantine/core';
interface ControlPanelProps {
grabando: boolean;
onAlternar: () => void;
onLimpiar: () => void;
onDesfijar: () => void;
}
export function ControlPanel({ grabando, onAlternar, onLimpiar, onDesfijar }: ControlPanelProps) {
return (
<Group gap="sm">
<Button onClick={onAlternar} variant="light" color={grabando ? 'orange' : 'blue'}>
{grabando ? 'Grabando... (Presiona Espacio)' : 'Iniciar grabación'}
</Button>
<Button onClick={onLimpiar} variant="filled" color="red">
Eliminar imágenes
</Button>
<Button onClick={onDesfijar} variant="default" color="gray">
Desfijar todos (D)
</Button>
</Group>
);
}
@@ -1,125 +0,0 @@
// Archivo: components/FrameCard.tsx
import { Box, HoverCard, Image, Slider, Stack, Text } from '@mantine/core';
interface FrameCardProps {
index: number;
frames: string[];
currentIndex: number;
fijado: boolean;
onScroll: (e: React.WheelEvent, index: number) => void;
onSliderChange: (val: number) => void;
onSliderEnd: (index: number, val: number) => void;
onImageClick?: (index: number, dataUrl: string) => void;
}
export function FrameCard({
index,
frames,
currentIndex,
fijado,
onScroll,
onSliderChange,
onSliderEnd,
onImageClick,
}: FrameCardProps) {
const borderColor = fijado ? '#4caf50' : '#ccc';
return (
<HoverCard key={index} width={700} shadow="md" position="top" withArrow>
<HoverCard.Target>
<Box
style={{
width: 160,
height: 120,
borderRadius: 8,
border: `2px solid ${borderColor}`,
overflow: 'hidden',
position: 'relative',
background: '#f1f1f1',
cursor: 'pointer',
}}
>
{frames.length > 0 && currentIndex < frames.length && (
<>
<Image
src={frames[currentIndex]}
alt={`Captura ${index}`}
width="100%"
height="100%"
fit="cover"
/>
<Text
size="xs"
style={{
position: 'absolute',
bottom: 4,
right: 6,
backgroundColor: 'rgba(0,0,0,0.6)',
color: 'white',
borderRadius: 4,
padding: '2px 4px',
fontSize: 10,
}}
>
{currentIndex + 1} / {frames.length}
</Text>
</>
)}
</Box>
</HoverCard.Target>
<HoverCard.Dropdown p="xs" onWheel={(e) => onScroll(e, index)}>
{frames.length > 0 && currentIndex < frames.length && (
<Stack gap={6}>
<Image
src={frames[currentIndex]}
alt={`Vista ampliada ${index}`}
style={{ width: '100%', height: 'auto', maxWidth: '100%', cursor: 'zoom-in' }}
fit="contain"
onClick={async (e) => {
const imgElement = e.currentTarget;
const img = document.createElement('img');
img.src = frames[currentIndex];
await img.decode();
const rect = imgElement.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const ratioX = clickX / rect.width;
const ratioY = clickY / rect.height;
const zoomFactor = 2;
const cropWidth = img.width / zoomFactor;
const cropHeight = img.height / zoomFactor;
const centerX = img.width * ratioX;
const centerY = img.height * ratioY;
const x = Math.max(0, Math.min(img.width - cropWidth, centerX - cropWidth / 2));
const y = Math.max(0, Math.min(img.height - cropHeight, centerY - cropHeight / 2));
const canvas = document.createElement('canvas');
canvas.width = cropWidth;
canvas.height = cropHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, x, y, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
const dataUrl = canvas.toDataURL('image/jpeg');
onImageClick?.(index, dataUrl);
}
}}
/>
<Slider
value={currentIndex}
onChange={(val) => onSliderChange(val)}
onChangeEnd={(val) => onSliderEnd(index, val)}
min={0}
max={frames.length - 1}
/>
</Stack>
)}
</HoverCard.Dropdown>
</HoverCard>
);
}
@@ -1,36 +0,0 @@
// Archivo: components/GridConfigPanel.tsx
import { NumberInput, Stack, Text } from '@mantine/core';
interface GridConfigPanelProps {
numFilas: number;
setNumFilas: (val: number) => void;
numColumnas: number;
setNumColumnas: (val: number) => void;
}
export function GridConfigPanel({ numFilas, setNumFilas, numColumnas, setNumColumnas }: GridConfigPanelProps) {
return (
<Stack align="center" gap="xs" ml="md">
<Text size="sm">Cartas</Text>
<NumberInput
value={numFilas}
onChange={(val) => setNumFilas(Number(val))}
min={1}
max={5}
step={1}
size="xs"
style={{ width: 60 }}
/>
<Text size="sm">Jugadores</Text>
<NumberInput
value={numColumnas}
onChange={(val) => setNumColumnas(Number(val))}
min={1}
max={11}
step={1}
size="xs"
style={{ width: 60 }}
/>
</Stack>
);
}
@@ -1,6 +0,0 @@
export * from './CanvasDisplay';
export * from './ControlPanel';
export * from './GridConfigPanel';
export * from './CaptureGrid';
export * from './FrameCard';
export * from './useCamaraNoir';
@@ -1,212 +0,0 @@
import { useEffect, useRef, useState } from 'react';
export function useCamaraNoir() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const bufferPrevioRef = useRef<string[]>([]);
const bufferGrabacionRef = useRef<string[]>([]);
const bufferAcumuladoRef = useRef<string[]>([]);
const pregrabacionActivaRef = useRef(true);
const primeraGrabacionRealizadaRef = useRef(false);
const [bufferGlobal, setBufferGlobal] = useState<string[]>([]);
const [intervaloId, setIntervaloId] = useState<ReturnType<typeof setInterval> | null>(null);
const [grabando, setGrabando] = useState(false);
const [capturas, setCapturas] = useState<string[][]>([]);
const [frameIndices, setFrameIndices] = useState<number[]>([]);
const [fijados, setFijados] = useState<boolean[]>([]);
const [frameTemporal, setFrameTemporal] = useState<number | null>(null);
const [numFilas, setNumFilas] = useState(2);
const [numColumnas, setNumColumnas] = useState(10);
const DELAY_ENTRE_FRAMES_MS = 10;
const SEGUNDOS_PRE_GRABACION = 5;
const FPS = 1000 / DELAY_ENTRE_FRAMES_MS;
const FRAMES_PRE_GRABACION = Math.floor(FPS * SEGUNDOS_PRE_GRABACION);
const totalSlots = numFilas * numColumnas;
useEffect(() => {
const id = setInterval(() => {
if (!pregrabacionActivaRef.current) return;
const canvas = canvasRef.current;
if (!canvas) return;
const frame = canvas.toDataURL('image/jpeg');
bufferPrevioRef.current.push(frame);
if (bufferPrevioRef.current.length > FRAMES_PRE_GRABACION) {
bufferPrevioRef.current.shift();
}
}, DELAY_ENTRE_FRAMES_MS);
return () => clearInterval(id);
}, []);
const desfijarTodos = () => {
setFijados(Array(totalSlots).fill(false));
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const socket = new WebSocket('ws://10.8.0.9:8000/ws');
socket.binaryType = 'blob';
socket.onmessage = async (event) => {
if (event.data instanceof Blob) {
const imgBitmap = await createImageBitmap(event.data);
ctx.drawImage(imgBitmap, 0, 0, canvas.width, canvas.height);
}
};
socket.onerror = (e) => console.error('WebSocket error:', e);
socket.onclose = () => console.warn('WebSocket cerrado');
return () => socket.close();
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.code === 'Space') {
event.preventDefault();
alternarGrabacion();
} else if (event.key.toLowerCase() === 'f') {
event.preventDefault();
limpiarCapturas();
} else if (event.key.toLowerCase() === 'd') {
event.preventDefault();
desfijarTodos();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [grabando, bufferGlobal, totalSlots]);
useEffect(() => {
const total = numFilas * numColumnas;
const nuevoCapturas = Array(total)
.fill(null)
.map((_, i) => capturas[i] ?? bufferGlobal);
const nuevoIndices = Array(total)
.fill(null)
.map((_, i) => frameIndices[i] ?? (frameTemporal ?? Math.floor(bufferGlobal.length / 2)));
const nuevosFijados = Array(total)
.fill(null)
.map((_, i) => fijados[i] ?? false);
setCapturas(nuevoCapturas);
setFrameIndices(nuevoIndices);
setFijados(nuevosFijados);
}, [numFilas, numColumnas]);
const iniciarGrabacion = () => {
if (grabando) return;
setGrabando(true);
bufferGrabacionRef.current = [];
if (!primeraGrabacionRealizadaRef.current) {
pregrabacionActivaRef.current = false;
}
const id = setInterval(() => {
const frame = capturarFrame();
if (frame) {
bufferGrabacionRef.current.push(frame);
}
}, DELAY_ENTRE_FRAMES_MS);
setIntervaloId(id);
};
const capturarFrame = (): string | null => {
const canvas = canvasRef.current;
if (!canvas) return null;
return canvas.toDataURL('image/jpeg');
};
const detenerGrabacion = () => {
if (!grabando) return;
setGrabando(false);
if (intervaloId) clearInterval(intervaloId);
const nuevaSesion = primeraGrabacionRealizadaRef.current
? [...bufferGrabacionRef.current] // solo frames nuevos
: [...bufferPrevioRef.current, ...bufferGrabacionRef.current];
bufferAcumuladoRef.current.push(...nuevaSesion);
primeraGrabacionRealizadaRef.current = true;
setBufferGlobal([...bufferAcumuladoRef.current]);
setCapturas(Array(totalSlots).fill([...bufferAcumuladoRef.current]));
const frameInicial = Math.floor(bufferAcumuladoRef.current.length / 2);
setFrameIndices(Array(totalSlots).fill(frameInicial));
setFijados(Array(totalSlots).fill(false));
setFrameTemporal(frameInicial);
};
const alternarGrabacion = () => {
grabando ? detenerGrabacion() : iniciarGrabacion();
};
const limpiarCapturas = () => {
setCapturas([]);
setFrameIndices([]);
setFijados([]);
setBufferGlobal([]);
bufferGrabacionRef.current = [];
bufferPrevioRef.current = [];
bufferAcumuladoRef.current = [];
primeraGrabacionRealizadaRef.current = false;
pregrabacionActivaRef.current = true;
setFrameTemporal(null);
if (intervaloId) clearInterval(intervaloId);
setGrabando(false);
};
const moverFrame = (capturaIndex: number, nuevoIndice: number) => {
setFrameIndices((prev) => {
const nuevos = [...prev];
const max = capturas[capturaIndex]?.length - 1 ?? 0;
nuevos[capturaIndex] = Math.max(0, Math.min(max, nuevoIndice));
return nuevos;
});
};
const moverFrameYFijar = (capturaIndex: number, nuevoIndice: number) => {
moverFrame(capturaIndex, nuevoIndice);
setFijados((prev) => {
const actualizados = [...prev];
actualizados[capturaIndex] = true;
return actualizados;
});
setFrameTemporal(nuevoIndice);
};
const manejarScrollEnSlider = (e: React.WheelEvent, index: number) => {
e.preventDefault();
if (!fijados[index]) return;
moverFrame(index, frameIndices[index] + (e.deltaY > 0 ? 1 : -1));
};
return {
canvasRef,
bufferGlobal,
grabando,
capturas,
frameIndices,
fijados,
frameTemporal,
numFilas,
setNumFilas,
numColumnas,
setNumColumnas,
alternarGrabacion,
limpiarCapturas,
desfijarTodos,
manejarScrollEnSlider,
moverFrameYFijar,
setFrameTemporal,
};
}
+8 -5
View File
@@ -4,17 +4,20 @@ import {
IconDeviceDesktopAnalytics,
IconFingerprint,
IconGauge,
IconNotebook,
IconHome2,
IconSettings,
IconUserOutline as IconUser,
CameraPlus,
Flask,
Users
} from '../assets/icons'; // ajusta según tu estructura de proyecto
export const mainLinksdata = [
{ icon: IconHome2, label: 'Home' },
{ icon: IconGauge, label: 'Dashboard' },
{ icon: IconDeviceDesktopAnalytics, label: 'Analytics' },
{ icon: IconCalendarStats, label: 'Releases' },
{ icon: IconUser, label: 'Account' },
{ icon: IconFingerprint, label: 'Security' },
{ icon: IconNotebook, label: 'Biblioteca' },
{ icon: Users, label: 'AgentesLLMs' },
{ icon: CameraPlus, label: 'CameraNoir' },
{ icon: Flask, label: 'Experimentos' },
{ icon: IconSettings, label: 'Settings' },
];
+35 -21
View File
@@ -1,35 +1,49 @@
// src/data/submenuLinks.ts
import { Biblioteca } from "@/frontend_domains/TextEditor/Biblioteca";
export const submenuLinks = {
// Home Principal
Home: [
{ label: 'Inicio', to: '/' },
{ label: 'Consulta Api', to: '/Consulta_API' },
{ label: 'Biblioteca', to: '/Biblioteca' },
],
Dashboard: [
{ label: 'Resumen', to: '/dashboard/resumen' },
{ label: 'Grid_Dashboard', to: '/Grid_Dashboard' },
{ label: 'Estadísticas', to: '/dashboard/estadisticas' },
{ label: 'Usuarios', to: '/dashboard/usuarios' },
// Biblioteca
Biblioteca: [
{ label: 'Biblioteca', to: '/bibliot/Biblioteca' },
{ label: 'test', to: '/bibliot/editortest' },
],
Analytics: [
{ label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' },
{ label: 'Camaras', to: '/analytics/Camaras' },
{ label: 'Tendencias', to: '/analytics/tendencias' },
// Experimentos
Experimentos: [
{ label: 'Consulta Api', to: '/experiments/Consulta_API' },
{ label: 'Visualizaciones_Random', to: '/experiments/Visualizaciones_Random' },
{ label: 'Grid_Dashboard', to: '/experiments/Grid_Dashboard' },
],
Releases: [
{ label: 'Notas de versión', to: '/releases/notas-de-version' },
{ label: 'Historial', to: '/releases/historial' },
// Camara
CameraNoir: [
{ label: 'Camara_principal', to: '/camara/principal' },
],
Account: [
{ label: 'Perfil', to: '/account/perfil' },
{ label: 'Suscripciones', to: '/account/suscripciones' },
],
Security: [
{ label: 'Contraseña', to: '/security/contraseña' },
{ label: '2FA', to: '/security/2fa' },
// LLms
AgentesLLMs: [
{ label: 'LLMs', to: '/llms' },
{ label: 'Chat', to: '/llms/chat' },
{ label: 'Documentos', to: '/llms/documentos' },
],
// Settings
Settings: [
{ label: 'Preferencias', to: '/settings/preferencias' },
{ label: 'Notificaciones', to: '/settings/notificaciones' },
@@ -0,0 +1,39 @@
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
import { Card, Text, Container } from '@mantine/core';
export function Camara_noir() {
return (
<AppShellWithMenu>
<Container
size="lg"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
}}
>
<Card shadow="sm" padding="xl" radius="md" withBorder>
<Text size="lg" mb="md">
Cámara Noir en Vivo
</Text>
<img
src="http://10.8.0.9:8000/video"
alt="Stream MJPEG en vivo desde Raspberry Pi"
style={{
width: '640px',
height: '480px',
borderRadius: '8px',
border: '1px solid #ccc',
objectFit: 'cover',
}}
/>
<Text size="sm" color="dimmed" mt="sm">
Transmisión MJPEG en vivo vía FastAPI / libcamera-vid
</Text>
</Card>
</Container>
</AppShellWithMenu>
);
}
@@ -1,6 +1,6 @@
import { LlamadorAPI } from '../components/LlamadorAPI';
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { LlamadorAPI } from './LlamadorAPI';
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
export function Consulta_API() {
@@ -1,6 +1,6 @@
import { Grid } from '@mantine/core';
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { GridDashboard } from '../components/Grid_dashboard';
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
import { GridDashboard } from './Grid_dashboard_component';
export function Grid_Dashboard() {
return (
@@ -1,5 +1,5 @@
import { Select, Group } from '@mantine/core';
import { IconCheck } from '../assets/icons';
import { IconCheck } from '../../assets/icons';
interface MetodoSelectProps {
metodo: string;
@@ -1,4 +1,4 @@
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
import { Card, Grid, Title, Loader } from '@mantine/core';
import { useEffect, useState } from 'react';
import ReactECharts from 'echarts-for-react';
@@ -10,7 +10,7 @@ function useChartOption(endpoint: string) {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/${endpoint}`)
fetch(`/api/v1/charts/${endpoint}`)
.then((res) => res.json())
.then((json) => setOption(json))
.catch(console.error)
@@ -1,9 +1,10 @@
import { Box, Title, Text, Button, Group, Stack, Image, Center } from '@mantine/core';
import { useMantineTheme } from '@mantine/core';
import { IconArrowLeft } from '../assets/icons';
import { IconArrowLeft } from '../../../assets/icons';
import { Link } from 'react-router-dom';
import { MantineCardWithShader } from '../components/HoloShader'; // Ajusta ruta si es necesario
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { MantineCardWithShader } from './HoloShader_404'; // Ajusta ruta si es necesario
import { AppShellWithMenu } from '../Appshell/Appshell';
export function Error_404() {
@@ -11,9 +11,9 @@ import { useDisclosure, useMediaQuery } from '@mantine/hooks';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { default as LogoIcon } from '../../assets/icons/favicon';
import { mainLinksdata } from '../../data/navigationsLinks_1';
import { submenuLinks } from '../../data/submenuLinks_1';
import { default as LogoIcon } from '../../../assets/icons/favicon';
import { mainLinksdata } from '../../../data/navigationsLinks_1';
import { submenuLinks } from '../../../data/submenuLinks_1';
import classes from './Appshell.module.css';
@@ -1,4 +1,4 @@
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { AppShellWithMenu } from './Appshell/Appshell';
export function Plantilla() {

Some files were not shown because too many files have changed in this diff Show More