openrag/frontend/components/knowledge-dropdown.tsx
Edwin Jose 6c99c1b61d Implement unified upload and ingest endpoint in Langflow
This commit introduces a new combined endpoint for uploading files and running ingestion in Langflow. The frontend component is updated to utilize this endpoint, streamlining the process by eliminating separate upload and ingestion calls. The response structure is adjusted to include deletion status and other relevant information, enhancing error handling and logging practices throughout the codebase.
2025-09-08 02:03:02 -04:00

395 lines
No EOL
13 KiB
TypeScript

"use client"
import { useState, useEffect, useRef } from "react"
import { useRouter } from "next/navigation"
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"
interface KnowledgeDropdownProps {
active?: boolean
variant?: 'navigation' | 'button'
}
export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeDropdownProps) {
const router = useRouter()
const { addTask } = useTask()
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 fileInputRef = useRef<HTMLInputElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
// Check AWS availability on mount
useEffect(() => {
const checkAws = async () => {
try {
const res = await fetch("/api/upload_options")
if (res.ok) {
const data = await res.json()
setAwsEnabled(Boolean(data.aws))
}
} catch (err) {
console.error("Failed to check AWS availability", err)
}
}
checkAws()
}, [])
// 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<HTMLInputElement>) => {
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 unified upload and ingest endpoint
const uploadIngestRes = await fetch('/api/langflow/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 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)
}
}] : []),
{
label: "Cloud Connectors",
icon: PlugZap,
onClick: () => {
setIsOpen(false)
router.push("/settings")
}
}
]
return (
<>
<div ref={dropdownRef} className="relative">
<button
onClick={() => !(fileUploading || folderLoading || s3Loading) && setIsOpen(!isOpen)}
disabled={fileUploading || folderLoading || s3Loading}
className={cn(
variant === 'button'
? "rounded-lg h-12 px-4 flex items-center gap-2 bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
: "text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:bg-accent hover:text-accent-foreground rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed",
variant === 'navigation' && active
? "bg-accent text-accent-foreground shadow-sm"
: variant === 'navigation' ? "text-foreground hover:text-accent-foreground" : "",
)}
>
{variant === 'button' ? (
<>
<Plus className="h-4 w-4" />
<span>Add Knowledge</span>
<ChevronDown className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")} />
</>
) : (
<>
<div className="flex items-center flex-1">
<Upload className={cn("h-4 w-4 mr-3 shrink-0", active ? "text-accent-foreground" : "text-muted-foreground group-hover:text-foreground")} />
Knowledge
</div>
<ChevronDown className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")} />
</>
)}
</button>
{isOpen && (
<div className="absolute top-full left-0 right-0 mt-1 bg-popover border border-border rounded-md shadow-md z-50">
<div className="py-1">
{menuItems.map((item, index) => (
<button
key={index}
onClick={item.onClick}
className="w-full px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground"
>
{item.label}
</button>
))}
</div>
</div>
)}
<input
ref={fileInputRef}
type="file"
onChange={handleFileChange}
className="hidden"
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
/>
</div>
{/* Process Folder Dialog */}
<Dialog open={showFolderDialog} onOpenChange={setShowFolderDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderOpen className="h-5 w-5" />
Process Folder
</DialogTitle>
<DialogDescription>
Process all documents in a folder path
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="folder-path">Folder Path</Label>
<Input
id="folder-path"
type="text"
placeholder="/path/to/documents"
value={folderPath}
onChange={(e) => setFolderPath(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setShowFolderDialog(false)}
>
Cancel
</Button>
<Button
onClick={handleFolderUpload}
disabled={!folderPath.trim() || folderLoading}
>
{folderLoading ? "Processing..." : "Process Folder"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Process S3 Bucket Dialog */}
<Dialog open={showS3Dialog} onOpenChange={setShowS3Dialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Cloud className="h-5 w-5" />
Process S3 Bucket
</DialogTitle>
<DialogDescription>
Process all documents from an S3 bucket. AWS credentials must be configured.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="bucket-url">S3 URL</Label>
<Input
id="bucket-url"
type="text"
placeholder="s3://bucket/path"
value={bucketUrl}
onChange={(e) => setBucketUrl(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setShowS3Dialog(false)}
>
Cancel
</Button>
<Button
onClick={handleS3Upload}
disabled={!bucketUrl.trim() || s3Loading}
>
{s3Loading ? "Processing..." : "Process Bucket"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
)
}