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.
This commit is contained in:
@@ -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 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()
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# backend/api/router.py
|
||||
|
||||
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.include_router(ping.router, prefix="/api/v1/ping")
|
||||
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 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):
|
||||
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}")
|
||||
|
||||
Generated
+2924
-18
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -20,18 +20,25 @@
|
||||
"storybook:build": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "8.0.0",
|
||||
"@mantine/hooks": "8.0.0",
|
||||
"@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",
|
||||
|
||||
+42
-13
@@ -6,34 +6,63 @@ import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en o
|
||||
import { Biblioteca } from './pages/Biblioteca';
|
||||
import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
|
||||
import { Camara_noir } from './pages/Camaras_noir';
|
||||
import EditorTest from "./pages/Editor_Test"
|
||||
|
||||
|
||||
const router = createBrowserRouter([
|
||||
|
||||
// Home Principal
|
||||
|
||||
{
|
||||
path: '/',
|
||||
element: <HomePage />,
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
// LLMs
|
||||
|
||||
{
|
||||
path: '/Consulta_API',
|
||||
element: <Consulta_API />,
|
||||
},
|
||||
{
|
||||
path: '/Grid_Dashboard',
|
||||
element: <Grid_Dashboard />,
|
||||
},
|
||||
{
|
||||
path: '/Biblioteca',
|
||||
path: '/llms/Biblioteca',
|
||||
element: <Biblioteca />,
|
||||
},
|
||||
{
|
||||
path: '/analytics/Visualizaciones_Random',
|
||||
element: <VisualizacionesRandom />,
|
||||
path: '/llms/editortest',
|
||||
element: <EditorTest />,
|
||||
},
|
||||
|
||||
|
||||
// Camara
|
||||
|
||||
{
|
||||
path: '/analytics/Camaras',
|
||||
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 />,
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
// Error 404
|
||||
|
||||
{
|
||||
path: '*',
|
||||
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 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';
|
||||
|
||||
// FILLED
|
||||
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,
|
||||
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: Users, label: 'AgentesLLMs' },
|
||||
{ icon: CameraPlus, label: 'Camera' },
|
||||
{ icon: Flask, label: 'Experimentos' },
|
||||
{ icon: IconSettings, label: 'Settings' },
|
||||
];
|
||||
@@ -1,35 +1,38 @@
|
||||
// src/data/submenuLinks.ts
|
||||
|
||||
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' },
|
||||
|
||||
// Experimentos
|
||||
Experimentos: [
|
||||
{ label: 'Consulta Api', to: '/experiments/Consulta_API' },
|
||||
{ label: 'Visualizaciones_Random', to: '/experiments/Visualizaciones_Random' },
|
||||
{ label: 'Grid_Dashboard', to: '/experiments/Grid_Dashboard' },
|
||||
],
|
||||
Analytics: [
|
||||
{ label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' },
|
||||
{ label: 'Camaras', to: '/analytics/Camaras' },
|
||||
{ label: 'Tendencias', to: '/analytics/tendencias' },
|
||||
|
||||
// Camara
|
||||
Camera: [
|
||||
{ label: 'Camara principal', to: '/camara/principal' },
|
||||
],
|
||||
Releases: [
|
||||
{ label: 'Notas de versión', to: '/releases/notas-de-version' },
|
||||
{ label: 'Historial', to: '/releases/historial' },
|
||||
],
|
||||
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' },
|
||||
{ label: 'Biblioteca', to: '/llms/Biblioteca' },
|
||||
{ label: 'test', to: '/llms/editortest' },
|
||||
|
||||
],
|
||||
|
||||
// Settings
|
||||
Settings: [
|
||||
{ label: 'Preferencias', to: '/settings/preferencias' },
|
||||
{ label: 'Notificaciones', to: '/settings/notificaciones' },
|
||||
|
||||
+175
-229
@@ -1,8 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
AppShell,
|
||||
Stack,
|
||||
Card,
|
||||
Text,
|
||||
Title,
|
||||
ScrollArea,
|
||||
@@ -10,17 +8,25 @@ import {
|
||||
Button,
|
||||
TextInput,
|
||||
Modal,
|
||||
Box,
|
||||
Loader,
|
||||
Textarea
|
||||
Box
|
||||
} from '@mantine/core';
|
||||
import { AppShellWithMenu } from '../components/Appshell/Appshell';
|
||||
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 = {
|
||||
id: string;
|
||||
titulo: string;
|
||||
texto: string;
|
||||
texto: string; // Markdown
|
||||
};
|
||||
|
||||
type Biblioteca = {
|
||||
@@ -30,30 +36,66 @@ type Biblioteca = {
|
||||
notas: Nota[];
|
||||
};
|
||||
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
bulletListMarker: '-',
|
||||
codeBlockStyle: 'fenced',
|
||||
emDelimiter: '*',
|
||||
strongDelimiter: '**',
|
||||
});
|
||||
|
||||
export function Biblioteca() {
|
||||
const [bibliotecas, setBibliotecas] = useState<Biblioteca[]>([]);
|
||||
const [bibliotecaSeleccionada, setBibliotecaSeleccionada] = useState<Biblioteca | null>(null);
|
||||
const [modalAbierto, setModalAbierto] = useState(false);
|
||||
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 [notaSeleccionada, setNotaSeleccionada] = useState<Nota | null>(null);
|
||||
const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false);
|
||||
const [nombreBiblio, setNombreBiblio] = useState('');
|
||||
const [descripcionBiblio, setDescripcionBiblio] = useState('');
|
||||
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 () => {
|
||||
try {
|
||||
const res = await axios.get('/api/v1/text_manager/list');
|
||||
console.log('📦 Respuesta del backend:', res.data);
|
||||
|
||||
if (!Array.isArray(res.data)) {
|
||||
console.error('❌ La respuesta no es un array:', res.data);
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(res.data)) return;
|
||||
|
||||
const bibliotecasConNotas = await Promise.all(
|
||||
res.data.map(async (biblio: Omit<Biblioteca, 'notas'>) => {
|
||||
@@ -68,119 +110,59 @@ export function Biblioteca() {
|
||||
}
|
||||
};
|
||||
|
||||
const crearBiblioteca = async () => {
|
||||
setLoadingNuevaBiblio(true); // 🔄 Activa el loader en el botón
|
||||
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;
|
||||
|
||||
const crearBiblioteca = async () => {
|
||||
setLoadingNuevaBiblio(true);
|
||||
try {
|
||||
await axios.post(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}`, {
|
||||
titulo: tituloNota,
|
||||
texto: contenidoNota,
|
||||
tags: [],
|
||||
conexiones: [],
|
||||
resumen: '',
|
||||
await axios.post('/api/v1/text_manager/biblioteca', {
|
||||
nombre_biblioteca: nombreBiblio,
|
||||
descripcion: descripcionBiblio,
|
||||
});
|
||||
|
||||
setLoadingNotas(true);
|
||||
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);
|
||||
setTituloNota('');
|
||||
setContenidoNota('');
|
||||
setModalAbierto(false);
|
||||
setNombreBiblio('');
|
||||
setDescripcionBiblio('');
|
||||
setModalNuevaBiblio(false);
|
||||
await fetchBibliotecas();
|
||||
} catch (error) {
|
||||
console.error('Error al agregar nota:', error);
|
||||
console.error('❌ Error al crear biblioteca:', error);
|
||||
} 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 () => {
|
||||
if (!notaEnEdicion || !bibliotecaSeleccionada) return;
|
||||
if (!notaSeleccionada || !bibliotecaSeleccionada) return;
|
||||
try {
|
||||
await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaEnEdicion.id}`, {
|
||||
titulo: notaEnEdicion.titulo,
|
||||
texto: notaEnEdicion.texto,
|
||||
await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaSeleccionada.id}`, {
|
||||
titulo: notaSeleccionada.titulo,
|
||||
texto: notaSeleccionada.texto,
|
||||
tags: [],
|
||||
conexiones: [],
|
||||
resumen: ""
|
||||
});
|
||||
setModalEditarAbierto(false);
|
||||
setNotaEnEdicion(null);
|
||||
await fetchBibliotecas();
|
||||
} catch (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 (
|
||||
<AppShellWithMenu>
|
||||
<Box display="flex" h="100%">
|
||||
<Box display="flex" h="100%" style={{ overflow: 'hidden' }}>
|
||||
<Box w={240} p="md">
|
||||
<ScrollArea h="100%">
|
||||
<Stack>
|
||||
<Stack gap="md">
|
||||
<Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
|
||||
<Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}>➕ Nueva biblioteca</Button>
|
||||
|
||||
|
||||
{bibliotecas.map((biblio) => (
|
||||
<Button
|
||||
key={biblio.id}
|
||||
@@ -188,7 +170,10 @@ const crearBiblioteca = async () => {
|
||||
fullWidth
|
||||
variant={biblio.id === bibliotecaSeleccionada?.id ? 'filled' : 'light'}
|
||||
color="blue"
|
||||
onClick={() => setBibliotecaSeleccionada(biblio)}
|
||||
onClick={() => {
|
||||
setBibliotecaSeleccionada(biblio);
|
||||
setNotaSeleccionada(null);
|
||||
}}
|
||||
>
|
||||
{biblio.nombre}
|
||||
</Button>
|
||||
@@ -197,131 +182,97 @@ const crearBiblioteca = async () => {
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
<Box p="md" style={{ flex: 1 }}>
|
||||
{bibliotecaSeleccionada ? (
|
||||
<Stack>
|
||||
<Title order={2}>{bibliotecaSeleccionada.nombre}</Title>
|
||||
<Group>
|
||||
<Button onClick={() => setModalAbierto(true)}>Agregar nota</Button>
|
||||
</Group>
|
||||
<Group>
|
||||
{loadingNotas ? (
|
||||
<Loader />
|
||||
) : (
|
||||
bibliotecaSeleccionada.notas.map((nota) => (
|
||||
<Box w={240} p="md">
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="md">
|
||||
<Title order={4}>Notas</Title>
|
||||
<Button
|
||||
color="green"
|
||||
variant="outline"
|
||||
fullWidth
|
||||
onClick={async () => {
|
||||
if (!bibliotecaSeleccionada) return;
|
||||
try {
|
||||
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
|
||||
|
||||
<Card
|
||||
key={nota.id}
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={{
|
||||
width: 300,
|
||||
height: 250, // Altura fija para asegurar la separación
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title>
|
||||
<Text>{nota.texto}</Text>
|
||||
</div>
|
||||
|
||||
<Box mt="md" style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => abrirModalEditar(nota)}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
// Fin de notas en cards
|
||||
|
||||
))
|
||||
)}
|
||||
<Box p="md" style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||
{notaSeleccionada ? (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Título"
|
||||
size="lg"
|
||||
styles={{ input: { fontSize: 20, fontWeight: 600 } }}
|
||||
value={notaSeleccionada.titulo}
|
||||
onChange={(e) =>
|
||||
setNotaSeleccionada((prev) => prev ? { ...prev, titulo: e.currentTarget.value } : null)
|
||||
}
|
||||
/>
|
||||
{editor && !editor.isDestroyed && (
|
||||
<RichTextEditor
|
||||
editor={editor}
|
||||
miw={0}
|
||||
style={{ fontSize: 14, minHeight: 200 }}
|
||||
classNames={{ content: 'tiptap' }}
|
||||
>
|
||||
<RichTextEditor.Toolbar sticky stickyOffset={0}>
|
||||
<RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.Bold />
|
||||
<RichTextEditor.Italic />
|
||||
<RichTextEditor.Strikethrough />
|
||||
<RichTextEditor.ClearFormatting />
|
||||
<RichTextEditor.H1 />
|
||||
<RichTextEditor.H2 />
|
||||
<RichTextEditor.Blockquote />
|
||||
<RichTextEditor.CodeBlock />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
</RichTextEditor.Toolbar>
|
||||
{/* tabIndex removido */}
|
||||
<RichTextEditor.Content className="tiptap" />
|
||||
</RichTextEditor>
|
||||
)}
|
||||
<Group mt="sm">
|
||||
<Button color="blue" onClick={guardarEdicionNota}>💾 Guardar</Button>
|
||||
<Button color="red" onClick={() => eliminarNota(notaSeleccionada.id)}>🗑️ Eliminar</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack>
|
||||
<Text>Selecciona una biblioteca</Text>
|
||||
|
||||
</Stack>
|
||||
<Text>Selecciona una nota para editar</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Modal para agregar */}
|
||||
<Modal opened={modalAbierto} onClose={() => setModalAbierto(false)} title="Agregar nueva nota">
|
||||
<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>
|
||||
<Modal opened={modalNuevaBiblio} onClose={() => setModalNuevaBiblio(false)} title="Crear nueva biblioteca">
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nombre"
|
||||
value={nombreBiblio}
|
||||
@@ -334,14 +285,9 @@ const crearBiblioteca = async () => {
|
||||
onChange={(e) => setDescripcionBiblio(e.currentTarget.value)}
|
||||
disabled={loadingNuevaBiblio}
|
||||
/>
|
||||
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>
|
||||
Crear
|
||||
</Button>
|
||||
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>Crear</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
</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);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/${endpoint}`)
|
||||
fetch(`/api/v1/charts/${endpoint}`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => setOption(json))
|
||||
.catch(console.error)
|
||||
|
||||
+1714
-18
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,9 @@ 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)
|
||||
|
||||
@@ -13,6 +13,30 @@ from src.ArquitectureLayer.Mapper import Mapper_base
|
||||
from src.ArquitectureLayer.Model import Model_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()
|
||||
|
||||
@@ -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
|
||||
|
||||
# ----------------------
|
||||
|
||||
Reference in New Issue
Block a user