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>
{/* Timeline Filter */}
<FilterSection title='Timeline' content={<Timeline />} />
{/* Level Filter */}
<FilterSection title='Level' content={<Level />} />
{/* Trigger Filter */}
<FilterSection title='Trigger' content={<Trigger />} />
{/* Workflow Filter */}
<FilterSection title='Workflow' content={<Workflow />} />
{/* Folder Filter */}
<FilterSection title='Folder' content={<FolderFilter />} />
{/* Workflow Filter */}
<FilterSection title='Workflow' content={<Workflow />} />
{/* Trigger Filter */}
<FilterSection title='Trigger' content={<Trigger />} />
{/* Timeline Filter */}
<FilterSection title='Timeline' content={<Timeline />} />
</div>
)
}

View File

@@ -1,6 +1,6 @@
'use client'
import { useEffect, useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { Loader2, Search, X } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -89,6 +89,16 @@ export function AutocompleteSearch({
}
}, [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 newValue = e.target.value
const cursorPos = e.target.selectionStart || 0
@@ -126,7 +136,7 @@ export function AutocompleteSearch({
state.isOpen && 'ring-1 ring-ring'
)}
>
{state.pendingQuery ? (
{showSpinner ? (
<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} />

View File

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

View File

@@ -1189,7 +1189,13 @@ export function ToolInput({
</div>
</div>
</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.Input placeholder='Search tools...' onValueChange={setSearchQuery} />
<ToolCommand.List>
@@ -1749,7 +1755,13 @@ export function ToolInput({
Add Tool
</Button>
</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.Input placeholder='Search tools...' onValueChange={setSearchQuery} />
<ToolCommand.List>

View File

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

View File

@@ -56,11 +56,13 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef<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.Content
ref={ref}
sideOffset={sideOffset}
avoidCollisions={avoidCollisions}
sticky={sticky as any}
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',
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
*/
generatePreview(suggestion: Suggestion, currentValue: string, cursorPosition: number): string {