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:
2025-06-01 00:33:48 +02:00
parent cf6a768f6b
commit 628cddc3ae
18 changed files with 5092 additions and 345 deletions
+42 -13
View File
@@ -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 />,
+3 -1
View File
@@ -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;
}
+6 -5
View File
@@ -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' },
];
+25 -22
View File
@@ -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
View File
@@ -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>
);
}
+27
View File
@@ -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)