Skip to content

Commit 9dd3028

Browse files
committed
improvement(knowledge): use combobox filter panel matching logs UI style
1 parent 2553cac commit 9dd3028

File tree

1 file changed

+138
-116
lines changed

1 file changed

+138
-116
lines changed

apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx

Lines changed: 138 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import { useCallback, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { useParams, useRouter } from 'next/navigation'
6-
import { Tooltip } from '@/components/emcn'
6+
import type { ComboboxOption } from '@/components/emcn'
7+
import { Combobox, Tooltip } from '@/components/emcn'
78
import { Database } from '@/components/emcn/icons'
8-
import { cn } from '@/lib/core/utils/cn'
99
import type { KnowledgeBaseData } from '@/lib/knowledge/types'
1010
import type {
1111
CreateAction,
@@ -106,8 +106,8 @@ export function Knowledge() {
106106
column: string
107107
direction: 'asc' | 'desc'
108108
} | null>(null)
109-
const [connectorFilter, setConnectorFilter] = useState<'all' | 'connected' | 'unconnected'>('all')
110-
const [contentFilter, setContentFilter] = useState<'all' | 'has-docs' | 'empty'>('all')
109+
const [connectorFilter, setConnectorFilter] = useState<string[]>([])
110+
const [contentFilter, setContentFilter] = useState<string[]>([])
111111
const [ownerFilter, setOwnerFilter] = useState<string[]>([])
112112

113113
const [searchInputValue, setSearchInputValue] = useState('')
@@ -186,20 +186,22 @@ export function Knowledge() {
186186
const processedKBs = useMemo(() => {
187187
let result = filterKnowledgeBases(knowledgeBases, debouncedSearchQuery)
188188

189-
if (connectorFilter !== 'all') {
190-
result = result.filter((kb) =>
191-
connectorFilter === 'connected'
192-
? (kb.connectorTypes?.length ?? 0) > 0
193-
: (kb.connectorTypes?.length ?? 0) === 0
194-
)
189+
if (connectorFilter.length > 0) {
190+
result = result.filter((kb) => {
191+
const hasConnectors = (kb.connectorTypes?.length ?? 0) > 0
192+
if (connectorFilter.includes('connected') && hasConnectors) return true
193+
if (connectorFilter.includes('unconnected') && !hasConnectors) return true
194+
return false
195+
})
195196
}
196197

197-
if (contentFilter !== 'all') {
198-
result = result.filter((kb) =>
199-
contentFilter === 'has-docs'
200-
? ((kb as KnowledgeBaseWithDocCount).docCount ?? 0) > 0
201-
: ((kb as KnowledgeBaseWithDocCount).docCount ?? 0) === 0
202-
)
198+
if (contentFilter.length > 0) {
199+
const docCount = (kb: KnowledgeBaseData) => (kb as KnowledgeBaseWithDocCount).docCount ?? 0
200+
result = result.filter((kb) => {
201+
if (contentFilter.includes('has-docs') && docCount(kb) > 0) return true
202+
if (contentFilter.includes('empty') && docCount(kb) === 0) return true
203+
return false
204+
})
203205
}
204206

205207
if (ownerFilter.length > 0) {
@@ -370,122 +372,142 @@ export function Knowledge() {
370372
[activeSort]
371373
)
372374

375+
const connectorDisplayLabel = useMemo(() => {
376+
if (connectorFilter.length === 0) return 'All'
377+
if (connectorFilter.length === 1)
378+
return connectorFilter[0] === 'connected' ? 'With connectors' : 'Without connectors'
379+
return `${connectorFilter.length} selected`
380+
}, [connectorFilter])
381+
382+
const contentDisplayLabel = useMemo(() => {
383+
if (contentFilter.length === 0) return 'All'
384+
if (contentFilter.length === 1)
385+
return contentFilter[0] === 'has-docs' ? 'Has documents' : 'Empty'
386+
return `${contentFilter.length} selected`
387+
}, [contentFilter])
388+
389+
const ownerDisplayLabel = useMemo(() => {
390+
if (ownerFilter.length === 0) return 'All'
391+
if (ownerFilter.length === 1)
392+
return members?.find((m) => m.userId === ownerFilter[0])?.name ?? '1 member'
393+
return `${ownerFilter.length} members`
394+
}, [ownerFilter, members])
395+
396+
const memberOptions: ComboboxOption[] = useMemo(
397+
() =>
398+
(members ?? []).map((m) => ({
399+
value: m.userId,
400+
label: m.name,
401+
iconElement: m.image ? (
402+
<img
403+
src={m.image}
404+
alt={m.name}
405+
referrerPolicy='no-referrer'
406+
className='h-[14px] w-[14px] rounded-full border border-[var(--border)] object-cover'
407+
/>
408+
) : (
409+
<span className='flex h-[14px] w-[14px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
410+
{m.name.charAt(0).toUpperCase()}
411+
</span>
412+
),
413+
})),
414+
[members]
415+
)
416+
417+
const hasActiveFilters =
418+
connectorFilter.length > 0 || contentFilter.length > 0 || ownerFilter.length > 0
419+
373420
const filterContent = (
374-
<div className='w-[200px]'>
375-
<div className='border-[var(--border-1)] border-b px-3 py-2'>
421+
<div className='flex w-[240px] flex-col gap-3 p-3'>
422+
<div className='flex flex-col gap-1.5'>
376423
<span className='font-medium text-[var(--text-secondary)] text-caption'>Connectors</span>
377-
</div>
378-
<div className='flex flex-col gap-0.5 px-3 py-2'>
379-
{(
380-
[
381-
{ value: 'all', label: 'All' },
424+
<Combobox
425+
options={[
382426
{ value: 'connected', label: 'With connectors' },
383427
{ value: 'unconnected', label: 'Without connectors' },
384-
] as const
385-
).map(({ value, label }) => (
386-
<button
387-
key={value}
388-
type='button'
389-
className={cn(
390-
'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
391-
connectorFilter === value && 'bg-[var(--surface-active)]'
392-
)}
393-
onClick={() => setConnectorFilter(value)}
394-
>
395-
{label}
396-
</button>
397-
))}
428+
]}
429+
multiSelect
430+
multiSelectValues={connectorFilter}
431+
onMultiSelectChange={setConnectorFilter}
432+
overlayContent={
433+
<span className='truncate text-[var(--text-primary)]'>{connectorDisplayLabel}</span>
434+
}
435+
showAllOption
436+
allOptionLabel='All'
437+
size='sm'
438+
className='h-[32px] w-full rounded-md'
439+
/>
398440
</div>
399-
<div className='border-[var(--border-1)] border-t border-b px-3 py-2'>
441+
<div className='flex flex-col gap-1.5'>
400442
<span className='font-medium text-[var(--text-secondary)] text-caption'>Content</span>
401-
</div>
402-
<div className='flex flex-col gap-0.5 px-3 py-2'>
403-
{(
404-
[
405-
{ value: 'all', label: 'All' },
443+
<Combobox
444+
options={[
406445
{ value: 'has-docs', label: 'Has documents' },
407446
{ value: 'empty', label: 'Empty' },
408-
] as const
409-
).map(({ value, label }) => (
410-
<button
411-
key={value}
412-
type='button'
413-
className={cn(
414-
'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
415-
contentFilter === value && 'bg-[var(--surface-active)]'
416-
)}
417-
onClick={() => setContentFilter(value)}
418-
>
419-
{label}
420-
</button>
421-
))}
447+
]}
448+
multiSelect
449+
multiSelectValues={contentFilter}
450+
onMultiSelectChange={setContentFilter}
451+
overlayContent={
452+
<span className='truncate text-[var(--text-primary)]'>{contentDisplayLabel}</span>
453+
}
454+
showAllOption
455+
allOptionLabel='All'
456+
size='sm'
457+
className='h-[32px] w-full rounded-md'
458+
/>
422459
</div>
423-
{members && members.length > 0 && (
424-
<>
425-
<div className='border-[var(--border-1)] border-t border-b px-3 py-2'>
426-
<span className='font-medium text-[var(--text-secondary)] text-caption'>Owner</span>
427-
</div>
428-
<div className='flex flex-col gap-0.5 px-3 py-2'>
429-
<button
430-
type='button'
431-
className={cn(
432-
'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
433-
ownerFilter.length === 0 && 'bg-[var(--surface-active)]'
434-
)}
435-
onClick={() => setOwnerFilter([])}
436-
>
437-
All
438-
</button>
439-
{members.map((member) => (
440-
<button
441-
key={member.userId}
442-
type='button'
443-
className={cn(
444-
'flex w-full cursor-pointer select-none items-center gap-1.5 rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
445-
ownerFilter.includes(member.userId) && 'bg-[var(--surface-active)]'
446-
)}
447-
onClick={() =>
448-
setOwnerFilter((prev) =>
449-
prev.includes(member.userId)
450-
? prev.filter((id) => id !== member.userId)
451-
: [...prev, member.userId]
452-
)
453-
}
454-
>
455-
{member.image ? (
456-
<img
457-
src={member.image}
458-
alt={member.name}
459-
referrerPolicy='no-referrer'
460-
className='h-[14px] w-[14px] shrink-0 rounded-full border border-[var(--border)] object-cover'
461-
/>
462-
) : (
463-
<span className='flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
464-
{member.name.charAt(0).toUpperCase()}
465-
</span>
466-
)}
467-
<span className='truncate'>{member.name}</span>
468-
</button>
469-
))}
470-
</div>
471-
</>
460+
{memberOptions.length > 0 && (
461+
<div className='flex flex-col gap-1.5'>
462+
<span className='font-medium text-[var(--text-secondary)] text-caption'>Owner</span>
463+
<Combobox
464+
options={memberOptions}
465+
multiSelect
466+
multiSelectValues={ownerFilter}
467+
onMultiSelectChange={setOwnerFilter}
468+
overlayContent={
469+
<span className='truncate text-[var(--text-primary)]'>{ownerDisplayLabel}</span>
470+
}
471+
searchable
472+
searchPlaceholder='Search members...'
473+
showAllOption
474+
allOptionLabel='All'
475+
size='sm'
476+
className='h-[32px] w-full rounded-md'
477+
/>
478+
</div>
479+
)}
480+
{hasActiveFilters && (
481+
<button
482+
type='button'
483+
onClick={() => {
484+
setConnectorFilter([])
485+
setContentFilter([])
486+
setOwnerFilter([])
487+
}}
488+
className='flex h-[32px] w-full items-center justify-center rounded-md text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-active)]'
489+
>
490+
Clear all filters
491+
</button>
472492
)}
473493
</div>
474494
)
475495

476496
const filterTags: FilterTag[] = useMemo(() => {
477497
const tags: FilterTag[] = []
478-
if (connectorFilter !== 'all') {
479-
tags.push({
480-
label: connectorFilter === 'connected' ? 'Connectors: Active' : 'Connectors: None',
481-
onRemove: () => setConnectorFilter('all'),
482-
})
498+
if (connectorFilter.length > 0) {
499+
const label =
500+
connectorFilter.length === 1
501+
? `Connectors: ${connectorFilter[0] === 'connected' ? 'With connectors' : 'Without connectors'}`
502+
: `Connectors: ${connectorFilter.length} types`
503+
tags.push({ label, onRemove: () => setConnectorFilter([]) })
483504
}
484-
if (contentFilter !== 'all') {
485-
tags.push({
486-
label: contentFilter === 'has-docs' ? 'Content: Has documents' : 'Content: Empty',
487-
onRemove: () => setContentFilter('all'),
488-
})
505+
if (contentFilter.length > 0) {
506+
const label =
507+
contentFilter.length === 1
508+
? `Content: ${contentFilter[0] === 'has-docs' ? 'Has documents' : 'Empty'}`
509+
: `Content: ${contentFilter.length} types`
510+
tags.push({ label, onRemove: () => setContentFilter([]) })
489511
}
490512
if (ownerFilter.length > 0) {
491513
const label =

0 commit comments

Comments
 (0)