"use client"; import { useQueryClient } from "@tanstack/react-query"; import { ChevronDown, Cloud, File as FileIcon, Folder, FolderOpen, Loader2, PlugZap, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import type { File as SearchFile } from "@/app/api/queries/useGetSearchQuery"; import { useGetTasksQuery } from "@/app/api/queries/useGetTasksQuery"; import { DuplicateHandlingDialog } from "@/components/duplicate-handling-dialog"; import AwsIcon from "@/components/icons/aws-logo"; import GoogleDriveIcon from "@/components/icons/google-drive-logo"; import OneDriveIcon from "@/components/icons/one-drive-logo"; import SharePointIcon from "@/components/icons/share-point-logo"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useTask } from "@/contexts/task-context"; import { duplicateCheck, uploadFile as uploadFileUtil, } from "@/lib/upload-utils"; import { cn } from "@/lib/utils"; // Supported file extensions - single source of truth const SUPPORTED_EXTENSIONS = [ ".pdf", ".doc", ".docx", ".pptx", ".ppt", ".xlsx", ".xls", ".csv", ".txt", ".md", ".html", ".htm", ".rtf", ".odt", ".asciidoc", ".adoc", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp", ]; export function KnowledgeDropdown() { const { addTask } = useTask(); const { refetch: refetchTasks } = useGetTasksQuery(); const queryClient = useQueryClient(); const router = useRouter(); const [isMenuOpen, setIsMenuOpen] = useState(false); const [showFolderDialog, setShowFolderDialog] = useState(false); const [showS3Dialog, setShowS3Dialog] = useState(false); const [showDuplicateDialog, setShowDuplicateDialog] = useState(false); const [awsEnabled, setAwsEnabled] = useState(false); const [folderPath, setFolderPath] = useState(""); const [bucketUrl, setBucketUrl] = useState("s3://"); const [folderLoading, setFolderLoading] = useState(false); const [s3Loading, setS3Loading] = useState(false); const [fileUploading, setFileUploading] = useState(false); const [isNavigatingToCloud, setIsNavigatingToCloud] = useState(false); const [pendingFile, setPendingFile] = useState(null); const [duplicateFilename, setDuplicateFilename] = useState(""); const [cloudConnectors, setCloudConnectors] = useState<{ [key: string]: { name: string; available: boolean; connected: boolean; hasToken: boolean; }; }>({}); const fileInputRef = useRef(null); const folderInputRef = useRef(null); // Check AWS availability and cloud connectors on mount useEffect(() => { const checkAvailability = async () => { try { // Check AWS const awsRes = await fetch("/api/upload_options"); if (awsRes.ok) { const awsData = await awsRes.json(); setAwsEnabled(Boolean(awsData.aws)); } // Check cloud connectors const connectorsRes = await fetch("/api/connectors"); if (connectorsRes.ok) { const connectorsResult = await connectorsRes.json(); const cloudConnectorTypes = [ "google_drive", "onedrive", "sharepoint", ]; const connectorInfo: { [key: string]: { name: string; available: boolean; connected: boolean; hasToken: boolean; }; } = {}; for (const type of cloudConnectorTypes) { if (connectorsResult.connectors[type]) { connectorInfo[type] = { name: connectorsResult.connectors[type].name, available: connectorsResult.connectors[type].available, connected: false, hasToken: false, }; // Check connection status try { const statusRes = await fetch(`/api/connectors/${type}/status`); if (statusRes.ok) { const statusData = await statusRes.json(); const connections = statusData.connections || []; const activeConnection = connections.find( (conn: { is_active: boolean; connection_id: string }) => conn.is_active, ); const isConnected = activeConnection !== undefined; if (isConnected && activeConnection) { connectorInfo[type].connected = true; // Check token availability try { const tokenRes = await fetch( `/api/connectors/${type}/token?connection_id=${activeConnection.connection_id}`, ); if (tokenRes.ok) { const tokenData = await tokenRes.json(); if (tokenData.access_token) { connectorInfo[type].hasToken = true; } } } catch { // Token check failed } } } } catch { // Status check failed } } } setCloudConnectors(connectorInfo); } } catch (err) { console.error("Failed to check availability", err); } }; checkAvailability(); }, []); const handleFileUpload = () => { fileInputRef.current?.click(); }; const resetFileInput = () => { if (fileInputRef.current) { fileInputRef.current.value = ""; } }; const handleFileChange = async ( event: React.ChangeEvent, ) => { const files = event.target.files; if (files && files.length > 0) { const file = files[0]; // File selection will close dropdown automatically try { console.log("[Duplicate Check] Checking file:", file.name); const checkData = await duplicateCheck(file); console.log("[Duplicate Check] Result:", checkData); if (checkData.exists) { console.log("[Duplicate Check] Duplicate detected, showing dialog"); setPendingFile(file); setDuplicateFilename(file.name); setShowDuplicateDialog(true); resetFileInput(); return; } // No duplicate, proceed with upload console.log("[Duplicate Check] No duplicate, proceeding with upload"); await uploadFile(file, false); } catch (error) { console.error("[Duplicate Check] Exception:", error); toast.error("Failed to check for duplicates", { description: error instanceof Error ? error.message : "Unknown error", }); } } resetFileInput(); }; const uploadFile = async (file: File, replace: boolean) => { setFileUploading(true); try { await uploadFileUtil(file, replace); refetchTasks(); } catch (error) { // Dispatch event that chat context can listen to // This avoids circular dependency issues if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("ingestionFailed", { detail: { source: "knowledge-dropdown" }, }), ); } toast.error("Upload failed", { description: error instanceof Error ? error.message : "Unknown error", }); } finally { setFileUploading(false); } }; const handleOverwriteFile = async () => { if (pendingFile) { // Remove the old file from all search query caches before overwriting queryClient.setQueriesData({ queryKey: ["search"] }, (oldData: []) => { if (!oldData) return oldData; // Filter out the file that's being overwritten return oldData.filter( (file: SearchFile) => file.filename !== pendingFile.name, ); }); await uploadFile(pendingFile, true); setPendingFile(null); setDuplicateFilename(""); } }; const handleFolderSelect = async ( event: React.ChangeEvent, ) => { const files = event.target.files; if (!files || files.length === 0) return; setFolderLoading(true); try { const fileList = Array.from(files); const filteredFiles = fileList.filter((file) => { const ext = file.name .substring(file.name.lastIndexOf(".")) .toLowerCase(); return SUPPORTED_EXTENSIONS.includes(ext); }); if (filteredFiles.length === 0) { toast.error("No supported files found", { description: "Please select a folder containing supported document files (PDF, DOCX, PPTX, XLSX, CSV, HTML, images, etc.).", }); return; } toast.info(`Processing ${filteredFiles.length} file(s)...`); for (const originalFile of filteredFiles) { try { // Extract just the filename without the folder path const fileName = originalFile.name.split("/").pop() || originalFile.name; console.log( `[Folder Upload] Processing file: ${originalFile.name} -> ${fileName}`, ); // Create a new File object with just the basename (no folder path) // This is necessary because the webkitRelativePath includes the folder name const file = new File([originalFile], fileName, { type: originalFile.type, lastModified: originalFile.lastModified, }); console.log(`[Folder Upload] Created new File object:`, { name: file.name, type: file.type, size: file.size, }); // Check for duplicates using the clean filename const checkData = await duplicateCheck(file); console.log(`[Folder Upload] Duplicate check result:`, checkData); if (!checkData.exists) { console.log(`[Folder Upload] Uploading file: ${fileName}`); await uploadFileUtil(file, false); console.log(`[Folder Upload] Successfully uploaded: ${fileName}`); } else { console.log(`[Folder Upload] Skipping duplicate: ${fileName}`); } } catch (error) { console.error( `[Folder Upload] Failed to upload ${originalFile.name}:`, error, ); } } refetchTasks(); toast.success(`Successfully processed ${filteredFiles.length} file(s)`); } catch (error) { console.error("Folder upload error:", error); toast.error("Folder upload failed", { description: error instanceof Error ? error.message : "Unknown error", }); } finally { setFolderLoading(false); if (folderInputRef.current) { folderInputRef.current.value = ""; } } }; const handleFolderUpload = async () => { if (!folderPath.trim()) return; setFolderLoading(true); setShowFolderDialog(false); try { const response = await fetch("/api/upload_path", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ path: folderPath }), }); const result = await response.json(); if (response.status === 201) { const taskId = result.task_id || result.id; if (!taskId) { throw new Error("No task ID received from server"); } addTask(taskId); setFolderPath(""); // Refetch tasks to show the new task refetchTasks(); } else if (response.ok) { setFolderPath(""); // Refetch tasks even for direct uploads in case tasks were created refetchTasks(); } else { console.error("Folder upload failed:", result.error); if (response.status === 400) { toast.error("Upload failed", { description: result.error || "Bad request", }); } } } catch (error) { console.error("Folder upload error:", error); } finally { setFolderLoading(false); } }; const handleS3Upload = async () => { if (!bucketUrl.trim()) return; setS3Loading(true); setShowS3Dialog(false); try { const response = await fetch("/api/upload_bucket", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ s3_url: bucketUrl }), }); const result = await response.json(); if (response.status === 201) { const taskId = result.task_id || result.id; if (!taskId) { throw new Error("No task ID received from server"); } addTask(taskId); setBucketUrl("s3://"); // Refetch tasks to show the new task refetchTasks(); } else { console.error("S3 upload failed:", result.error); if (response.status === 400) { toast.error("Upload failed", { description: result.error || "Bad request", }); } } } catch (error) { console.error("S3 upload error:", error); } finally { setS3Loading(false); } }; // Icon mapping for cloud connectors const connectorIconMap = { google_drive: GoogleDriveIcon, onedrive: OneDriveIcon, sharepoint: SharePointIcon, }; const cloudConnectorItems = Object.entries(cloudConnectors) .filter(([, info]) => info.available) .map(([type, info]) => ({ label: info.name, icon: connectorIconMap[type as keyof typeof connectorIconMap] || PlugZap, onClick: async () => { if (info.connected && info.hasToken) { setIsNavigatingToCloud(true); try { router.push(`/upload/${type}`); // Keep loading state for a short time to show feedback setTimeout(() => setIsNavigatingToCloud(false), 1000); } catch { setIsNavigatingToCloud(false); } } else { router.push("/settings"); } }, disabled: !info.connected || !info.hasToken, })); const menuItems = [ { label: "File", icon: ({ className }: { className?: string }) => ( ), onClick: handleFileUpload, }, { label: "Folder", icon: ({ className }: { className?: string }) => ( ), onClick: () => folderInputRef.current?.click(), }, ...(awsEnabled ? [ { label: "Amazon S3", icon: AwsIcon, onClick: () => setShowS3Dialog(true), }, ] : []), ...cloudConnectorItems, ]; // Comprehensive loading state const isLoading = fileUploading || folderLoading || s3Loading || isNavigatingToCloud; return ( <> {menuItems.map((item, index) => ( {item.label} ))} {/* Process Folder Dialog */} Process Folder Process all documents in a folder path
setFolderPath(e.target.value)} />
{/* Process S3 Bucket Dialog */} Process S3 Bucket Process all documents from an S3 bucket. AWS credentials must be configured.
setBucketUrl(e.target.value)} />
{/* Duplicate Handling Dialog */} ); }