import { useNavigate } from '@tanstack/react-router' import CSL from 'citeproc' import { Globe, Loader2, Plus, RefreshCw, X } from 'lucide-react' import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import type { BuscarBibliografiaRequest } from '@/data' import type { EndpointResult, GoogleBooksVolume, OpenLibraryDoc, } from '@/data/api/subjects.api' import type { TablesInsert } from '@/types/supabase' import { defineStepper } from '@/components/stepper' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' import { WizardLayout } from '@/components/wizard/WizardLayout' import { WizardResponsiveHeader } from '@/components/wizard/WizardResponsiveHeader' import { buscar_bibliografia } from '@/data' import { useCreateBibliografia } from '@/data/hooks/useSubjects' import { cn } from '@/lib/utils' type MetodoBibliografia = 'MANUAL' | 'IA' | null export type FormatoCita = 'apa' | 'ieee' | 'vancouver' | 'chicago' type IdiomaBibliografia = | 'ALL' | 'ES' | 'EN' | 'DE' | 'ZH' | 'FR' | 'IT' | 'JA' | 'RU' const IDIOMA_LABEL: Record = { ALL: 'Todos', ES: 'Español', EN: 'Inglés', DE: 'Alemán', ZH: 'Chino', FR: 'Francés', IT: 'Italiano', JA: 'Japonés', RU: 'Ruso', } const IDIOMA_TO_GOOGLE: Record = { ALL: undefined, ES: 'es', EN: 'en', DE: 'de', ZH: 'zh', FR: 'fr', IT: 'it', JA: 'ja', RU: 'ru', } // ISO 639-2 (bibliographic codes) commonly used by Open Library. const IDIOMA_TO_OPEN_LIBRARY: Record = { ALL: undefined, ES: 'spa', EN: 'eng', DE: 'ger', ZH: 'chi', FR: 'fre', IT: 'ita', JA: 'jpn', RU: 'rus', } type CSLAuthor = { family: string given: string } type CSLItem = { id: string type: 'book' title: string author: Array publisher?: string issued?: { 'date-parts': Array> } ISBN?: string } type BibliografiaAsignaturaInsert = TablesInsert<'bibliografia_asignatura'> type BibliografiaTipo = BibliografiaAsignaturaInsert['tipo'] type BibliografiaTipoFuente = NonNullable< BibliografiaAsignaturaInsert['tipo_fuente'] > type BibliografiaRef = { id: string source: BibliografiaTipoFuente raw?: GoogleBooksVolume | OpenLibraryDoc title: string authors: Array publisher?: string year?: number isbn?: string tipo: BibliografiaTipo } type WizardState = { metodo: MetodoBibliografia ia: { q: string idioma: IdiomaBibliografia showConservacionTooltip: boolean sugerencias: Array<{ id: string selected: boolean endpoint: EndpointResult['endpoint'] item: GoogleBooksVolume | OpenLibraryDoc }> isLoading: boolean errorMessage: string | null } manual: { draft: { title: string authorsText: string publisher: string yearText: string isbn: string } refs: Array } formato: FormatoCita | null refs: Array citaEdits: Record> generatingIds: Set isSaving: boolean errorMessage: string | null } type IASugerencia = WizardState['ia']['sugerencias'][number] function iaSugerenciaToEndpointResult(s: IASugerencia): EndpointResult { return s.endpoint === 'google' ? { endpoint: 'google', item: s.item as GoogleBooksVolume } : { endpoint: 'open_library', item: s.item as OpenLibraryDoc } } const Wizard = defineStepper( { id: 'metodo', title: 'Método', description: 'Manual o Buscar en línea' }, { id: 'paso2', title: 'Datos básicos', description: 'Seleccionar o capturar', }, { id: 'paso3', title: 'Detalles', description: 'Formato y citas' }, { id: 'resumen', title: 'Resumen', description: 'Confirmar' }, ) function parsearAutor(nombreCompleto: string): CSLAuthor { if (nombreCompleto.includes(',')) { return { family: nombreCompleto.split(',')[0]?.trim() ?? '', given: nombreCompleto.split(',')[1]?.trim() ?? '', } } const partes = nombreCompleto.trim().split(/\s+/).filter(Boolean) if (partes.length === 1) return { family: partes[0] ?? '', given: '' } const family = partes.pop() ?? '' const given = partes.join(' ') return { family, given } } function tryParseYear(publishedDate?: string): number | undefined { if (!publishedDate) return undefined const match = String(publishedDate).match(/\d{4}/) if (!match) return undefined const year = Number.parseInt(match[0], 10) return Number.isFinite(year) ? year : undefined } function randomUUID(): string { try { const c = (globalThis as any).crypto if (c && typeof c.randomUUID === 'function') return c.randomUUID() } catch { // ignore } return `${Date.now()}-${Math.random().toString(16).slice(2)}` } function tryParseYearFromOpenLibrary(doc: OpenLibraryDoc): number | undefined { const y1 = doc['first_publish_year'] if (typeof y1 === 'number' && Number.isFinite(y1)) return y1 const years = doc['publish_year'] if (Array.isArray(years)) { const numeric = years .map((x) => (typeof x === 'number' ? x : Number(x))) .filter((n) => Number.isFinite(n)) if (numeric.length > 0) return Math.max(...numeric) } const published = doc['publish_date'] if (typeof published === 'string') return tryParseYear(published) return undefined } function getEndpointResultId(result: EndpointResult): string { if (result.endpoint === 'google') { return `google:${result.item.id}` } const doc = result.item const key = doc['key'] if (typeof key === 'string' && key.trim()) return `open_library:${key}` const cover = doc['cover_edition_key'] if (typeof cover === 'string' && cover.trim()) return `open_library:${cover}` const editionKey = doc['edition_key'] if (Array.isArray(editionKey) && typeof editionKey[0] === 'string') { return `open_library:${editionKey[0]}` } return `open_library:${randomUUID()}` } function endpointResultToRef(result: EndpointResult): BibliografiaRef { if (result.endpoint === 'google') { const volume = result.item const info = volume.volumeInfo ?? {} const title = (info.title ?? '').trim() || 'Sin título' const authors = Array.isArray(info.authors) ? info.authors : [] const publisher = typeof info.publisher === 'string' ? info.publisher : undefined const year = tryParseYear(info.publishedDate) const isbn = info.industryIdentifiers?.find((x) => x?.identifier)?.identifier ?? undefined return { id: getEndpointResultId(result), source: 'BIBLIOTECA', raw: volume, title, authors, publisher, year, isbn, tipo: 'BASICA', } } const doc = result.item const title = (typeof doc['title'] === 'string' ? doc['title'] : '').trim() || 'Sin título' const authors = Array.isArray(doc['author_name']) ? (doc['author_name'] as Array).filter((a): a is string => typeof a === 'string') : [] const publisher = Array.isArray(doc['publisher']) ? (doc['publisher'] as Array).find( (p): p is string => typeof p === 'string', ) : typeof doc['publisher'] === 'string' ? doc['publisher'] : undefined const year = tryParseYearFromOpenLibrary(doc) const isbn = Array.isArray(doc['isbn']) ? (doc['isbn'] as Array).find((x): x is string => typeof x === 'string') : undefined return { id: getEndpointResultId(result), source: 'BIBLIOTECA', raw: doc, title, authors, publisher, year, isbn, tipo: 'BASICA', } } function getResultYear(result: EndpointResult): number | undefined { if (result.endpoint === 'google') { const info = result.item.volumeInfo ?? {} return tryParseYear(info.publishedDate) } return tryParseYearFromOpenLibrary(result.item) } function sortResultsByMostRecent(a: EndpointResult, b: EndpointResult) { const ya = getResultYear(a) const yb = getResultYear(b) if (typeof ya === 'number' && typeof yb === 'number') return yb - ya if (typeof ya === 'number') return -1 if (typeof yb === 'number') return 1 return 0 } function AutoSizeTextarea({ value, disabled, placeholder, className, onChange, }: { value: string disabled?: boolean placeholder?: string className?: string onChange: (next: string) => void }) { const ref = useRef(null) const autosize = () => { const el = ref.current if (!el) return el.style.height = '0px' el.style.height = `${el.scrollHeight}px` } useLayoutEffect(() => { autosize() }, [value]) return (