mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -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>
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user