From 615a8ef97beea01fff474330e9afd36a98171d0b Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Mon, 29 Sep 2025 20:24:21 +0530 Subject: [PATCH 1/2] feat: XYN-298 indexing kb with vespa frontend part of indexing --- .../src/components/CollectionNavigation.tsx | 86 ++++++++++--- frontend/src/components/FileTree.tsx | 80 ++++++++++++ .../src/components/UploadProgressWidget.tsx | 117 +++++++++++++++--- frontend/src/routes/_authenticated/agent.tsx | 2 +- .../_authenticated/knowledgeManagement.tsx | 30 ++++- frontend/src/types/knowledgeBase.ts | 5 + frontend/src/utils/fileUtils.ts | 11 +- server/api/knowledgeBase.ts | 4 +- server/db/schema/knowledgeBase.ts | 2 +- server/queue/fileProcessor.ts | 3 +- server/scripts/kbFileStatusMigration.ts | 2 +- server/shared/types.ts | 7 ++ server/types.ts | 6 - server/worker.ts | 3 +- 14 files changed, 308 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/CollectionNavigation.tsx b/frontend/src/components/CollectionNavigation.tsx index c18ea5c2d..9ddf800f2 100644 --- a/frontend/src/components/CollectionNavigation.tsx +++ b/frontend/src/components/CollectionNavigation.tsx @@ -1,7 +1,13 @@ import React from "react" -import { ChevronRight } from "lucide-react" +import { ChevronRight, AlertOctagon } from "lucide-react" import { DropdownMenuItem } from "@/components/ui/dropdown-menu" import { api } from "@/api" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" interface CollectionNavigationProps { navigationPath: Array<{ @@ -49,6 +55,12 @@ interface CollectionNavigationProps { navigateToCl: (clId: string, clName: string) => Promise } +// Helper function to check if an item should be non-selectable based on upload status +function isItemNonSelectable(item: any): boolean { + const uploadStatus = item.uploadStatus + return uploadStatus === "pending" || uploadStatus === "processing" || uploadStatus === "failed" +} + // Utility function to check if an item is selected either directly or through parent inheritance function isItemSelectedWithInheritance( item: any, @@ -198,10 +210,14 @@ export const CollectionNavigation: React.FC = ({ ) const isInherited = isSelected && !isDirectlySelected + const isNonSelectable = isItemNonSelectable(result) const handleResultSelect = () => { // Don't allow selection changes for inherited items if (isInherited) return + + // For non-selectable items, prevent all interactions (no navigation or selection) + if (isNonSelectable) return if (result.type === "collection") { // Toggle collection selection @@ -262,25 +278,25 @@ export const CollectionNavigation: React.FC = ({ return (
{}} - className={`w-4 h-4 mr-3 ${isInherited ? "opacity-60" : ""}`} + className={`w-4 h-4 mr-3 ${isInherited || isNonSelectable ? "opacity-60" : ""}`} />
- - {result.name} - + + {result.name} + {result.type} @@ -289,6 +305,18 @@ export const CollectionNavigation: React.FC = ({ Selected )} + {isNonSelectable && ( + + + + + + +

Indexing is in progress

+
+
+
+ )}
{result.collectionName && result.type !== "collection" && ( @@ -370,13 +398,20 @@ export const CollectionNavigation: React.FC = ({ Loading...
) : currentItems.length > 0 ? ( - currentItems.map((item: any) => ( + currentItems.map((item: any) => { + const isNonSelectable = isItemNonSelectable(item) + + return (
{ - if (item.type === "folder") { - // When navigating to a folder, if it's selected, auto-select all children + if (item.type === "folder" && !isNonSelectable) { + // Only allow navigation to folder if it's selectable navigateToFolder(item.id, item.name) } }} @@ -425,7 +460,7 @@ export const CollectionNavigation: React.FC = ({ isSelected || isInheritedFromParent, ) const isDisabled: boolean = Boolean( - isInheritedFromParent && !isSelected, + (isInheritedFromParent && !isSelected) || isNonSelectable, ) return ( @@ -435,7 +470,7 @@ export const CollectionNavigation: React.FC = ({ disabled={isDisabled} onChange={(e) => { e.stopPropagation() - if (isDisabled) return // Prevent changes if inherited from parent + if (isDisabled) return // Prevent changes if inherited from parent or non-selectable const isCurrentlySelected = selectedSet.has(item.id) @@ -557,19 +592,32 @@ export const CollectionNavigation: React.FC = ({ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - className="mr-2 text-gray-800" + className={`mr-2 ${isNonSelectable ? "text-gray-400" : "text-gray-800"}`} > )} - + {item.name} - {item.type === "folder" && ( + {isNonSelectable && ( + + + + + + +

Indexing is in progress

+
+
+
+ )} + {item.type === "folder" && !isNonSelectable && ( )}
- )) + ) + }) ) : (
No items found diff --git a/frontend/src/components/FileTree.tsx b/frontend/src/components/FileTree.tsx index 269e54336..2dfe011eb 100644 --- a/frontend/src/components/FileTree.tsx +++ b/frontend/src/components/FileTree.tsx @@ -7,6 +7,10 @@ import { Plus, Download, Trash2, + Check, + Loader2, + AlertOctagon, + RotateCcw, } from "lucide-react" import { Tooltip, @@ -58,6 +62,7 @@ interface FileTreeProps { onToggle: (node: FileNode) => void onFileClick: (node: FileNode) => void onDownload?: (node: FileNode, path: string) => void + onRetry?: (node: FileNode, path: string) => void } const FileTree = ({ @@ -67,6 +72,7 @@ const FileTree = ({ onToggle, onFileClick, onDownload, + onRetry, }: FileTreeProps) => { return (
@@ -79,6 +85,7 @@ const FileTree = ({ onToggle={onToggle} onFileClick={onFileClick} onDownload={onDownload} + onRetry={onRetry} /> ))}
@@ -94,6 +101,7 @@ const FileNodeComponent = ({ onToggle, onFileClick, onDownload, + onRetry, }: { node: FileNode level?: number @@ -103,6 +111,7 @@ const FileNodeComponent = ({ onToggle: (node: FileNode) => void onFileClick: (node: FileNode) => void onDownload?: (node: FileNode, path: string) => void + onRetry?: (node: FileNode, path: string) => void }) => { const [isHovered, setIsHovered] = useState(false) @@ -136,6 +145,31 @@ const FileNodeComponent = ({ > {node.name} + {/* Upload status indicator for folders */} + {node.uploadStatus && ( +
+ + + +
+ {node.uploadStatus === "completed" && ( + + )} + {(node.uploadStatus === "processing" || node.uploadStatus === "pending") && ( + + )} + {node.uploadStatus === "failed" && ( + + )} +
+
+ +

{node.statusMessage || node.uploadStatus}

+
+
+
+
+ )}
) : (
{node.name} + {/* Upload status indicator */} + {node.uploadStatus && ( +
+ + + +
+ {node.uploadStatus === "completed" && ( + + )} + {(node.uploadStatus === "processing" || node.uploadStatus === "pending") && ( + + )} + {node.uploadStatus === "failed" && ( + + )} +
+
+ +

{node.statusMessage }

+
+
+
+
+ )}
)}
@@ -176,6 +235,26 @@ const FileNodeComponent = ({ }} /> )} + {(node.retryCount ?? 0) == 4 && onRetry && ( + + + + { + e.preventDefault() + e.stopPropagation() + onRetry(node, currentPath) + }} + /> + + +

Retry

+
+
+
+ )} ))} diff --git a/frontend/src/components/UploadProgressWidget.tsx b/frontend/src/components/UploadProgressWidget.tsx index 17bb33bec..57356950d 100644 --- a/frontend/src/components/UploadProgressWidget.tsx +++ b/frontend/src/components/UploadProgressWidget.tsx @@ -1,16 +1,96 @@ -import React, { useState } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { useUploadProgress } from '@/contexts/UploadProgressContext' import { Button } from '@/components/ui/button' -import { X, ChevronUp, ChevronDown } from 'lucide-react' +import { X, ChevronUp, ChevronDown, Loader2 } from 'lucide-react' import { ConfirmModal } from '@/components/ui/confirmModal' type TabType = 'all' | 'uploaded' | 'failed' +interface Position { + x: number + y: number +} + export const UploadProgressWidget: React.FC = () => { const { currentUpload, cancelUpload } = useUploadProgress() const [isExpanded, setIsExpanded] = useState(false) const [showCancelModal, setShowCancelModal] = useState(false) const [activeTab, setActiveTab] = useState('all') + + // Drag functionality state + const [position, setPosition] = useState(() => { + const padding = 6 + const widgetWidth = 480 + const widgetHeight = 150 // collapsed height + return { + x: window.innerWidth - widgetWidth - padding, + y: window.innerHeight - widgetHeight - padding + } + }) + const [isDragging, setIsDragging] = useState(false) + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) + const widgetRef = useRef(null) + + // Drag event handlers + const handleMouseDown = (e: React.MouseEvent) => { + if (widgetRef.current) { + const rect = widgetRef.current.getBoundingClientRect() + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top + }) + setIsDragging(true) + } + } + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging) return + + const newX = e.clientX - dragOffset.x + const newY = e.clientY - dragOffset.y + + // Constrain to screen bounds with 6px padding + const padding = 6 + const widgetWidth = 480 + const widgetHeight = isExpanded ? 400 : 150 + const maxX = window.innerWidth - widgetWidth - padding + const maxY = window.innerHeight - widgetHeight - padding + + setPosition({ + x: Math.max(padding, Math.min(newX, maxX)), + y: Math.max(padding, Math.min(newY, maxY)) + }) + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + }, [isDragging, dragOffset, isExpanded]) + + // Adjust position when widget expands/collapses to prevent overflow + useEffect(() => { + const padding = 6 + const widgetWidth = 480 + const widgetHeight = isExpanded ? 400 : 150 + const maxX = window.innerWidth - widgetWidth - padding + const maxY = window.innerHeight - widgetHeight - padding + + setPosition(currentPos => ({ + x: Math.max(padding, Math.min(currentPos.x, maxX)), + y: Math.max(padding, Math.min(currentPos.y, maxY)) + })) + }, [isExpanded]) if (!currentUpload || !currentUpload.isUploading) { return null @@ -31,7 +111,16 @@ export const UploadProgressWidget: React.FC = () => { return ( <> {/* Main Upload Widget */} -
+
{/* Header */}
@@ -98,30 +187,30 @@ export const UploadProgressWidget: React.FC = () => {