"use client" import { useState, useEffect, useRef } from "react" import { ChevronDown, Upload, FolderOpen, Cloud, PlugZap, Plus } from "lucide-react" 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 { cn } from "@/lib/utils" import { useTask } from "@/contexts/task-context" import { useRouter } from "next/navigation" interface KnowledgeDropdownProps { active?: boolean variant?: 'navigation' | 'button' } export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeDropdownProps) { const { addTask } = useTask() 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 [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 unified response const fileId = uploadIngestJson?.upload?.id const filePath = uploadIngestJson?.upload?.path const runJson = uploadIngestJson?.ingestion const deleteResult = uploadIngestJson?.deletion if (!fileId || !filePath) { throw new Error('Upload successful but no file id/path returned') } // 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 } } })) // Trigger search refresh after successful ingestion window.dispatchEvent(new CustomEvent('knowledgeUpdated')) } 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("") // Trigger search refresh after successful folder processing starts window.dispatchEvent(new CustomEvent('knowledgeUpdated')) } else if (response.ok) { setFolderPath("") window.dispatchEvent(new CustomEvent('knowledgeUpdated')) } else { console.error("Folder upload failed:", result.error) } } 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://") // Trigger search refresh after successful S3 processing starts window.dispatchEvent(new CustomEvent('knowledgeUpdated')) } else { console.error("S3 upload failed:", result.error) } } 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: () => { setIsOpen(false) if (info.connected && info.hasToken) { router.push(`/upload/${type}`) } 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 ] return ( <>
{isOpen && (
{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)} />
) }