Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,8 @@

import { useState } from 'react'
import { Plus } from 'lucide-react'
import {
Button,
Combobox,
type ComboboxOption,
Input,
Label,
Popover,
PopoverAnchor,
PopoverContent,
PopoverItem,
PopoverScrollArea,
Trash,
} from '@/components/emcn'
import { Button, Combobox, type ComboboxOption, Label, Trash } from '@/components/emcn'
import { Input } from '@/components/ui/input'
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
Expand Down Expand Up @@ -66,21 +55,15 @@ export function KnowledgeTagFilters({
previewValue,
}: KnowledgeTagFiltersProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)

// Hook for immediate tag/dropdown selections
const emitTagSelection = useTagSelection(blockId, subBlock.id)

// Get the knowledge base ID from other sub-blocks
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseId = knowledgeBaseIdValue || null

// Use KB tag definitions hook to get available tags
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)

// Get accessible prefixes for variable highlighting
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)

// State for managing tag dropdown
const [activeTagDropdown, setActiveTagDropdown] = useState<{
rowIndex: number
showTags: boolean
Expand All @@ -89,15 +72,10 @@ export function KnowledgeTagFilters({
element?: HTMLElement | null
} | null>(null)

// State for dropdown visibility - one for each row
const [dropdownStates, setDropdownStates] = useState<Record<number, boolean>>({})

// Parse the current value to extract filters
const parseFilters = (filterValue: string | null): TagFilter[] => {
if (!filterValue) return []
try {
const parsed = JSON.parse(filterValue)
// Handle legacy format (without fieldType/operator)
return parsed.map((f: TagFilter) => ({
...f,
fieldType: f.fieldType || 'text',
Expand All @@ -111,7 +89,6 @@ export function KnowledgeTagFilters({
const currentValue = isPreview ? previewValue : storeValue
const filters = parseFilters(currentValue || null)

// Transform filters to table format for display
const rows: TagFilterRow[] =
filters.length > 0
? filters.map((filter) => ({
Expand All @@ -128,7 +105,7 @@ export function KnowledgeTagFilters({
: [
{
id: 'empty-row-0',
cells: { tagName: '', fieldType: 'text', operator: 'eq', value: '' },
cells: { tagName: '', fieldType: 'text', operator: '', value: '' },
},
]

Expand All @@ -138,17 +115,18 @@ export function KnowledgeTagFilters({
setStoreValue(value)
}

/** Convert rows back to TagFilter format */
const rowsToFilters = (rowsToConvert: TagFilterRow[]): TagFilter[] => {
return rowsToConvert.map((row) => ({
id: row.id,
tagName: row.cells.tagName || '',
tagSlot: row.cells.tagSlot,
fieldType: row.cells.fieldType || 'text',
operator: row.cells.operator || 'eq',
tagValue: row.cells.value || '',
valueTo: row.cells.valueTo,
}))
return rowsToConvert
.filter((row) => row.cells.tagName?.trim() && row.cells.value?.trim())
.map((row) => ({
id: row.id,
tagName: row.cells.tagName || '',
tagSlot: row.cells.tagSlot,
fieldType: row.cells.fieldType || 'text',
operator: row.cells.operator || 'eq',
tagValue: row.cells.value || '',
valueTo: row.cells.valueTo,
}))
}

const handleCellChange = (rowIndex: number, column: string, value: string | FilterFieldType) => {
Expand All @@ -158,15 +136,13 @@ export function KnowledgeTagFilters({
if (idx === rowIndex) {
const newCells = { ...row.cells, [column]: value }

// Reset operator when field type changes
if (column === 'fieldType') {
const operators = getOperatorsForFieldType(value as FilterFieldType)
newCells.operator = operators[0]?.value || 'eq'
newCells.value = '' // Reset value when type changes
newCells.value = ''
newCells.valueTo = undefined
}

// Reset valueTo if operator is not 'between'
if (column === 'operator' && value !== 'between') {
newCells.valueTo = undefined
}
Expand All @@ -179,11 +155,9 @@ export function KnowledgeTagFilters({
updateFilters(rowsToFilters(updatedRows))
}

/** Handle tag name selection from dropdown */
const handleTagNameSelection = (rowIndex: number, tagName: string) => {
if (isPreview || disabled) return

// Find the tag definition to get fieldType and tagSlot
const tagDef = tagDefinitions.find((t) => t.displayName === tagName)
const fieldType = (tagDef?.fieldType || 'text') as FilterFieldType
const operators = getOperatorsForFieldType(fieldType)
Expand All @@ -198,7 +172,7 @@ export function KnowledgeTagFilters({
tagSlot: tagDef?.tagSlot,
fieldType,
operator: operators[0]?.value || 'eq',
value: '', // Reset value when tag changes
value: '',
valueTo: undefined,
},
}
Expand Down Expand Up @@ -261,95 +235,63 @@ export function KnowledgeTagFilters({
}

const renderHeader = () => (
<thead>
<tr className='border-b'>
<th className='w-[35%] border-r px-2 py-2 text-center font-medium text-sm'>Tag</th>
<th className='w-[25%] border-r px-2 py-2 text-center font-medium text-sm'>Operator</th>
<th className='border-r px-2 py-2 text-center font-medium text-sm'>Value</th>
<th className='w-10' />
<thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'>
<th className='w-[35%] min-w-0 border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Tag
</th>
<th className='w-[35%] min-w-0 border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Operator
</th>
<th className='w-[30%] min-w-0 bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Value
</th>
</tr>
</thead>
)

const renderTagNameCell = (row: TagFilterRow, rowIndex: number) => {
const cellValue = row.cells.tagName || ''
const isOpen = dropdownStates[rowIndex] || false

const setIsOpen = (open: boolean) => {
setDropdownStates((prev) => ({ ...prev, [rowIndex]: open }))
}
const tagOptions: ComboboxOption[] = tagDefinitions.map((tag) => ({
value: tag.displayName,
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
}))

return (
<td className='relative border-r p-1'>
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverAnchor asChild>
<div
className='relative w-full cursor-pointer'
onClick={() => !disabled && !isLoading && setIsOpen(true)}
>
<Input
value={cellValue}
readOnly
disabled={disabled || isLoading}
autoComplete='off'
className='w-full cursor-pointer border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
/>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-[8px] font-medium font-sans text-sm'>
<span className='truncate'>{cellValue || 'Select tag'}</span>
</div>
</div>
</PopoverAnchor>
{tagDefinitions.length > 0 && (
<PopoverContent
side='bottom'
align='start'
sideOffset={4}
maxHeight={192}
className='w-[200px]'
>
<PopoverScrollArea>
{tagDefinitions.map((tag) => (
<PopoverItem
key={tag.id}
onClick={() => {
handleTagNameSelection(rowIndex, tag.displayName)
setIsOpen(false)
}}
>
<span className='flex-1 truncate'>{tag.displayName}</span>
<span className='flex-shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground'>
{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}
</span>
</PopoverItem>
))}
</PopoverScrollArea>
</PopoverContent>
)}
</Popover>
<td className='relative min-w-0 overflow-hidden border-[var(--border-strong)] border-r bg-transparent p-0'>
<Combobox
options={tagOptions}
value={cellValue}
onChange={(value) => handleTagNameSelection(rowIndex, value)}
disabled={disabled || isLoading}
placeholder='Select tag'
className='!border-0 !bg-transparent hover:!bg-transparent px-[10px] py-[8px] font-medium text-sm leading-[21px] focus-visible:ring-0 focus-visible:ring-offset-0 [&>span]:truncate'
/>
</td>
)
}

/** Render operator cell */
const renderOperatorCell = (row: TagFilterRow, rowIndex: number) => {
const fieldType = row.cells.fieldType || 'text'
const operator = row.cells.operator || 'eq'
const operator = row.cells.operator || ''
const operators = getOperatorsForFieldType(fieldType)
const isOperatorDisabled = disabled || !row.cells.tagName

const operatorOptions: ComboboxOption[] = operators.map((op) => ({
value: op.value,
label: op.label,
}))

return (
<td className='border-r p-1'>
<td className='relative min-w-0 overflow-hidden border-[var(--border-strong)] border-r bg-transparent p-0'>
<Combobox
options={operatorOptions}
value={operator}
onChange={(value) => handleCellChange(rowIndex, 'operator', value)}
disabled={disabled || !row.cells.tagName}
placeholder='Operator'
size='sm'
disabled={isOperatorDisabled}
placeholder='Select operator'
className='!border-0 !bg-transparent hover:!bg-transparent px-[10px] py-[8px] font-medium text-sm leading-[21px] focus-visible:ring-0 focus-visible:ring-offset-0 [&>span]:truncate'
/>
</td>
)
Expand All @@ -364,7 +306,6 @@ export function KnowledgeTagFilters({
const isDisabled = disabled || !row.cells.tagName
const placeholder = getPlaceholderForFieldType(fieldType)

// Single text input for all field types with variable support
const renderInput = (value: string, column: 'value' | 'valueTo') => (
<div className='relative w-full'>
<Input
Expand All @@ -375,7 +316,6 @@ export function KnowledgeTagFilters({

handleCellChange(rowIndex, column, newValue)

// Check for tag trigger (only for primary value input)
if (column === 'value') {
const tagTrigger = checkTagTrigger(newValue, cursorPosition)

Expand Down Expand Up @@ -412,10 +352,10 @@ export function KnowledgeTagFilters({
disabled={isDisabled}
autoComplete='off'
placeholder={placeholder}
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
className='w-full border-0 bg-transparent px-[10px] py-[8px] font-medium text-sm text-transparent leading-[21px] caret-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0'
/>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-[8px] font-medium font-sans text-sm'>
<div className='whitespace-pre'>
<div className='scrollbar-hide pointer-events-none absolute top-0 right-[10px] bottom-0 left-[10px] overflow-x-auto overflow-y-hidden bg-transparent'>
<div className='whitespace-pre py-[8px] font-medium text-[var(--text-primary)] text-sm leading-[21px]'>
{formatDisplayText(value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
Expand All @@ -425,11 +365,10 @@ export function KnowledgeTagFilters({
</div>
)

// Render with optional "between" second input
if (isBetween) {
return (
<td className='p-1'>
<div className='flex items-center gap-1'>
<td className='relative min-w-0 overflow-hidden bg-transparent p-0'>
<div className='flex items-center gap-1 px-[10px]'>
{renderInput(cellValue, 'value')}
<span className='flex-shrink-0 text-muted-foreground text-xs'>to</span>
{renderInput(valueTo, 'valueTo')}
Expand All @@ -438,20 +377,24 @@ export function KnowledgeTagFilters({
)
}

return <td className='p-1'>{renderInput(cellValue, 'value')}</td>
return (
<td className='relative min-w-0 overflow-hidden bg-transparent p-0'>
{renderInput(cellValue, 'value')}
</td>
)
}

const renderDeleteButton = (rowIndex: number) => {
const canDelete = !isPreview && !disabled
const canDelete = !isPreview && !disabled && rows.length > 1

return canDelete ? (
<td className='w-10 p-1'>
<td className='w-0 p-0'>
<Button
variant='ghost'
className='h-8 w-8 p-0 opacity-0 group-hover:opacity-100'
className='-translate-y-1/2 absolute top-1/2 right-[8px] opacity-0 transition-opacity group-hover:opacity-100'
onClick={() => handleDeleteRow(rowIndex)}
>
<Trash className='h-4 w-4 text-muted-foreground' />
<Trash className='h-[14px] w-[14px]' />
</Button>
</td>
) : null
Expand All @@ -462,13 +405,16 @@ export function KnowledgeTagFilters({
}

return (
<div className='relative'>
<div className='overflow-visible rounded-md border'>
<table className='w-full'>
<div className='relative w-full'>
<div className='overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
<table className='w-full table-fixed bg-transparent'>
{renderHeader()}
<tbody>
<tbody className='bg-transparent'>
{rows.map((row, rowIndex) => (
<tr key={row.id} className='group relative border-t'>
<tr
key={row.id}
className='group relative border-[var(--border-strong)] border-t bg-transparent'
>
{renderTagNameCell(row, rowIndex)}
{renderOperatorCell(row, rowIndex)}
{renderValueCell(row, rowIndex)}
Expand Down Expand Up @@ -502,7 +448,7 @@ export function KnowledgeTagFilters({
{/* Add Filter Button */}
{!isPreview && !disabled && (
<div className='mt-3 flex items-center justify-between'>
<Button variant='outline' onClick={handleAddRow} className='h-7 px-2 text-xs'>
<Button onClick={handleAddRow} className='h-7 px-2 text-xs'>
<Plus className='mr-1 h-2.5 w-2.5' />
Add Filter
</Button>
Expand Down
1 change: 0 additions & 1 deletion apps/sim/blocks/blocks/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export const KnowledgeBlock: BlockConfig = {
type: 'knowledge-tag-filters',
placeholder: 'Add tag filters',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
{
id: 'documentId',
Expand Down
Loading