"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]) // 1) Upload to Langflow const upRes = await fetch('/api/langflow/files/upload', { method: 'POST', body: formData, }) const upJson = await upRes.json() if (!upRes.ok) { throw new Error(upJson?.error || 'Upload to Langflow failed') } const fileId = upJson?.id const filePath = upJson?.path if (!fileId || !filePath) { throw new Error('Langflow did not return file id/path') } // 2) Run ingestion flow const runRes = await fetch('/api/langflow/ingest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_paths: [filePath] }), }) const runJson = await runRes.json() if (!runRes.ok) { throw new Error(runJson?.error || 'Langflow ingestion failed') } // 3) Delete file from Langflow const delRes = await fetch('/api/langflow/files', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_ids: [fileId] }), }) const delJson = await delRes.json().catch(() => ({})) if (!delRes.ok) { throw new Error(delJson?.error || 'Langflow file delete failed') } // Notify UI window.dispatchEvent(new CustomEvent('fileUploaded', { detail: { file: files[0], result: { file_id: fileId, file_path: filePath, run: runJson } } })) // 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)} />
) }