From f28ba54da3637ab5ac72b915c42b2d988ca52906 Mon Sep 17 00:00:00 2001 From: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:07:49 -0600 Subject: [PATCH 01/10] Implement Delete Confirmation Dialog and Enhance Knowledge Dropdown - Added a new `DeleteConfirmationDialog` component for confirming deletions. - Updated `KnowledgeDropdown` to include a loading state and improved user feedback during file operations. - Enhanced the search page to support bulk deletion of documents with confirmation dialog. - Integrated event dispatching for knowledge updates after file operations. - Refactored various components for better readability and maintainability. --- frontend/components/confirmation-dialog.tsx | 84 +++ frontend/components/knowledge-dropdown.tsx | 165 ++++-- frontend/components/ui/input.tsx | 16 +- .../src/app/api/queries/useGetNudgesQuery.ts | 11 +- .../src/app/api/queries/useGetSearchQuery.ts | 21 +- frontend/src/app/knowledge/page.tsx | 495 ++++++++++++++++-- frontend/src/app/upload/[provider]/page.tsx | 332 ++++++------ frontend/src/contexts/task-context.tsx | 30 +- src/api/connectors.py | 2 + src/api/documents.py | 59 +++ src/main.py | 13 + src/services/document_service.py | 1 + src/session_manager.py | 13 +- 13 files changed, 949 insertions(+), 293 deletions(-) create mode 100644 frontend/components/confirmation-dialog.tsx create mode 100644 src/api/documents.py 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-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 5c14e593..67fb6727 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 ( -