"use client"; import { useCallback, useMemo, useRef, useState, type DragEvent } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle, CheckCircle2, FileText, Files, X } from "lucide-react"; import type { KnowledgeUploadPolicy } from "@/lib/knowledge-api"; import { formatFileSize, mergeSelectedFiles, selectionFileId, validateFiles, type ValidatedFileSelection, } from "@/lib/knowledge-helpers"; interface DropState { active: boolean; invalid: boolean; draggedCount: number; } const EMPTY_DROP_STATE: DropState = { active: false, invalid: false, draggedCount: 0, }; interface FileDropZoneProps { files: File[]; onChange: (files: File[]) => void; uploadPolicy: KnowledgeUploadPolicy; disabled?: boolean; compact?: boolean; hidePolicyHint?: boolean; } export default function FileDropZone({ files, onChange, uploadPolicy, disabled = false, compact = false, hidePolicyHint = false, }: FileDropZoneProps) { const { t } = useTranslation(); const inputRef = useRef(null); const dirInputRef = useRef(null); const depthRef = useRef(0); const [dropState, setDropState] = useState(EMPTY_DROP_STATE); // `webkitdirectory`/`directory` are non-standard attrs (not in React's // typings) but widely supported — set them imperatively to pick a folder. const setDirInput = useCallback((el: HTMLInputElement | null) => { dirInputRef.current = el; if (el) { el.setAttribute("webkitdirectory", ""); el.setAttribute("directory", ""); } }, []); const selection = useMemo( () => validateFiles(files, uploadPolicy, t), [files, uploadPolicy, t], ); const previewDropped = useCallback( (incoming: File[]) => { const validated = validateFiles(incoming, uploadPolicy, t); return { count: incoming.length, invalid: validated.invalidFiles.length > 0, }; }, [t, uploadPolicy], ); const reset = useCallback(() => { depthRef.current = 0; setDropState(EMPTY_DROP_STATE); }, []); const handleEnter = useCallback( (event: DragEvent) => { if (disabled) return; if (!Array.from(event.dataTransfer.types).includes("Files")) return; event.preventDefault(); event.stopPropagation(); depthRef.current += 1; const incoming = Array.from(event.dataTransfer.items) .filter((item) => item.kind === "file") .map((item) => item.getAsFile()) .filter((file): file is File => Boolean(file)); const preview = previewDropped(incoming); setDropState({ active: true, invalid: preview.invalid, draggedCount: preview.count, }); }, [disabled, previewDropped], ); const handleOver = useCallback( (event: DragEvent) => { if (disabled) return; if (!Array.from(event.dataTransfer.types).includes("Files")) return; event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = "copy"; const incoming = Array.from(event.dataTransfer.items) .filter((item) => item.kind === "file") .map((item) => item.getAsFile()) .filter((file): file is File => Boolean(file)); const preview = previewDropped(incoming); setDropState({ active: true, invalid: preview.invalid, draggedCount: preview.count, }); }, [disabled, previewDropped], ); const handleLeave = useCallback( (event: DragEvent) => { if (disabled) return; if (!Array.from(event.dataTransfer.types).includes("Files")) return; event.preventDefault(); event.stopPropagation(); depthRef.current = Math.max(0, depthRef.current - 1); if (depthRef.current === 0) reset(); }, [disabled, reset], ); const handleDrop = useCallback( (event: DragEvent) => { if (disabled) return; if (!Array.from(event.dataTransfer.types).includes("Files")) return; event.preventDefault(); event.stopPropagation(); const dropped = Array.from(event.dataTransfer.files || []); reset(); if (!dropped.length) return; onChange(mergeSelectedFiles(files, dropped)); }, [disabled, files, onChange, reset], ); const removeFile = useCallback( (id: string) => { onChange(files.filter((file) => selectionFileId(file) !== id)); }, [files, onChange], ); const clearAll = useCallback(() => { onChange([]); if (inputRef.current) inputRef.current.value = ""; }, [onChange]); const padding = compact ? "px-4 py-5" : "px-5 py-7"; return (
{!disabled && ( )} {!hidePolicyHint && (

{uploadPolicy.extensions.length} {t("types")} ·{" "} {t("Maximum file size: {{size}}", { size: formatFileSize(uploadPolicy.max_file_size_bytes), })}

)} { const picked = Array.from(event.target.files || []); event.target.value = ""; onChange(mergeSelectedFiles(files, picked)); }} /> {/* Directory picker — files carry webkitRelativePath so the upload keeps the folder structure. Unsupported members are flagged, not blocked. */} { const picked = Array.from(event.target.files || []); event.target.value = ""; onChange(mergeSelectedFiles(files, picked)); }} /> {selection.items.length > 0 && ( )}
); } function SelectionSummary({ selection, onRemove, onClear, }: { selection: ValidatedFileSelection; onRemove: (id: string) => void; onClear: () => void; }) { const { t } = useTranslation(); const invalidCount = selection.invalidFiles.length; const readyCount = selection.validFiles.length; const hasIssues = invalidCount > 0; return (
{hasIssues ? ( ) : ( )} {hasIssues ? t("{{ready}} ready, {{skip}} will be skipped", { ready: readyCount, skip: invalidCount, }) : t("{{count}} files ready", { count: readyCount })}

{hasIssues ? t("Unsupported files are skipped; the rest will be indexed.") : t("Ready to upload")}{" "} · {formatFileSize(selection.totalBytes)}

{selection.items.map((item) => (
{item.file.name}
{item.extension} {item.sizeLabel} {item.valid ? t("Supported") : t("Needs attention")}
{item.error && (

{item.error}

)}
))}
); }