improvement(ui): updated kb tag component to match existing table (#2498)

* improvement(ui): updated kb tag component to match existing table

* fix selection

* fix more ui

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Waleed
2025-12-19 22:26:24 -08:00
committed by GitHub
parent f91beb324e
commit 6385d82b85
4 changed files with 148 additions and 236 deletions

View File

@@ -1,18 +1,9 @@
'use client' 'use client'
import { useMemo, useRef, useState } from 'react' import { useMemo, useRef } from 'react'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
import { import { Button, Combobox, type ComboboxOption, Label, Trash } from '@/components/emcn'
Button, import { Input } from '@/components/ui/input'
Input,
Label,
Popover,
PopoverAnchor,
PopoverContent,
PopoverItem,
PopoverScrollArea,
Trash,
} from '@/components/emcn'
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants' import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
@@ -74,9 +65,6 @@ export function DocumentTagEntry({
const emitTagSelection = useTagSelection(blockId, subBlock.id) const emitTagSelection = useTagSelection(blockId, subBlock.id)
// State for tag name dropdown visibility - one for each row
const [dropdownStates, setDropdownStates] = useState<Record<number, boolean>>({})
// Use preview value when in preview mode, otherwise use store value // Use preview value when in preview mode, otherwise use store value
const currentValue = isPreview ? previewValue : storeValue const currentValue = isPreview ? previewValue : storeValue
@@ -194,7 +182,13 @@ export function DocumentTagEntry({
} }
const handleDeleteRow = (rowIndex: number) => { const handleDeleteRow = (rowIndex: number) => {
if (isPreview || disabled || rows.length <= 1) return if (isPreview || disabled) return
if (rows.length <= 1) {
// Clear the single row instead of deleting
setStoreValue('')
return
}
const updatedRows = rows.filter((_, idx) => idx !== rowIndex) const updatedRows = rows.filter((_, idx) => idx !== rowIndex)
const tableDataForStorage = updatedRows.map((row) => ({ const tableDataForStorage = updatedRows.map((row) => ({
@@ -227,88 +221,55 @@ export function DocumentTagEntry({
if (tagDefinitions.length === 0) { if (tagDefinitions.length === 0) {
return ( return (
<div className='rounded-md border p-4 text-center text-muted-foreground text-sm'> <div className='flex h-32 items-center justify-center rounded-lg border border-muted-foreground/25 bg-muted/20'>
No tags defined for this knowledge base. <div className='text-center'>
<br /> <p className='font-medium text-[var(--text-secondary)] text-sm'>
Define tags at the knowledge base level first. No tags defined for this knowledge base
</p>
<p className='mt-1 text-[var(--text-muted)] text-xs'>
Define tags at the knowledge base level first
</p>
</div>
</div> </div>
) )
} }
const renderHeader = () => ( const renderHeader = () => (
<thead> <thead className='bg-transparent'>
<tr className='border-b'> <tr className='border-[var(--border-strong)] border-b bg-transparent'>
<th className='w-2/5 border-r px-4 py-2 text-center font-medium text-sm'>Tag</th> <th className='w-[50%] 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)]'>
<th className='border-r px-4 py-2 text-center font-medium text-sm'>Value</th> Tag
<th className='w-10' /> </th>
<th className='w-[50%] min-w-0 bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Value
</th>
</tr> </tr>
</thead> </thead>
) )
const renderTagNameCell = (row: DocumentTagRow, rowIndex: number) => { const renderTagNameCell = (row: DocumentTagRow, rowIndex: number) => {
const cellValue = row.cells.tagName || '' const cellValue = row.cells.tagName || ''
const isOpen = dropdownStates[rowIndex] || false
const setIsOpen = (open: boolean) => {
setDropdownStates((prev) => ({ ...prev, [rowIndex]: open }))
}
// Show tags that are either available OR currently selected for this row // Show tags that are either available OR currently selected for this row
const selectableTags = tagDefinitions.filter( const selectableTags = tagDefinitions.filter(
(def) => def.displayName === cellValue || !usedTagNames.has(def.displayName.toLowerCase()) (def) => def.displayName === cellValue || !usedTagNames.has(def.displayName.toLowerCase())
) )
const tagOptions: ComboboxOption[] = selectableTags.map((tag) => ({
value: tag.displayName,
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
}))
return ( return (
<td className='relative border-r p-1'> <td className='relative min-w-0 overflow-hidden border-[var(--border-strong)] border-r bg-transparent p-0'>
<Popover open={isOpen} onOpenChange={setIsOpen}> <Combobox
<PopoverAnchor asChild> options={tagOptions}
<div value={cellValue}
className='relative w-full cursor-pointer' onChange={(value) => handleTagSelection(rowIndex, value)}
onClick={() => !disabled && setIsOpen(true)} disabled={disabled || isLoading}
> placeholder='Select tag'
<Input 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'
value={cellValue} />
readOnly
disabled={disabled}
autoComplete='off'
placeholder='Select tag'
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 || <span className='text-muted-foreground/50'>Select tag</span>}
</span>
</div>
</div>
</PopoverAnchor>
{selectableTags.length > 0 && (
<PopoverContent
side='bottom'
align='start'
sideOffset={4}
maxHeight={192}
className='w-[200px]'
>
<PopoverScrollArea>
{selectableTags.map((tagDef) => (
<PopoverItem
key={tagDef.id}
active={tagDef.displayName === cellValue}
onClick={() => {
handleTagSelection(rowIndex, tagDef.displayName)
setIsOpen(false)
}}
>
<span className='flex-1 truncate'>{tagDef.displayName}</span>
<span className='flex-shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground'>
{FIELD_TYPE_LABELS[tagDef.fieldType] || 'Text'}
</span>
</PopoverItem>
))}
</PopoverScrollArea>
</PopoverContent>
)}
</Popover>
</td> </td>
) )
} }
@@ -333,7 +294,7 @@ export function DocumentTagEntry({
) )
return ( return (
<td className='p-1'> <td className='relative min-w-0 overflow-hidden bg-transparent p-0'>
<div className='relative w-full'> <div className='relative w-full'>
<Input <Input
ref={(el) => { ref={(el) => {
@@ -347,10 +308,10 @@ export function DocumentTagEntry({
disabled={disabled || !isTagSelected} disabled={disabled || !isTagSelected}
autoComplete='off' autoComplete='off'
placeholder={isTagSelected ? placeholder : 'Select a tag first'} placeholder={isTagSelected ? placeholder : 'Select a tag first'}
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='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'> <div className='whitespace-pre py-[8px] font-medium text-[var(--text-primary)] text-sm leading-[21px]'>
{formatDisplayText(cellValue, { {formatDisplayText(cellValue, {
accessiblePrefixes, accessiblePrefixes,
highlightAll: !accessiblePrefixes, highlightAll: !accessiblePrefixes,
@@ -379,29 +340,32 @@ export function DocumentTagEntry({
} }
const renderDeleteButton = (rowIndex: number) => { const renderDeleteButton = (rowIndex: number) => {
const canDelete = !isPreview && !disabled if (isPreview || disabled) return null
return canDelete ? ( return (
<td className='w-10 p-1'> <td className='w-0 p-0'>
<Button <Button
variant='ghost' 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] transition-opacity'
onClick={() => handleDeleteRow(rowIndex)} onClick={() => handleDeleteRow(rowIndex)}
> >
<Trash className='h-4 w-4 text-muted-foreground' /> <Trash className='h-[14px] w-[14px]' />
</Button> </Button>
</td> </td>
) : null )
} }
return ( return (
<div className='relative'> <div className='relative w-full'>
<div className='overflow-visible rounded-md border'> <div className='overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
<table className='w-full'> <table className='w-full table-fixed bg-transparent'>
{renderHeader()} {renderHeader()}
<tbody> <tbody className='bg-transparent'>
{rows.map((row, rowIndex) => ( {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)} {renderTagNameCell(row, rowIndex)}
{renderValueCell(row, rowIndex)} {renderValueCell(row, rowIndex)}
{renderDeleteButton(rowIndex)} {renderDeleteButton(rowIndex)}
@@ -411,15 +375,10 @@ export function DocumentTagEntry({
</table> </table>
</div> </div>
{/* Add Row Button */} {/* Add Tag Button */}
{!isPreview && !disabled && ( {!isPreview && !disabled && (
<div className='mt-3'> <div className='mt-3'>
<Button <Button onClick={handleAddRow} disabled={!canAddMoreTags} className='h-7 px-2 text-xs'>
variant='outline'
onClick={handleAddRow}
disabled={!canAddMoreTags}
className='h-7 px-2 text-xs'
>
<Plus className='mr-1 h-2.5 w-2.5' /> <Plus className='mr-1 h-2.5 w-2.5' />
Add Tag Add Tag
</Button> </Button>

View File

@@ -2,19 +2,8 @@
import { useState } from 'react' import { useState } from 'react'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
import { import { Button, Combobox, type ComboboxOption, Label, Trash } from '@/components/emcn'
Button, import { Input } from '@/components/ui/input'
Combobox,
type ComboboxOption,
Input,
Label,
Popover,
PopoverAnchor,
PopoverContent,
PopoverItem,
PopoverScrollArea,
Trash,
} from '@/components/emcn'
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants' import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types' 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' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
@@ -66,21 +55,15 @@ export function KnowledgeTagFilters({
previewValue, previewValue,
}: KnowledgeTagFiltersProps) { }: KnowledgeTagFiltersProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id) const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
// Hook for immediate tag/dropdown selections
const emitTagSelection = useTagSelection(blockId, subBlock.id) const emitTagSelection = useTagSelection(blockId, subBlock.id)
// Get the knowledge base ID from other sub-blocks
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId') const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseId = knowledgeBaseIdValue || null const knowledgeBaseId = knowledgeBaseIdValue || null
// Use KB tag definitions hook to get available tags
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
// Get accessible prefixes for variable highlighting
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
// State for managing tag dropdown
const [activeTagDropdown, setActiveTagDropdown] = useState<{ const [activeTagDropdown, setActiveTagDropdown] = useState<{
rowIndex: number rowIndex: number
showTags: boolean showTags: boolean
@@ -89,15 +72,10 @@ export function KnowledgeTagFilters({
element?: HTMLElement | null element?: HTMLElement | null
} | null>(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[] => { const parseFilters = (filterValue: string | null): TagFilter[] => {
if (!filterValue) return [] if (!filterValue) return []
try { try {
const parsed = JSON.parse(filterValue) const parsed = JSON.parse(filterValue)
// Handle legacy format (without fieldType/operator)
return parsed.map((f: TagFilter) => ({ return parsed.map((f: TagFilter) => ({
...f, ...f,
fieldType: f.fieldType || 'text', fieldType: f.fieldType || 'text',
@@ -111,7 +89,6 @@ export function KnowledgeTagFilters({
const currentValue = isPreview ? previewValue : storeValue const currentValue = isPreview ? previewValue : storeValue
const filters = parseFilters(currentValue || null) const filters = parseFilters(currentValue || null)
// Transform filters to table format for display
const rows: TagFilterRow[] = const rows: TagFilterRow[] =
filters.length > 0 filters.length > 0
? filters.map((filter) => ({ ? filters.map((filter) => ({
@@ -128,7 +105,7 @@ export function KnowledgeTagFilters({
: [ : [
{ {
id: 'empty-row-0', id: 'empty-row-0',
cells: { tagName: '', fieldType: 'text', operator: 'eq', value: '' }, cells: { tagName: '', fieldType: 'text', operator: '', value: '' },
}, },
] ]
@@ -138,17 +115,18 @@ export function KnowledgeTagFilters({
setStoreValue(value) setStoreValue(value)
} }
/** Convert rows back to TagFilter format */
const rowsToFilters = (rowsToConvert: TagFilterRow[]): TagFilter[] => { const rowsToFilters = (rowsToConvert: TagFilterRow[]): TagFilter[] => {
return rowsToConvert.map((row) => ({ return rowsToConvert
id: row.id, .filter((row) => row.cells.tagName?.trim())
tagName: row.cells.tagName || '', .map((row) => ({
tagSlot: row.cells.tagSlot, id: row.id,
fieldType: row.cells.fieldType || 'text', tagName: row.cells.tagName || '',
operator: row.cells.operator || 'eq', tagSlot: row.cells.tagSlot,
tagValue: row.cells.value || '', fieldType: row.cells.fieldType || 'text',
valueTo: row.cells.valueTo, operator: row.cells.operator || 'eq',
})) tagValue: row.cells.value || '',
valueTo: row.cells.valueTo,
}))
} }
const handleCellChange = (rowIndex: number, column: string, value: string | FilterFieldType) => { const handleCellChange = (rowIndex: number, column: string, value: string | FilterFieldType) => {
@@ -158,15 +136,13 @@ export function KnowledgeTagFilters({
if (idx === rowIndex) { if (idx === rowIndex) {
const newCells = { ...row.cells, [column]: value } const newCells = { ...row.cells, [column]: value }
// Reset operator when field type changes
if (column === 'fieldType') { if (column === 'fieldType') {
const operators = getOperatorsForFieldType(value as FilterFieldType) const operators = getOperatorsForFieldType(value as FilterFieldType)
newCells.operator = operators[0]?.value || 'eq' newCells.operator = operators[0]?.value || 'eq'
newCells.value = '' // Reset value when type changes newCells.value = ''
newCells.valueTo = undefined newCells.valueTo = undefined
} }
// Reset valueTo if operator is not 'between'
if (column === 'operator' && value !== 'between') { if (column === 'operator' && value !== 'between') {
newCells.valueTo = undefined newCells.valueTo = undefined
} }
@@ -179,11 +155,9 @@ export function KnowledgeTagFilters({
updateFilters(rowsToFilters(updatedRows)) updateFilters(rowsToFilters(updatedRows))
} }
/** Handle tag name selection from dropdown */
const handleTagNameSelection = (rowIndex: number, tagName: string) => { const handleTagNameSelection = (rowIndex: number, tagName: string) => {
if (isPreview || disabled) return if (isPreview || disabled) return
// Find the tag definition to get fieldType and tagSlot
const tagDef = tagDefinitions.find((t) => t.displayName === tagName) const tagDef = tagDefinitions.find((t) => t.displayName === tagName)
const fieldType = (tagDef?.fieldType || 'text') as FilterFieldType const fieldType = (tagDef?.fieldType || 'text') as FilterFieldType
const operators = getOperatorsForFieldType(fieldType) const operators = getOperatorsForFieldType(fieldType)
@@ -198,7 +172,7 @@ export function KnowledgeTagFilters({
tagSlot: tagDef?.tagSlot, tagSlot: tagDef?.tagSlot,
fieldType, fieldType,
operator: operators[0]?.value || 'eq', operator: operators[0]?.value || 'eq',
value: '', // Reset value when tag changes value: '',
valueTo: undefined, valueTo: undefined,
}, },
} }
@@ -242,7 +216,14 @@ export function KnowledgeTagFilters({
} }
const handleDeleteRow = (rowIndex: number) => { const handleDeleteRow = (rowIndex: number) => {
if (isPreview || disabled || rows.length <= 1) return if (isPreview || disabled) return
if (rows.length <= 1) {
// Clear the single row instead of deleting
setStoreValue(null)
return
}
const updatedRows = rows.filter((_, idx) => idx !== rowIndex) const updatedRows = rows.filter((_, idx) => idx !== rowIndex)
updateFilters(rowsToFilters(updatedRows)) updateFilters(rowsToFilters(updatedRows))
} }
@@ -261,80 +242,48 @@ export function KnowledgeTagFilters({
} }
const renderHeader = () => ( const renderHeader = () => (
<thead> <thead className='bg-transparent'>
<tr className='border-b'> <tr className='border-[var(--border-strong)] border-b bg-transparent'>
<th className='w-[35%] border-r px-2 py-2 text-center font-medium text-sm'>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)]'>
<th className='w-[25%] border-r px-2 py-2 text-center font-medium text-sm'>Operator</th> Tag
<th className='border-r px-2 py-2 text-center font-medium text-sm'>Value</th> </th>
<th className='w-10' /> <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> </tr>
</thead> </thead>
) )
const renderTagNameCell = (row: TagFilterRow, rowIndex: number) => { const renderTagNameCell = (row: TagFilterRow, rowIndex: number) => {
const cellValue = row.cells.tagName || '' const cellValue = row.cells.tagName || ''
const isOpen = dropdownStates[rowIndex] || false
const setIsOpen = (open: boolean) => { const tagOptions: ComboboxOption[] = tagDefinitions.map((tag) => ({
setDropdownStates((prev) => ({ ...prev, [rowIndex]: open })) value: tag.displayName,
} label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
}))
return ( return (
<td className='relative border-r p-1'> <td className='relative min-w-0 overflow-hidden border-[var(--border-strong)] border-r bg-transparent p-0'>
<Popover open={isOpen} onOpenChange={setIsOpen}> <Combobox
<PopoverAnchor asChild> options={tagOptions}
<div value={cellValue}
className='relative w-full cursor-pointer' onChange={(value) => handleTagNameSelection(rowIndex, value)}
onClick={() => !disabled && !isLoading && setIsOpen(true)} disabled={disabled || isLoading}
> placeholder='Select tag'
<Input 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'
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> </td>
) )
} }
/** Render operator cell */
const renderOperatorCell = (row: TagFilterRow, rowIndex: number) => { const renderOperatorCell = (row: TagFilterRow, rowIndex: number) => {
const fieldType = row.cells.fieldType || 'text' const fieldType = row.cells.fieldType || 'text'
const operator = row.cells.operator || 'eq' const operator = row.cells.operator || ''
const operators = getOperatorsForFieldType(fieldType) const operators = getOperatorsForFieldType(fieldType)
const isOperatorDisabled = disabled || !row.cells.tagName
const operatorOptions: ComboboxOption[] = operators.map((op) => ({ const operatorOptions: ComboboxOption[] = operators.map((op) => ({
value: op.value, value: op.value,
@@ -342,14 +291,14 @@ export function KnowledgeTagFilters({
})) }))
return ( 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 <Combobox
options={operatorOptions} options={operatorOptions}
value={operator} value={operator}
onChange={(value) => handleCellChange(rowIndex, 'operator', value)} onChange={(value) => handleCellChange(rowIndex, 'operator', value)}
disabled={disabled || !row.cells.tagName} disabled={isOperatorDisabled}
placeholder='Operator' placeholder='Select operator'
size='sm' 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> </td>
) )
@@ -364,7 +313,6 @@ export function KnowledgeTagFilters({
const isDisabled = disabled || !row.cells.tagName const isDisabled = disabled || !row.cells.tagName
const placeholder = getPlaceholderForFieldType(fieldType) const placeholder = getPlaceholderForFieldType(fieldType)
// Single text input for all field types with variable support
const renderInput = (value: string, column: 'value' | 'valueTo') => ( const renderInput = (value: string, column: 'value' | 'valueTo') => (
<div className='relative w-full'> <div className='relative w-full'>
<Input <Input
@@ -375,7 +323,6 @@ export function KnowledgeTagFilters({
handleCellChange(rowIndex, column, newValue) handleCellChange(rowIndex, column, newValue)
// Check for tag trigger (only for primary value input)
if (column === 'value') { if (column === 'value') {
const tagTrigger = checkTagTrigger(newValue, cursorPosition) const tagTrigger = checkTagTrigger(newValue, cursorPosition)
@@ -412,10 +359,10 @@ export function KnowledgeTagFilters({
disabled={isDisabled} disabled={isDisabled}
autoComplete='off' autoComplete='off'
placeholder={placeholder} 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='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'> <div className='whitespace-pre py-[8px] font-medium text-[var(--text-primary)] text-sm leading-[21px]'>
{formatDisplayText(value || '', { {formatDisplayText(value || '', {
accessiblePrefixes, accessiblePrefixes,
highlightAll: !accessiblePrefixes, highlightAll: !accessiblePrefixes,
@@ -425,11 +372,10 @@ export function KnowledgeTagFilters({
</div> </div>
) )
// Render with optional "between" second input
if (isBetween) { if (isBetween) {
return ( return (
<td className='p-1'> <td className='relative min-w-0 overflow-hidden bg-transparent p-0'>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1 px-[10px]'>
{renderInput(cellValue, 'value')} {renderInput(cellValue, 'value')}
<span className='flex-shrink-0 text-muted-foreground text-xs'>to</span> <span className='flex-shrink-0 text-muted-foreground text-xs'>to</span>
{renderInput(valueTo, 'valueTo')} {renderInput(valueTo, 'valueTo')}
@@ -438,23 +384,27 @@ 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 renderDeleteButton = (rowIndex: number) => {
const canDelete = !isPreview && !disabled if (isPreview || disabled) return null
return canDelete ? ( return (
<td className='w-10 p-1'> <td className='w-0 p-0'>
<Button <Button
variant='ghost' 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] transition-opacity'
onClick={() => handleDeleteRow(rowIndex)} onClick={() => handleDeleteRow(rowIndex)}
> >
<Trash className='h-4 w-4 text-muted-foreground' /> <Trash className='h-[14px] w-[14px]' />
</Button> </Button>
</td> </td>
) : null )
} }
if (isLoading) { if (isLoading) {
@@ -462,13 +412,16 @@ export function KnowledgeTagFilters({
} }
return ( return (
<div className='relative'> <div className='relative w-full'>
<div className='overflow-visible rounded-md border'> <div className='overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
<table className='w-full'> <table className='w-full table-fixed bg-transparent'>
{renderHeader()} {renderHeader()}
<tbody> <tbody className='bg-transparent'>
{rows.map((row, rowIndex) => ( {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)} {renderTagNameCell(row, rowIndex)}
{renderOperatorCell(row, rowIndex)} {renderOperatorCell(row, rowIndex)}
{renderValueCell(row, rowIndex)} {renderValueCell(row, rowIndex)}
@@ -502,7 +455,7 @@ export function KnowledgeTagFilters({
{/* Add Filter Button */} {/* Add Filter Button */}
{!isPreview && !disabled && ( {!isPreview && !disabled && (
<div className='mt-3 flex items-center justify-between'> <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' /> <Plus className='mr-1 h-2.5 w-2.5' />
Add Filter Add Filter
</Button> </Button>

View File

@@ -57,7 +57,6 @@ export const KnowledgeBlock: BlockConfig = {
type: 'knowledge-tag-filters', type: 'knowledge-tag-filters',
placeholder: 'Add tag filters', placeholder: 'Add tag filters',
condition: { field: 'operation', value: 'search' }, condition: { field: 'operation', value: 'search' },
mode: 'advanced',
}, },
{ {
id: 'documentId', id: 'documentId',

View File

@@ -18,8 +18,13 @@
import * as React from 'react' import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority' import { cva, type VariantProps } from 'class-variance-authority'
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react' import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'
import { Button } from '@/components/emcn/components/button/button'
import {
Popover,
PopoverAnchor,
PopoverContent,
} from '@/components/emcn/components/popover/popover'
import { cn } from '@/lib/core/utils/cn' import { cn } from '@/lib/core/utils/cn'
import { Popover, PopoverAnchor, PopoverContent } from '../popover/popover'
/** /**
* Variant styles for the date picker trigger button. * Variant styles for the date picker trigger button.
@@ -388,13 +393,9 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(
{/* Today Button */} {/* Today Button */}
<div className='border-[var(--surface-11)] border-t px-[8px] py-[8px]'> <div className='border-[var(--surface-11)] border-t px-[8px] py-[8px]'>
<button <Button variant='active' className='w-full' onClick={handleSelectToday}>
type='button'
className='w-full rounded-[4px] py-[6px] text-[12px] text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]'
onClick={handleSelectToday}
>
Today Today
</button> </Button>
</div> </div>
</PopoverContent> </PopoverContent>
</div> </div>