improvement(search): improved filters UI and search suggestions (#1387)

* improvement(search): improved filters UI and search suggestions

* update tool input UI
This commit is contained in:
Waleed
2025-09-19 11:22:47 -07:00
committed by GitHub
parent a1c518e4e1
commit 225571c49a
7 changed files with 40 additions and 38 deletions

View File

@@ -58,20 +58,20 @@ export function Filters() {
<h2 className='mb-4 pl-2 font-medium text-sm'>Filters</h2> <h2 className='mb-4 pl-2 font-medium text-sm'>Filters</h2>
{/* Timeline Filter */}
<FilterSection title='Timeline' content={<Timeline />} />
{/* Level Filter */} {/* Level Filter */}
<FilterSection title='Level' content={<Level />} /> <FilterSection title='Level' content={<Level />} />
{/* Trigger Filter */} {/* Workflow Filter */}
<FilterSection title='Trigger' content={<Trigger />} /> <FilterSection title='Workflow' content={<Workflow />} />
{/* Folder Filter */} {/* Folder Filter */}
<FilterSection title='Folder' content={<FolderFilter />} /> <FilterSection title='Folder' content={<FolderFilter />} />
{/* Workflow Filter */} {/* Trigger Filter */}
<FilterSection title='Workflow' content={<Workflow />} /> <FilterSection title='Trigger' content={<Trigger />} />
{/* Timeline Filter */}
<FilterSection title='Timeline' content={<Timeline />} />
</div> </div>
) )
} }

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { Loader2, Search, X } from 'lucide-react' import { Loader2, Search, X } from 'lucide-react'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -89,6 +89,16 @@ export function AutocompleteSearch({
} }
}, [state.isOpen, state.highlightedIndex]) }, [state.isOpen, state.highlightedIndex])
const [showSpinner, setShowSpinner] = useState(false)
useEffect(() => {
if (!state.pendingQuery) {
setShowSpinner(false)
return
}
const t = setTimeout(() => setShowSpinner(true), 200)
return () => clearTimeout(t)
}, [state.pendingQuery])
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value const newValue = e.target.value
const cursorPos = e.target.selectionStart || 0 const cursorPos = e.target.selectionStart || 0
@@ -126,7 +136,7 @@ export function AutocompleteSearch({
state.isOpen && 'ring-1 ring-ring' state.isOpen && 'ring-1 ring-ring'
)} )}
> >
{state.pendingQuery ? ( {showSpinner ? (
<Loader2 className='h-4 w-4 flex-shrink-0 animate-spin text-muted-foreground' /> <Loader2 className='h-4 w-4 flex-shrink-0 animate-spin text-muted-foreground' />
) : ( ) : (
<Search className='h-4 w-4 flex-shrink-0 text-muted-foreground' strokeWidth={2} /> <Search className='h-4 w-4 flex-shrink-0 text-muted-foreground' strokeWidth={2} />

View File

@@ -12,7 +12,6 @@ import {
import { Search } from 'lucide-react' import { Search } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
// Context for the command component
type CommandContextType = { type CommandContextType = {
searchQuery: string searchQuery: string
setSearchQuery: (value: string) => void setSearchQuery: (value: string) => void
@@ -26,7 +25,6 @@ type CommandContextType = {
const CommandContext = createContext<CommandContextType | undefined>(undefined) const CommandContext = createContext<CommandContextType | undefined>(undefined)
// Hook to use the command context
const useCommandContext = () => { const useCommandContext = () => {
const context = useContext(CommandContext) const context = useContext(CommandContext)
if (!context) { if (!context) {
@@ -35,7 +33,6 @@ const useCommandContext = () => {
return context return context
} }
// Types for the components
interface CommandProps { interface CommandProps {
children: ReactNode children: ReactNode
className?: string className?: string
@@ -76,17 +73,14 @@ interface CommandSeparatorProps {
className?: string className?: string
} }
// Main Command component
export function Command({ children, className, filter }: CommandProps) { export function Command({ children, className, filter }: CommandProps) {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [activeIndex, setActiveIndex] = useState(-1) const [activeIndex, setActiveIndex] = useState(-1)
const [items, setItems] = useState<string[]>([]) const [items, setItems] = useState<string[]>([])
const [filteredItems, setFilteredItems] = useState<string[]>([]) const [filteredItems, setFilteredItems] = useState<string[]>([])
// Register and unregister items - memoize to prevent infinite loops
const registerItem = useCallback((id: string) => { const registerItem = useCallback((id: string) => {
setItems((prev) => { setItems((prev) => {
// Only add if not already in the array
if (prev.includes(id)) return prev if (prev.includes(id)) return prev
return [...prev, id] return [...prev, id]
}) })
@@ -96,7 +90,6 @@ export function Command({ children, className, filter }: CommandProps) {
setItems((prev) => prev.filter((item) => item !== id)) setItems((prev) => prev.filter((item) => item !== id))
}, []) }, [])
// Handle item selection
const selectItem = useCallback( const selectItem = useCallback(
(id: string) => { (id: string) => {
const index = filteredItems.indexOf(id) const index = filteredItems.indexOf(id)
@@ -107,7 +100,6 @@ export function Command({ children, className, filter }: CommandProps) {
[filteredItems] [filteredItems]
) )
// Filter items based on search query
useEffect(() => { useEffect(() => {
if (!searchQuery) { if (!searchQuery) {
setFilteredItems(items) setFilteredItems(items)
@@ -127,7 +119,6 @@ export function Command({ children, className, filter }: CommandProps) {
setActiveIndex(filtered.length > 0 ? 0 : -1) setActiveIndex(filtered.length > 0 ? 0 : -1)
}, [searchQuery, items, filter]) }, [searchQuery, items, filter])
// Default filter function
const defaultFilter = useCallback((value: string, search: string): number => { const defaultFilter = useCallback((value: string, search: string): number => {
const normalizedValue = value.toLowerCase() const normalizedValue = value.toLowerCase()
const normalizedSearch = search.toLowerCase() const normalizedSearch = search.toLowerCase()
@@ -138,7 +129,6 @@ export function Command({ children, className, filter }: CommandProps) {
return 0 return 0
}, []) }, [])
// Handle keyboard navigation
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (filteredItems.length === 0) return if (filteredItems.length === 0) return
@@ -163,7 +153,6 @@ export function Command({ children, className, filter }: CommandProps) {
[filteredItems, activeIndex] [filteredItems, activeIndex]
) )
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo( const contextValue = useMemo(
() => ({ () => ({
searchQuery, searchQuery,
@@ -193,7 +182,6 @@ export function Command({ children, className, filter }: CommandProps) {
) )
} }
// Command Input component
export function CommandInput({ export function CommandInput({
placeholder = 'Search...', placeholder = 'Search...',
className, className,
@@ -208,7 +196,6 @@ export function CommandInput({
onValueChange?.(value) onValueChange?.(value)
} }
// Focus input on mount
useEffect(() => { useEffect(() => {
inputRef.current?.focus() inputRef.current?.focus()
}, []) }, [])
@@ -230,7 +217,6 @@ export function CommandInput({
) )
} }
// Command List component
export function CommandList({ children, className }: CommandListProps) { export function CommandList({ children, className }: CommandListProps) {
return ( return (
<div className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}> <div className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}>
@@ -239,7 +225,6 @@ export function CommandList({ children, className }: CommandListProps) {
) )
} }
// Command Empty component
export function CommandEmpty({ children, className }: CommandEmptyProps) { export function CommandEmpty({ children, className }: CommandEmptyProps) {
const { filteredItems } = useCommandContext() const { filteredItems } = useCommandContext()
@@ -252,7 +237,6 @@ export function CommandEmpty({ children, className }: CommandEmptyProps) {
) )
} }
// Command Group component
export function CommandGroup({ children, className, heading }: CommandGroupProps) { export function CommandGroup({ children, className, heading }: CommandGroupProps) {
return ( return (
<div <div
@@ -269,7 +253,6 @@ export function CommandGroup({ children, className, heading }: CommandGroupProps
) )
} }
// Command Item component
export function CommandItem({ export function CommandItem({
children, children,
className, className,
@@ -281,16 +264,13 @@ export function CommandItem({
const isActive = filteredItems.indexOf(value) === activeIndex const isActive = filteredItems.indexOf(value) === activeIndex
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false)
// Register and unregister item
useEffect(() => { useEffect(() => {
// Only register if value is defined
if (value) { if (value) {
registerItem(value) registerItem(value)
return () => unregisterItem(value) return () => unregisterItem(value)
} }
}, [value, registerItem, unregisterItem]) }, [value, registerItem, unregisterItem])
// Check if item should be displayed based on search
const shouldDisplay = filteredItems.includes(value) const shouldDisplay = filteredItems.includes(value)
if (!shouldDisplay) return null if (!shouldDisplay) return null
@@ -315,12 +295,10 @@ export function CommandItem({
) )
} }
// Command Separator component
export function CommandSeparator({ className }: CommandSeparatorProps) { export function CommandSeparator({ className }: CommandSeparatorProps) {
return <div className={cn('-mx-1 h-px bg-border', className)} /> return <div className={cn('-mx-1 h-px bg-border', className)} />
} }
// Export all components
export const ToolCommand = { export const ToolCommand = {
Root: Command, Root: Command,
Input: CommandInput, Input: CommandInput,

View File

@@ -1189,7 +1189,13 @@ export function ToolInput({
</div> </div>
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className='w-[200px] p-0' align='start'> <PopoverContent
className='h-[360px] w-[200px] p-0'
align='start'
side='bottom'
sideOffset={6}
avoidCollisions={false}
>
<ToolCommand.Root filter={customFilter}> <ToolCommand.Root filter={customFilter}>
<ToolCommand.Input placeholder='Search tools...' onValueChange={setSearchQuery} /> <ToolCommand.Input placeholder='Search tools...' onValueChange={setSearchQuery} />
<ToolCommand.List> <ToolCommand.List>
@@ -1749,7 +1755,13 @@ export function ToolInput({
Add Tool Add Tool
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className='w-[200px] p-0' align='start'> <PopoverContent
className='h-[360px] w-[280px] p-0'
align='start'
side='bottom'
sideOffset={6}
avoidCollisions={false}
>
<ToolCommand.Root filter={customFilter}> <ToolCommand.Root filter={customFilter}>
<ToolCommand.Input placeholder='Search tools...' onValueChange={setSearchQuery} /> <ToolCommand.Input placeholder='Search tools...' onValueChange={setSearchQuery} />
<ToolCommand.List> <ToolCommand.List>

View File

@@ -10,11 +10,11 @@ import Workflow from '@/app/workspace/[workspaceId]/logs/components/filters/comp
export function LogsFilters() { export function LogsFilters() {
const sections = [ const sections = [
{ key: 'timeline', title: 'Timeline', component: <Timeline /> },
{ key: 'level', title: 'Level', component: <Level /> }, { key: 'level', title: 'Level', component: <Level /> },
{ key: 'trigger', title: 'Trigger', component: <Trigger /> },
{ key: 'folder', title: 'Folder', component: <FolderFilter /> },
{ key: 'workflow', title: 'Workflow', component: <Workflow /> }, { key: 'workflow', title: 'Workflow', component: <Workflow /> },
{ key: 'folder', title: 'Folder', component: <FolderFilter /> },
{ key: 'trigger', title: 'Trigger', component: <Trigger /> },
{ key: 'timeline', title: 'Timeline', component: <Timeline /> },
] ]
return ( return (

View File

@@ -56,11 +56,13 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, avoidCollisions = false, sticky = 'always', ...props }, ref) => (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
avoidCollisions={avoidCollisions}
sticky={sticky as any}
className={cn( className={cn(
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in', 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in',
className className

View File

@@ -407,7 +407,7 @@ export class SearchSuggestions {
} }
/** /**
* Generate preview text for a suggestion - SIMPLE APPROACH * Generate preview text for a suggestion
* Show suggestion at the end of input, with proper spacing logic * Show suggestion at the end of input, with proper spacing logic
*/ */
generatePreview(suggestion: Suggestion, currentValue: string, cursorPosition: number): string { generatePreview(suggestion: Suggestion, currentValue: string, cursorPosition: number): string {