"use client"; import { ChevronDown, Cloud, FolderOpen, Loader2, PlugZap, Plus, Upload, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { useGetTasksQuery } from "@/app/api/queries/useGetTasksQuery"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useTask } from "@/contexts/task-context"; import { cn } from "@/lib/utils"; interface KnowledgeDropdownProps { active?: boolean; variant?: "navigation" | "button"; } export function KnowledgeDropdown({ active, variant = "navigation", }: KnowledgeDropdownProps) { const { addTask } = useTask(); const { refetch: refetchTasks } = useGetTasksQuery(); const router = useRouter(); const [isOpen, setIsOpen] = useState(false); const [showFolderDialog, setShowFolderDialog] = useState(false); const [showS3Dialog, setShowS3Dialog] = useState(false); const [awsEnabled, setAwsEnabled] = useState(false); const [folderPath, setFolderPath] = useState("/app/documents/"); 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 [cloudConnectors, setCloudConnectors] = useState<{ [key: string]: { name: string; available: boolean; connected: boolean; hasToken: boolean; }; }>({}); const fileInputRef = useRef(null); const dropdownRef = 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(); }, []); // Handle click outside to close dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); } }, [isOpen]); const handleFileUpload = () => { fileInputRef.current?.click(); }; const handleFileChange = async (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { // Close dropdown and disable button immediately after file selection setIsOpen(false); setFileUploading(true); // Trigger the same file upload event as the chat page window.dispatchEvent( new CustomEvent("fileUploadStart", { detail: { filename: files[0].name }, }), ); try { const formData = new FormData(); formData.append("file", files[0]); // Use router upload and ingest endpoint (automatically routes based on configuration) const uploadIngestRes = await fetch("/api/router/upload_ingest", { method: "POST", body: formData, }); const uploadIngestJson = await uploadIngestRes.json(); if (!uploadIngestRes.ok) { throw new Error( uploadIngestJson?.error || "Upload and ingest failed", ); } // Extract results from the response - handle both unified and simple formats const fileId = uploadIngestJson?.upload?.id || uploadIngestJson?.id || uploadIngestJson?.task_id; const filePath = uploadIngestJson?.upload?.path || uploadIngestJson?.path || "uploaded"; const runJson = uploadIngestJson?.ingestion; const deleteResult = uploadIngestJson?.deletion; console.log("c", uploadIngestJson ) if (!fileId) { throw new Error("Upload successful but no file id returned"); } // Check if ingestion actually succeeded if ( runJson && runJson.status !== "COMPLETED" && runJson.status !== "SUCCESS" ) { const errorMsg = runJson.error || "Ingestion pipeline failed"; throw new Error( `Ingestion failed: ${errorMsg}. Try setting DISABLE_INGEST_WITH_LANGFLOW=true if you're experiencing Langflow component issues.`, ); } // Log deletion status if provided if (deleteResult) { if (deleteResult.status === "deleted") { console.log( "File successfully cleaned up from Langflow:", deleteResult.file_id, ); } else if (deleteResult.status === "delete_failed") { console.warn( "Failed to cleanup file from Langflow:", deleteResult.error, ); } } // Notify UI window.dispatchEvent( new CustomEvent("fileUploaded", { detail: { file: files[0], result: { file_id: fileId, file_path: filePath, run: runJson, deletion: deleteResult, unified: true, }, }, }), ); refetchTasks(); } catch (error) { window.dispatchEvent( new CustomEvent("fileUploadError", { detail: { filename: files[0].name, error: error instanceof Error ? error.message : "Upload failed", }, }), ); } finally { window.dispatchEvent(new CustomEvent("fileUploadComplete")); setFileUploading(false); } } // Reset file input if (fileInputRef.current) { fileInputRef.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); } }; const cloudConnectorItems = Object.entries(cloudConnectors) .filter(([, info]) => info.available) .map(([type, info]) => ({ label: info.name, icon: PlugZap, onClick: async () => { setIsOpen(false); 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, tooltip: !info.connected ? `Connect ${info.name} in Settings first` : !info.hasToken ? `Reconnect ${info.name} - access token required` : undefined, })); const menuItems = [ { label: "Add File", icon: Upload, onClick: handleFileUpload, }, { label: "Process Folder", icon: FolderOpen, onClick: () => { setIsOpen(false); setShowFolderDialog(true); }, }, ...(awsEnabled ? [ { label: "Process S3 Bucket", icon: Cloud, onClick: () => { setIsOpen(false); setShowS3Dialog(true); }, }, ] : []), ...cloudConnectorItems, ]; // Comprehensive loading state const isLoading = fileUploading || folderLoading || s3Loading || isNavigatingToCloud; return ( <>
{isOpen && !isLoading && (
{menuItems.map((item, index) => ( ))}
)}
{/* 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)} />
); }