From 11369ce79255193400fef7e2b2a68e4059a22f53 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 11 Mar 2026 13:47:54 -0600 Subject: [PATCH 1/2] =?UTF-8?q?L=C3=ADmite=20de=20al=20menos=203=20caracte?= =?UTF-8?q?res=20y=20tooltip=20en=20boton=20de=20generar=20sugerencias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nueva/NuevaBibliografiaModalContainer.tsx | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx b/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx index 4098772..c1cd980 100644 --- a/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx +++ b/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx @@ -50,7 +50,7 @@ import { buscar_bibliografia } from '@/data' import { useCreateBibliografia } from '@/data/hooks/useSubjects' import { cn } from '@/lib/utils' -type MetodoBibliografia = 'MANUAL' | 'IA' | null +type MetodoBibliografia = 'MANUAL' | 'EN_LINEA' | null export type FormatoCita = 'apa' | 'ieee' | 'vancouver' | 'chicago' type IdiomaBibliografia = @@ -462,7 +462,7 @@ export function NuevaBibliografiaModalContainer({ const localeCacheRef = useRef(new Map()) const titleOverrides = - wizard.metodo === 'IA' + wizard.metodo === 'EN_LINEA' ? { paso2: 'Sugerencias', paso3: 'Estructura' } : { paso2: 'Datos básicos', paso3: 'Detalles' } @@ -474,7 +474,7 @@ export function NuevaBibliografiaModalContainer({ } const refsForStep3: Array = - wizard.metodo === 'IA' + wizard.metodo === 'EN_LINEA' ? wizard.ia.sugerencias .filter((s) => s.selected) .map((s) => endpointResultToRef(iaSugerenciaToEndpointResult(s))) @@ -501,10 +501,10 @@ export function NuevaBibliografiaModalContainer({ }, [wizard.citaEdits, wizard.formato, wizard.refs]) const canContinueDesdeMetodo = - wizard.metodo === 'MANUAL' || wizard.metodo === 'IA' + wizard.metodo === 'MANUAL' || wizard.metodo === 'EN_LINEA' const canContinueDesdePaso2 = - wizard.metodo === 'IA' + wizard.metodo === 'EN_LINEA' ? wizard.ia.sugerencias.some((s) => s.selected) : wizard.manual.refs.length > 0 @@ -842,7 +842,7 @@ export function NuevaBibliografiaModalContainer({ {idx === 1 && ( - {wizard.metodo === 'IA' ? ( + {wizard.metodo === 'EN_LINEA' ? ( onChange('IA')} + onClick={() => onChange('EN_LINEA')} > @@ -1097,22 +1097,46 @@ function SugerenciasStep({ - + {!isLoading && q.trim().length < 3 ? ( + + + + + + + +

El query debe ser de al menos 3 caracteres

+
+
+ ) : ( + + )} {errorMessage ? ( @@ -1689,7 +1713,11 @@ function ResumenStep({ const basicas = refs.filter((r) => r.tipo === 'BASICA') const complementarias = refs.filter((r) => r.tipo === 'COMPLEMENTARIA') const metodoLabel = - metodo === 'MANUAL' ? 'Manual' : metodo === 'IA' ? 'Buscar en línea' : '—' + metodo === 'MANUAL' + ? 'Manual' + : metodo === 'EN_LINEA' + ? 'Buscar en línea' + : '—' return (
From ea842ee46ce5256e58223794fe8294609c5869c5 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 11 Mar 2026 16:01:09 -0600 Subject: [PATCH 2/2] =?UTF-8?q?close=20#170:=20se=20a=C3=B1adieron=20valid?= =?UTF-8?q?aciones=20y=20mejoras=20en=20el=20modal=20de=20nueva=20bibliogr?= =?UTF-8?q?af=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -Se implementaron restricciones en SugerenciasStep: el campo de búsqueda se limitó a 200 caracteres y la generación quedó bloqueada si hay 20 o más referencias seleccionadas; se añadió tooltip en el botón de generar cuando la query tiene menos de 3 caracteres. -Se reforzaron validaciones en FormatoYCitasStep y DatosBasicosManualStep: el título se trim-eó y se forzó a no quedar vacío (max 500 caracteres); si un título queda vacío se hace scroll al input/card, se muestra mensaje de error junto al label y se resalta el input; autores se limitó a 2000 caracteres; editorial a 300 caracteres; ISBN a 20 caracteres; el año se convirtió en input numérico permitiendo vacío o un año de 4 dígitos entre 1450 y el año actual +1. -Se añadieron checkboxes "Año aproximado" y "En prensa" (mutuamente excluyentes): "En prensa" deshabilita el input de año y se marca el estado para citeproc; "Año aproximado" se envía como circa en issued. -Al generar CSL se incluyeron las propiedades issued.circa y status ('in press') según los flags del ref. -En ResumenStep se añadieron advertencias por referencia cuando falte autor(es), año (si no está "en prensa"), editorial o ISBN. -Se corrigieron detalles de UX en edición de autores para preservar saltos de línea y se añadieron handlers para evitar errores de validación al mover entre pasos. --- .../nueva/NuevaBibliografiaModalContainer.tsx | 495 ++++++++++++++---- 1 file changed, 403 insertions(+), 92 deletions(-) diff --git a/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx b/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx index c1cd980..a389f92 100644 --- a/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx +++ b/src/features/bibliografia/nueva/NuevaBibliografiaModalContainer.tsx @@ -8,7 +8,14 @@ import { RefreshCw, X, } from 'lucide-react' -import { useEffect, useMemo, useRef, useState } from 'react' +import { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' import type { BuscarBibliografiaRequest } from '@/data' import type { @@ -101,6 +108,9 @@ const IDIOMA_TO_OPEN_LIBRARY: Record = { RU: 'rus', } +const MIN_YEAR = 1450 +const MAX_YEAR = new Date().getFullYear() + 1 + type CSLAuthor = { family: string given: string @@ -112,7 +122,8 @@ type CSLItem = { title: string author: Array publisher?: string - issued?: { 'date-parts': Array> } + issued?: { 'date-parts': Array>; circa?: boolean } + status?: string ISBN?: string } @@ -131,6 +142,8 @@ type BibliografiaRef = { authors: Array publisher?: string year?: number + yearIsApproximate?: boolean + isInPress?: boolean isbn?: string tipo: BibliografiaTipo @@ -209,6 +222,19 @@ function tryParseYear(publishedDate?: string): number | undefined { return Number.isFinite(year) ? year : undefined } +function sanitizeYearInput(value: string): string { + return value.replace(/[^\d]/g, '').slice(0, 4) +} + +function tryParseStrictYear(value: string): number | undefined { + const cleaned = sanitizeYearInput(value) + if (!/^\d{4}$/.test(cleaned)) return undefined + const year = Number.parseInt(cleaned, 10) + if (!Number.isFinite(year)) return undefined + if (year < MIN_YEAR || year > MAX_YEAR) return undefined + return year +} + function randomUUID(): string { try { const c = (globalThis as any).crypto @@ -425,6 +451,8 @@ export function NuevaBibliografiaModalContainer({ const navigate = useNavigate() const createBibliografia = useCreateBibliografia() + const formatoStepRef = useRef(null) + const [wizard, setWizard] = useState({ metodo: null, ia: { @@ -513,6 +541,8 @@ export function NuevaBibliografiaModalContainer({ async function handleBuscarSugerencias() { const hadNoSugerenciasBefore = wizard.ia.sugerencias.length === 0 + if (wizard.ia.sugerencias.filter((s) => s.selected).length >= 20) return + const q = wizard.ia.q.trim() if (!q) return @@ -633,13 +663,21 @@ export function NuevaBibliografiaModalContainer({ const cslItems: Record = {} for (const r of refs) { + const trimmedTitle = r.title.trim() cslItems[r.id] = { id: r.id, type: 'book', - title: r.title || 'Sin título', + title: trimmedTitle || 'Sin título', author: r.authors.map(parsearAutor), publisher: r.publisher, - issued: r.year ? { 'date-parts': [[r.year]] } : undefined, + issued: + r.isInPress || !r.year + ? undefined + : { + 'date-parts': [[r.year]], + circa: r.yearIsApproximate ? true : undefined, + }, + status: r.isInPress ? 'in press' : undefined, ISBN: r.isbn, } } @@ -797,7 +835,14 @@ export function NuevaBibliografiaModalContainer({ ) : (
@@ -1124,7 +1171,9 @@ function SugerenciasStep({ type="button" variant="outline" onClick={onGenerate} - disabled={isLoading || q.trim().length < 3} + disabled={ + isLoading || q.trim().length < 3 || selectedCount >= 20 + } className="gap-2" > {isLoading ? ( @@ -1334,9 +1383,19 @@ function DatosBasicosManualStep({ - onChangeDraft({ ...draft, title: e.target.value }) + onChangeDraft({ + ...draft, + title: e.target.value.slice(0, 500), + }) } + onBlur={() => { + const trimmed = draft.title.trim() + if (trimmed !== draft.title) { + onChangeDraft({ ...draft, title: trimmed }) + } + }} /> @@ -1344,8 +1403,12 @@ function DatosBasicosManualStep({