Barra de busqueda para filtrar referencias para la IA, cambios a FileDropZone

This commit is contained in:
2026-01-08 13:41:37 -06:00
parent edae79c255
commit cddc676f7d
10 changed files with 417 additions and 148 deletions

View File

@@ -20,7 +20,7 @@ interface FileDropzoneProps {
}
export function FileDropzone({
// onFilesChange,
onFilesChange,
acceptedTypes = '.doc,.docx,.pdf',
maxFiles = 5,
title = 'Arrastra archivos aquí',
@@ -39,16 +39,12 @@ export function FileDropzone({
setIsDragging(false)
}, [])
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const droppedFiles = Array.from(e.dataTransfer.files)
addFiles(droppedFiles)
},
[files],
)
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const droppedFiles = Array.from(e.dataTransfer.files)
addFiles(droppedFiles)
}, [])
const handleFileInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -57,29 +53,40 @@ export function FileDropzone({
addFiles(selectedFiles)
}
},
[files],
[],
)
const addFiles = (newFiles: Array<File>) => {
const uploadedFiles: Array<UploadedFile> = newFiles
.slice(0, maxFiles - files.length)
.map((file) => ({
id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
const addFiles = useCallback(
(newFiles: Array<File>) => {
const toUpload: Array<UploadedFile> = newFiles.map((file) => ({
id:
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? (crypto as any).randomUUID()
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: file.name,
size: formatFileSize(file.size),
type: file.name.split('.').pop() || 'file',
}))
setFiles((prev) => {
const room = Math.max(0, maxFiles - prev.length)
const next = [...prev, ...toUpload.slice(0, room)].slice(0, maxFiles)
if (onFilesChange) onFilesChange(next)
return next
})
},
[maxFiles, onFilesChange],
)
const updatedFiles = [...files, ...uploadedFiles].slice(0, maxFiles)
setFiles(updatedFiles)
// onFilesChange(updatedFiles)
}
const removeFile = (fileId: string) => {
const updatedFiles = files.filter((f) => f.id !== fileId)
setFiles(updatedFiles)
// onFilesChange(updatedFiles)
}
const removeFile = useCallback(
(fileId: string) => {
setFiles((prev) => {
const next = prev.filter((f) => f.id !== fileId)
if (onFilesChange) onFilesChange(next)
return next
})
},
[onFilesChange],
)
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B'

View File

@@ -1,3 +1,4 @@
import { FileDropzone } from './FileDropZone'
import ReferenciasParaIA from './ReferenciasParaIA'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
@@ -82,7 +83,17 @@ export function PasoDetallesPanel({
}
/>
</div>
<ReferenciasParaIA />
<ReferenciasParaIA
onFilesChange={(files) =>
onChange((w) => ({
...w,
iaConfig: {
...(w.iaConfig || ({} as any)),
archivosAdjuntos: files,
},
}))
}
/>
<div className="flex items-center justify-between">
<div className="text-muted-foreground text-sm">
Opcional: se pueden adjuntar recursos IA más adelante.
@@ -229,10 +240,10 @@ export function PasoDetallesPanel({
wizard.subModoClonado === 'TRADICIONAL'
) {
return (
<div className="grid gap-4">
<div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<Label htmlFor="word">Word del plan (obligatorio)</Label>
<input
{/* <input
id="word"
type="file"
accept=".doc,.docx"
@@ -248,6 +259,20 @@ export function PasoDetallesPanel({
},
}))
}
/> */}
<FileDropzone
acceptedTypes=".doc,.docx"
maxFiles={1}
onFilesChange={(file) => {
onChange((w) => ({
...w,
clonTradicional: {
...(w.clonTradicional || ({} as any)),
archivoWordPlanId: file,
},
}))
}}
/>
</div>
<div>

View File

@@ -1,4 +1,7 @@
import { FileText, FolderOpen, Upload } from 'lucide-react'
import { useMemo, useState } from 'react'
import BarraBusqueda from '../../BarraBusqueda'
import { FileDropzone } from './FileDropZone'
@@ -13,93 +16,142 @@ import {
} from '@/components/ui/motion-tabs'
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
const tabs = [
{
name: 'Archivos existentes',
const ReferenciasParaIA = ({
onFilesChange,
}: {
onFilesChange?: (
files: Array<{ id: string; name: string; size: string; type: string }>,
) => void
}) => {
const [busquedaArchivos, setBusquedaArchivos] = useState('')
const [busquedaRepositorios, setBusquedaRepositorios] = useState('')
value: 'archivos-existentes',
const cleanText = (text: string) => {
return text
.normalize('NFD') // Descompone "á" en "a" + "´"
.replace(/[\u0300-\u036f]/g, '') // Elimina los símbolos diacríticos
.toLowerCase() // Convierte a minúsculas
}
icon: FileText,
// Filtrado de archivos y de repositorios
const archivosFiltrados = useMemo(() => {
// Función helper para limpiar texto (quita acentos y hace minúsculas)
content: (
<div className="flex flex-col gap-0.5">
{ARCHIVOS.map((archivo) => (
<Label
key={archivo.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
const term = cleanText(busquedaArchivos)
return ARCHIVOS.filter((archivo) =>
cleanText(archivo.nombre).includes(term),
)
}, [busquedaArchivos])
<FileText className="text-muted-foreground h-4 w-4" />
const repositoriosFiltrados = useMemo(() => {
const term = cleanText(busquedaRepositorios)
return REPOSITORIOS.filter((repositorio) =>
cleanText(repositorio.nombre).includes(term),
)
}, [busquedaRepositorios])
<div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium">
{archivo.nombre}
</p>
const tabs = [
{
name: 'Archivos existentes',
<p className="text-muted-foreground text-xs">{archivo.tamaño}</p>
</div>
</Label>
))}
</div>
),
},
value: 'archivos-existentes',
{
name: 'Repositorios',
icon: FileText,
value: 'repositorios',
content: (
<div className="flex flex-col">
<BarraBusqueda
value={busquedaArchivos}
onChange={setBusquedaArchivos}
placeholder="Buscar archivo existente..."
className="m-1 mb-1.5"
/>
<div className="flex h-72 flex-col gap-0.5 overflow-y-auto">
{archivosFiltrados.map((archivo) => (
<Label
key={archivo.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
icon: FolderOpen,
<FileText className="text-muted-foreground h-4 w-4" />
content: (
<div className="flex flex-col gap-0.5">
{REPOSITORIOS.map((repositorio) => (
<Label
key={repositorio.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
<div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium">
{archivo.nombre}
</p>
<FolderOpen className="text-muted-foreground h-4 w-4" />
<div className="min-w-0 flex-1">
<p className="text-foreground text-sm font-medium">
{repositorio.nombre}
</p>
<p className="text-muted-foreground text-xs">
{archivo.tamaño}
</p>
</div>
</Label>
))}
</div>
</div>
),
},
<p className="text-muted-foreground text-xs">
{repositorio.descripcion} · {repositorio.cantidadArchivos}{' '}
archivos
</p>
</div>
</Label>
))}
</div>
),
},
{
name: 'Repositorios',
{
name: 'Subir archivos',
value: 'repositorios',
value: 'subir-archivos',
icon: FolderOpen,
icon: Upload,
content: (
<div className="flex flex-col">
<BarraBusqueda
value={busquedaRepositorios}
onChange={setBusquedaRepositorios}
placeholder="Buscar repositorio..."
className="m-1 mb-1.5"
/>
<div className="flex h-72 flex-col gap-0.5 overflow-y-auto">
{repositoriosFiltrados.map((repositorio) => (
<Label
key={repositorio.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
content: (
<div>
<FileDropzone
// onFilesChange={(files) =>
// handleChange("archivosAdhocIds", files.map((f) => f.id))
// }
title="Sube archivos de referencia"
description="Documentos que serán usados como contexto para la generación"
/>
</div>
),
},
]
<FolderOpen className="text-muted-foreground h-4 w-4" />
<div className="min-w-0 flex-1">
<p className="text-foreground text-sm font-medium">
{repositorio.nombre}
</p>
<p className="text-muted-foreground text-xs">
{repositorio.descripcion} · {repositorio.cantidadArchivos}{' '}
archivos
</p>
</div>
</Label>
))}
</div>
</div>
),
},
{
name: 'Subir archivos',
value: 'subir-archivos',
icon: Upload,
content: (
<div>
<FileDropzone
onFilesChange={onFilesChange}
title="Sube archivos de referencia"
description="Documentos que serán usados como contexto para la generación"
/>
</div>
),
},
]
const ReferenciasParaIA = () => {
return (
<div className="flex w-full flex-col gap-1">
<Label>Referencias para la IA</Label>