mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 06:33:52 -05:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user