diff --git a/frontend/components/confirmation-dialog.tsx b/frontend/components/confirmation-dialog.tsx new file mode 100644 index 00000000..1ff8a370 --- /dev/null +++ b/frontend/components/confirmation-dialog.tsx @@ -0,0 +1,84 @@ +"use client"; + +import React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "./ui/dialog"; +import { Button } from "./ui/button"; +import { AlertTriangle } from "lucide-react"; + +interface ConfirmationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title?: string; + description?: string; + confirmText?: string; + cancelText?: string; + onConfirm: () => void | Promise; + isLoading?: boolean; + variant?: "destructive" | "default"; +} + +export const DeleteConfirmationDialog: React.FC = ({ + open, + onOpenChange, + title = "Are you sure?", + description = "This action cannot be undone.", + confirmText = "Confirm", + cancelText = "Cancel", + onConfirm, + isLoading = false, + variant = "destructive", +}) => { + const handleConfirm = async () => { + try { + await onConfirm(); + } finally { + // Only close if not in loading state (let the parent handle this) + if (!isLoading) { + onOpenChange(false); + } + } + }; + + return ( + + + +
+ {variant === "destructive" && ( + + )} + {title} +
+ {description} +
+ + + + + +
+
+ ); +}; diff --git a/frontend/components/knowledge-actions-dropdown.tsx b/frontend/components/knowledge-actions-dropdown.tsx index 745e66c5..f73ffe25 100644 --- a/frontend/components/knowledge-actions-dropdown.tsx +++ b/frontend/components/knowledge-actions-dropdown.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import { DropdownMenu, DropdownMenuContent, @@ -8,20 +9,72 @@ import { } from "@/components/ui/dropdown-menu"; import { EllipsisVertical } from "lucide-react"; import { Button } from "./ui/button"; +import { DeleteConfirmationDialog } from "./confirmation-dialog"; +import { useDeleteDocument } from "@/app/api/mutations/useDeleteDocument"; +import { toast } from "sonner"; -export function KnowledgeActionsDropdown() { - return ( - - - - - - - Delete - - - - ); +interface KnowledgeActionsDropdownProps { + filename: string; } + +export const KnowledgeActionsDropdown = ({ + filename, +}: KnowledgeActionsDropdownProps) => { + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const deleteDocumentMutation = useDeleteDocument(); + + const handleDelete = async () => { + try { + await deleteDocumentMutation.mutateAsync({ filename }); + toast.success(`Successfully deleted "${filename}"`); + setShowDeleteDialog(false); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to delete document" + ); + } + }; + + return ( + <> + + + + + + {/* //TODO: Implement rename and sync */} + {/* alert("Not implemented")} + > + Rename + + alert("Not implemented")} + > + Sync + */} + setShowDeleteDialog(true)} + > + Delete + + + + + + + ); +}; diff --git a/frontend/components/knowledge-dropdown.tsx b/frontend/components/knowledge-dropdown.tsx index 82581de8..f9176311 100644 --- a/frontend/components/knowledge-dropdown.tsx +++ b/frontend/components/knowledge-dropdown.tsx @@ -1,10 +1,10 @@ "use client"; -import { useQueryClient } from "@tanstack/react-query"; import { ChevronDown, Cloud, FolderOpen, + Loader2, PlugZap, Plus, Upload, @@ -44,6 +44,7 @@ export function KnowledgeDropdown({ 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; @@ -55,12 +56,6 @@ export function KnowledgeDropdown({ const fileInputRef = useRef(null); const dropdownRef = useRef(null); - const queryClient = useQueryClient(); - - const refetchSearch = () => { - queryClient.invalidateQueries({ queryKey: ["search"] }); - }; - // Check AWS availability and cloud connectors on mount useEffect(() => { const checkAvailability = async () => { @@ -107,7 +102,7 @@ export function KnowledgeDropdown({ const connections = statusData.connections || []; const activeConnection = connections.find( (conn: { is_active: boolean; connection_id: string }) => - conn.is_active, + conn.is_active ); const isConnected = activeConnection !== undefined; @@ -117,7 +112,7 @@ export function KnowledgeDropdown({ // Check token availability try { const tokenRes = await fetch( - `/api/connectors/${type}/token?connection_id=${activeConnection.connection_id}`, + `/api/connectors/${type}/token?connection_id=${activeConnection.connection_id}` ); if (tokenRes.ok) { const tokenData = await tokenRes.json(); @@ -178,7 +173,7 @@ export function KnowledgeDropdown({ window.dispatchEvent( new CustomEvent("fileUploadStart", { detail: { filename: files[0].name }, - }), + }) ); try { @@ -190,21 +185,38 @@ export function KnowledgeDropdown({ method: "POST", body: formData, }); + const uploadIngestJson = await uploadIngestRes.json(); + if (!uploadIngestRes.ok) { throw new Error( - uploadIngestJson?.error || "Upload and ingest failed", + uploadIngestJson?.error || "Upload and ingest failed" ); } - // Extract results from the unified response - const fileId = uploadIngestJson?.upload?.id; - const filePath = uploadIngestJson?.upload?.path; + // Extract results from the response - handle both unified and simple formats + const fileId = uploadIngestJson?.upload?.id || uploadIngestJson?.id; + const filePath = + uploadIngestJson?.upload?.path || + uploadIngestJson?.path || + "uploaded"; const runJson = uploadIngestJson?.ingestion; const deleteResult = uploadIngestJson?.deletion; - if (!fileId || !filePath) { - throw new Error("Upload successful but no file id/path returned"); + 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 @@ -212,12 +224,12 @@ export function KnowledgeDropdown({ if (deleteResult.status === "deleted") { console.log( "File successfully cleaned up from Langflow:", - deleteResult.file_id, + deleteResult.file_id ); } else if (deleteResult.status === "delete_failed") { console.warn( "Failed to cleanup file from Langflow:", - deleteResult.error, + deleteResult.error ); } } @@ -235,8 +247,9 @@ export function KnowledgeDropdown({ unified: true, }, }, - }), + }) ); + // Trigger search refresh after successful ingestion window.dispatchEvent(new CustomEvent("knowledgeUpdated")); } catch (error) { @@ -246,12 +259,12 @@ export function KnowledgeDropdown({ filename: files[0].name, error: error instanceof Error ? error.message : "Upload failed", }, - }), + }) ); } finally { window.dispatchEvent(new CustomEvent("fileUploadComplete")); setFileUploading(false); - refetchSearch(); + // Don't call refetchSearch() here - the knowledgeUpdated event will handle it } } @@ -288,9 +301,15 @@ export function KnowledgeDropdown({ addTask(taskId); setFolderPath(""); // Trigger search refresh after successful folder processing starts + console.log( + "Folder upload successful, dispatching knowledgeUpdated event" + ); window.dispatchEvent(new CustomEvent("knowledgeUpdated")); } else if (response.ok) { setFolderPath(""); + console.log( + "Folder upload successful (direct), dispatching knowledgeUpdated event" + ); window.dispatchEvent(new CustomEvent("knowledgeUpdated")); } else { console.error("Folder upload failed:", result.error); @@ -299,7 +318,7 @@ export function KnowledgeDropdown({ console.error("Folder upload error:", error); } finally { setFolderLoading(false); - refetchSearch(); + // Don't call refetchSearch() here - the knowledgeUpdated event will handle it } }; @@ -330,6 +349,7 @@ export function KnowledgeDropdown({ addTask(taskId); setBucketUrl("s3://"); // Trigger search refresh after successful S3 processing starts + console.log("S3 upload successful, dispatching knowledgeUpdated event"); window.dispatchEvent(new CustomEvent("knowledgeUpdated")); } else { console.error("S3 upload failed:", result.error); @@ -338,7 +358,7 @@ export function KnowledgeDropdown({ console.error("S3 upload error:", error); } finally { setS3Loading(false); - refetchSearch(); + // Don't call refetchSearch() here - the knowledgeUpdated event will handle it } }; @@ -347,10 +367,17 @@ export function KnowledgeDropdown({ .map(([type, info]) => ({ label: info.name, icon: PlugZap, - onClick: () => { + onClick: async () => { setIsOpen(false); if (info.connected && info.hasToken) { - router.push(`/upload/${type}`); + 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"); } @@ -392,14 +419,16 @@ export function KnowledgeDropdown({ ...cloudConnectorItems, ]; + // Comprehensive loading state + const isLoading = + fileUploading || folderLoading || s3Loading || isNavigatingToCloud; + return ( <>
- {isOpen && ( + {isOpen && !isLoading && (
{menuItems.map((item, index) => ( @@ -458,7 +511,7 @@ export function KnowledgeDropdown({ "w-full px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground", "disabled" in item && item.disabled && - "opacity-50 cursor-not-allowed hover:bg-transparent hover:text-current", + "opacity-50 cursor-not-allowed hover:bg-transparent hover:text-current" )} > {item.label} @@ -497,7 +550,7 @@ export function KnowledgeDropdown({ type="text" placeholder="/path/to/documents" value={folderPath} - onChange={(e) => setFolderPath(e.target.value)} + onChange={e => setFolderPath(e.target.value)} />
@@ -539,7 +592,7 @@ export function KnowledgeDropdown({ type="text" placeholder="s3://bucket/path" value={bucketUrl} - onChange={(e) => setBucketUrl(e.target.value)} + onChange={e => setBucketUrl(e.target.value)} />
diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx index 3dc0b5f0..1eea9079 100644 --- a/frontend/components/ui/input.tsx +++ b/frontend/components/ui/input.tsx @@ -1,7 +1,8 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -export interface InputProps extends React.InputHTMLAttributes { +export interface InputProps + extends React.InputHTMLAttributes { icon?: React.ReactNode; inputClassName?: string; } @@ -9,7 +10,12 @@ export interface InputProps extends React.InputHTMLAttributes const Input = React.forwardRef( ({ className, inputClassName, icon, type, placeholder, ...props }, ref) => { return ( -