diff --git a/frontend/components/knowledge-filter-panel.tsx b/frontend/components/knowledge-filter-panel.tsx index 6aedff22..3511b805 100644 --- a/frontend/components/knowledge-filter-panel.tsx +++ b/frontend/components/knowledge-filter-panel.tsx @@ -1,14 +1,14 @@ "use client" import { useState, useEffect } from 'react' -import { X, Edit3, Save, Settings, ChevronDown, ChevronUp, RefreshCw } from 'lucide-react' +import { X, Edit3, Save, Settings, RefreshCw } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' -import { Checkbox } from '@/components/ui/checkbox' -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' +import { MultiSelect } from '@/components/ui/multi-select' +import { Slider } from '@/components/ui/slider' import { useKnowledgeFilter } from '@/contexts/knowledge-filter-context' @@ -21,6 +21,7 @@ interface AvailableFacets { data_sources: FacetBucket[] document_types: FacetBucket[] owners: FacetBucket[] + connector_types: FacetBucket[] } export function KnowledgeFilterPanel() { @@ -37,21 +38,18 @@ export function KnowledgeFilterPanel() { const [selectedFilters, setSelectedFilters] = useState({ data_sources: ["*"] as string[], // Default to wildcard document_types: ["*"] as string[], // Default to wildcard - owners: ["*"] as string[] // Default to wildcard + owners: ["*"] as string[], // Default to wildcard + connector_types: ["*"] as string[] // Default to wildcard }) const [resultLimit, setResultLimit] = useState(10) const [scoreThreshold, setScoreThreshold] = useState(0) - const [openSections, setOpenSections] = useState({ - data_sources: true, - document_types: true, - owners: true - }) // Available facets (loaded from API) const [availableFacets, setAvailableFacets] = useState({ data_sources: [], document_types: [], - owners: [] + owners: [], + connector_types: [] }) // Load current filter data into controls @@ -67,7 +65,8 @@ export function KnowledgeFilterPanel() { const processedFilters = { data_sources: filters.data_sources, document_types: filters.document_types, - owners: filters.owners + owners: filters.owners, + connector_types: filters.connector_types || ["*"] } console.log("[DEBUG] Loading filter selections:", processedFilters) @@ -111,7 +110,8 @@ export function KnowledgeFilterPanel() { const facets = { data_sources: result.aggregations.data_sources?.buckets || [], document_types: result.aggregations.document_types?.buckets || [], - owners: result.aggregations.owners?.buckets || [] + owners: result.aggregations.owners?.buckets || [], + connector_types: result.aggregations.connector_types?.buckets || [] } console.log("[DEBUG] Setting facets:", facets) setAvailableFacets(facets) @@ -126,12 +126,6 @@ export function KnowledgeFilterPanel() { // Don't render if panel is closed or no filter selected if (!isPanelOpen || !selectedFilter || !parsedFilterData) return null - const toggleSection = (section: keyof typeof openSections) => { - setOpenSections(prev => ({ - ...prev, - [section]: !prev[section] - })) - } @@ -140,7 +134,8 @@ export function KnowledgeFilterPanel() { setSelectedFilters({ data_sources: ["*"], document_types: ["*"], - owners: ["*"] + owners: ["*"], + connector_types: ["*"] }) } @@ -148,7 +143,8 @@ export function KnowledgeFilterPanel() { setSelectedFilters({ data_sources: [], document_types: [], - owners: [] + owners: [], + connector_types: [] }) } @@ -243,119 +239,11 @@ export function KnowledgeFilterPanel() { }) } - const FacetSection = ({ - title, - buckets, - facetType, - isOpen, - onToggle - }: { - title: string - buckets: FacetBucket[] - facetType: keyof typeof selectedFilters - isOpen: boolean - onToggle: () => void - }) => { - if (!buckets || buckets.length === 0) return null - - // "All" is selected if it contains wildcard OR if no specific selections are made - const isAllSelected = selectedFilters[facetType].includes("*") - - const handleAllToggle = (checked: boolean) => { - if (checked) { - // Select "All" - clear specific selections and add wildcard - setSelectedFilters(prev => ({ - ...prev, - [facetType]: ["*"] - })) - } else { - // Unselect "All" - remove wildcard but keep any specific selections - setSelectedFilters(prev => ({ - ...prev, - [facetType]: prev[facetType].filter(item => item !== "*") - })) - } - } - - const handleSpecificToggle = (value: string, checked: boolean) => { - setSelectedFilters(prev => { - let newValues = [...prev[facetType]] - - // Remove wildcard if selecting specific items - newValues = newValues.filter(item => item !== "*") - - if (checked) { - newValues.push(value) - } else { - newValues = newValues.filter(item => item !== value) - } - - return { - ...prev, - [facetType]: newValues - } - }) - } - - return ( - - - - - - {/* "All" wildcard option */} -
- - -
- - {/* Individual items - disabled if "All" is selected */} - {buckets.map((bucket, index) => { - const isSelected = selectedFilters[facetType].includes(bucket.key) - const isDisabled = isAllSelected - - return ( -
- - handleSpecificToggle(bucket.key, checked as boolean) - } - /> - -
- ) - })} -
-
- ) + const handleFilterChange = (facetType: keyof typeof selectedFilters, newValues: string[]) => { + setSelectedFilters(prev => ({ + ...prev, + [facetType]: newValues + })) } return ( @@ -468,29 +356,67 @@ export function KnowledgeFilterPanel() { /> - {/* Facet Sections - exactly like search page */} -
- toggleSection('data_sources')} - /> - toggleSection('document_types')} - /> - toggleSection('owners')} - /> + {/* Filter Dropdowns */} +
+
+ + ({ + value: bucket.key, + label: bucket.key, + count: bucket.count + }))} + value={selectedFilters.data_sources} + onValueChange={(values) => handleFilterChange('data_sources', values)} + placeholder="Select data sources..." + allOptionLabel="All Data Sources" + /> +
+ +
+ + ({ + value: bucket.key, + label: bucket.key, + count: bucket.count + }))} + value={selectedFilters.document_types} + onValueChange={(values) => handleFilterChange('document_types', values)} + placeholder="Select document types..." + allOptionLabel="All Document Types" + /> +
+ +
+ + ({ + value: bucket.key, + label: bucket.key, + count: bucket.count + }))} + value={selectedFilters.owners} + onValueChange={(values) => handleFilterChange('owners', values)} + placeholder="Select owners..." + allOptionLabel="All Owners" + /> +
+ +
+ + ({ + value: bucket.key, + label: bucket.key, + count: bucket.count + }))} + value={selectedFilters.connector_types} + onValueChange={(values) => handleFilterChange('connector_types', values)} + placeholder="Select sources..." + allOptionLabel="All Sources" + /> +
{/* All/None buttons */}
@@ -526,16 +452,17 @@ export function KnowledgeFilterPanel() { const newLimit = Math.max(1, Math.min(1000, parseInt(e.target.value) || 1)) setResultLimit(newLimit) }} - className="w-16 h-6 text-xs text-center" + className="h-6 text-xs text-right px-2 bg-muted/30 !border-0 rounded ml-auto focus:ring-0 focus:outline-none" + style={{ width: '70px' }} />
- setResultLimit(values[0])} max={1000} - value={resultLimit} - onChange={(e) => setResultLimit(parseInt(e.target.value))} - className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + min={1} + step={1} + className="w-full" />
@@ -546,21 +473,21 @@ export function KnowledgeFilterPanel() { setScoreThreshold(parseFloat(e.target.value) || 0)} - className="w-16 h-6 text-xs text-center" + className="h-6 text-xs text-right px-2 bg-muted/30 !border-0 rounded ml-auto focus:ring-0 focus:outline-none" + style={{ width: '70px' }} />
- setScoreThreshold(parseFloat(e.target.value))} - className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + setScoreThreshold(values[0])} + max={5} + min={0} + step={0.1} + className="w-full" /> diff --git a/frontend/components/ui/command.tsx b/frontend/components/ui/command.tsx new file mode 100644 index 00000000..b4c16c27 --- /dev/null +++ b/frontend/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} \ No newline at end of file diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx new file mode 100644 index 00000000..4327940c --- /dev/null +++ b/frontend/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} \ No newline at end of file diff --git a/frontend/components/ui/multi-select.tsx b/frontend/components/ui/multi-select.tsx new file mode 100644 index 00000000..7bf742ec --- /dev/null +++ b/frontend/components/ui/multi-select.tsx @@ -0,0 +1,230 @@ +"use client" + +import * as React from "react" +import { X, ChevronDown, Check } from "lucide-react" +import { cn } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { ScrollArea } from "@/components/ui/scroll-area" + +interface Option { + value: string + label: string + count?: number +} + +interface MultiSelectProps { + options: Option[] + value: string[] + onValueChange: (value: string[]) => void + placeholder?: string + className?: string + maxSelection?: number + searchPlaceholder?: string + showAllOption?: boolean + allOptionLabel?: string +} + +export function MultiSelect({ + options, + value, + onValueChange, + placeholder = "Select items...", + className, + maxSelection, + searchPlaceholder = "Search...", + showAllOption = true, + allOptionLabel = "All" +}: MultiSelectProps) { + const [open, setOpen] = React.useState(false) + const [searchValue, setSearchValue] = React.useState("") + + const isAllSelected = value.includes("*") + + const filteredOptions = options.filter(option => + option.label.toLowerCase().includes(searchValue.toLowerCase()) + ) + + const handleSelect = (optionValue: string) => { + if (optionValue === "*") { + // Toggle "All" selection + if (isAllSelected) { + onValueChange([]) + } else { + onValueChange(["*"]) + } + } else { + let newValue: string[] + if (value.includes(optionValue)) { + // Remove the item + newValue = value.filter(v => v !== optionValue && v !== "*") + } else { + // Add the item and remove "All" if present + newValue = [...value.filter(v => v !== "*"), optionValue] + + // Check max selection limit + if (maxSelection && newValue.length > maxSelection) { + return + } + } + onValueChange(newValue) + } + } + + const handleRemove = (optionValue: string) => { + if (optionValue === "*") { + onValueChange([]) + } else { + onValueChange(value.filter(v => v !== optionValue)) + } + } + + const getDisplayText = () => { + if (isAllSelected) { + return allOptionLabel + } + + if (value.length === 0) { + return placeholder + } + + // Extract the noun from placeholder (e.g., "Select data sources..." -> "data sources") + const noun = placeholder.toLowerCase().replace('select ', '').replace('...', '') + return `${value.length} ${noun}` + } + + const getSelectedBadges = () => { + if (isAllSelected) { + return [ + + {allOptionLabel} + + + ] + } + + return value.map(val => { + const option = options.find(opt => opt.value === val) + return ( + + {option?.label || val} + + + ) + }) + } + + return ( + + + + + + + + No items found. + + + {showAllOption && ( + handleSelect("*")} + className="cursor-pointer" + > + + {allOptionLabel} + + * + + + )} + {filteredOptions.map((option) => ( + handleSelect(option.value)} + className="cursor-pointer" + disabled={isAllSelected} + > + + {option.label} + {option.count !== undefined && ( + + {option.count} + + )} + + ))} + + + + + + ) +} \ No newline at end of file diff --git a/frontend/components/ui/popover.tsx b/frontend/components/ui/popover.tsx new file mode 100644 index 00000000..ec42c030 --- /dev/null +++ b/frontend/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } \ No newline at end of file diff --git a/frontend/components/ui/slider.tsx b/frontend/components/ui/slider.tsx new file mode 100644 index 00000000..0ca945b1 --- /dev/null +++ b/frontend/components/ui/slider.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" + +import { cn } from "@/lib/utils" + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1f30a27d..103dd7aa 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,21 +11,26 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "lucide-react": "^0.525.0", "next": "15.3.5", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7" @@ -1206,6 +1211,114 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -1434,6 +1547,147 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", @@ -1611,6 +1865,45 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -3154,6 +3447,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -6035,6 +6344,15 @@ "react": "^19.1.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 21ad6665..4f1ebfd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,21 +12,26 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "lucide-react": "^0.525.0", "next": "15.3.5", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7" diff --git a/frontend/src/app/auth/callback/page.tsx b/frontend/src/app/auth/callback/page.tsx index 73a74929..9c6bdbb0 100644 --- a/frontend/src/app/auth/callback/page.tsx +++ b/frontend/src/app/auth/callback/page.tsx @@ -84,7 +84,7 @@ function AuthCallbackContent() { await refreshAuth() // Get redirect URL from login page - const redirectTo = searchParams.get('redirect') || '/knowledge-sources' + const redirectTo = searchParams.get('redirect') || '/chat' // Clean up localStorage localStorage.removeItem('connecting_connector_id') diff --git a/frontend/src/app/chat/page.tsx b/frontend/src/app/chat/page.tsx index 12620643..dedd3470 100644 --- a/frontend/src/app/chat/page.tsx +++ b/frontend/src/app/chat/page.tsx @@ -1,13 +1,14 @@ "use client" -import { useState, useRef, useEffect } from "react" +import { useState, useRef, useEffect, useCallback } from "react" import { Button } from "@/components/ui/button" -import { Loader2, User, Bot, Zap, Settings, ChevronDown, ChevronRight, Upload, AtSign, Plus } from "lucide-react" +import { Loader2, User, Bot, Zap, Settings, ChevronDown, ChevronRight, Upload, AtSign, Plus, X } from "lucide-react" import { ProtectedRoute } from "@/components/protected-route" import { useTask } from "@/contexts/task-context" import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context" import { useAuth } from "@/contexts/auth-context" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" interface Message { role: "user" | "assistant" @@ -81,11 +82,19 @@ function ChatPage() { }>({ chat: null, langflow: null }) const [isUploading, setIsUploading] = useState(false) const [isDragOver, setIsDragOver] = useState(false) + const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false) + const [availableFilters, setAvailableFilters] = useState([]) + const [filterSearchTerm, setFilterSearchTerm] = useState("") + const [selectedFilterIndex, setSelectedFilterIndex] = useState(0) + const [isFilterHighlighted, setIsFilterHighlighted] = useState(false) + const [dropdownDismissed, setDropdownDismissed] = useState(false) const dragCounterRef = useRef(0) const messagesEndRef = useRef(null) const inputRef = useRef(null) - const { addTask } = useTask() - const { selectedFilter, parsedFilterData } = useKnowledgeFilter() + const fileInputRef = useRef(null) + const dropdownRef = useRef(null) + const { addTask, isMenuOpen } = useTask() + const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } = useKnowledgeFilter() @@ -235,15 +244,104 @@ function ChatPage() { } } + const handleFilePickerClick = () => { + fileInputRef.current?.click() + } + + const handleFilePickerChange = (e: React.ChangeEvent) => { + const files = e.target.files + if (files && files.length > 0) { + handleFileUpload(files[0]) + } + // Reset the input so the same file can be selected again + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + + const loadAvailableFilters = async () => { + try { + const response = await fetch("/api/knowledge-filter/search", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: "", + limit: 20 + }), + }) + + const result = await response.json() + if (response.ok && result.success) { + setAvailableFilters(result.filters) + } else { + console.error("Failed to load knowledge filters:", result.error) + setAvailableFilters([]) + } + } catch (error) { + console.error('Failed to load knowledge filters:', error) + setAvailableFilters([]) + } + } + + const handleFilterDropdownToggle = () => { + if (!isFilterDropdownOpen) { + loadAvailableFilters() + } + setIsFilterDropdownOpen(!isFilterDropdownOpen) + } + + const handleFilterSelect = (filter: any) => { + setSelectedFilter(filter) + setIsFilterDropdownOpen(false) + setFilterSearchTerm("") + setIsFilterHighlighted(false) + + // Remove the @searchTerm from the input and replace with filter pill + const words = input.split(' ') + const lastWord = words[words.length - 1] + + if (lastWord.startsWith('@')) { + // Remove the @search term + words.pop() + setInput(words.join(' ') + (words.length > 0 ? ' ' : '')) + } + } + useEffect(() => { scrollToBottom() }, [messages, streamingMessage]) + // Reset selected index when search term changes + useEffect(() => { + setSelectedFilterIndex(0) + }, [filterSearchTerm]) + // Auto-focus the input on component mount useEffect(() => { inputRef.current?.focus() }, []) + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (isFilterDropdownOpen && + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + !inputRef.current?.contains(event.target as Node)) { + setIsFilterDropdownOpen(false) + setFilterSearchTerm("") + setSelectedFilterIndex(0) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [isFilterDropdownOpen]) + const handleSSEStream = async (userMessage: Message) => { const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow" @@ -720,6 +818,7 @@ function ChatPage() { setMessages(prev => [...prev, userMessage]) setInput("") setLoading(true) + setIsFilterHighlighted(false) if (asyncMode) { await handleSSEStream(userMessage) @@ -1034,16 +1133,16 @@ function ChatPage() { } return ( -
+
{/* Debug header - only show in debug mode */} {isDebugMode && (
- {selectedFilter && ( - - Context: {selectedFilter.name} - - )}
{/* Async Mode Toggle */} @@ -1226,39 +1325,247 @@ function ChatPage() {
-