Compare commits

...

1 Commits

Author SHA1 Message Date
waleed
e6c7bd3534 feat(kb): added tags information to kb docs table 2025-12-26 02:06:50 -08:00
3 changed files with 199 additions and 27 deletions

View File

@@ -45,6 +45,7 @@ import {
ActionBar,
AddDocumentsModal,
BaseTagsModal,
DocumentTagsCell,
} from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -53,6 +54,7 @@ import {
useKnowledgeBaseDocuments,
useKnowledgeBasesList,
} from '@/hooks/use-knowledge'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
import type { DocumentData } from '@/stores/knowledge/store'
const logger = createLogger('KnowledgeBase')
@@ -83,18 +85,17 @@ function DocumentTableRowSkeleton() {
<Skeleton className='h-[15px] w-[24px]' />
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
<div className='flex flex-col justify-center'>
<div className='flex items-center font-medium text-[12px]'>
<Skeleton className='h-[15px] w-[50px]' />
<span className='mx-[6px] hidden text-[var(--text-muted)] xl:inline'>|</span>
<Skeleton className='hidden h-[15px] w-[70px] xl:inline-block' />
</div>
<Skeleton className='mt-[2px] h-[15px] w-[40px] lg:hidden' />
</div>
<Skeleton className='h-[15px] w-[60px]' />
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
<Skeleton className='h-[24px] w-[64px] rounded-md' />
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
<div className='flex items-center gap-[4px]'>
<Skeleton className='h-[18px] w-[40px] rounded-full' />
<Skeleton className='h-[18px] w-[40px] rounded-full' />
</div>
</TableCell>
<TableCell className='py-[8px] pr-[4px] pl-[12px]'>
<div className='flex items-center gap-[4px]'>
<Skeleton className='h-[28px] w-[28px] rounded-[4px]' />
@@ -127,13 +128,16 @@ function DocumentTableSkeleton({ rowCount = 5 }: { rowCount?: number }) {
<TableHead className='hidden w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)] lg:table-cell'>
Chunks
</TableHead>
<TableHead className='w-[16%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
<TableHead className='w-[11%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
Uploaded
</TableHead>
<TableHead className='w-[12%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
<TableHead className='w-[10%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
Status
</TableHead>
<TableHead className='w-[14%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
<TableHead className='w-[12%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
Tags
</TableHead>
<TableHead className='w-[11%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
Actions
</TableHead>
</TableRow>
@@ -379,6 +383,8 @@ export function KnowledgeBase({
sortOrder,
})
const { tagDefinitions } = useKnowledgeBaseTagDefinitions(id)
const router = useRouter()
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
@@ -1061,9 +1067,12 @@ export function KnowledgeBase({
{renderSortableHeader('fileSize', 'Size', 'w-[8%]')}
{renderSortableHeader('tokenCount', 'Tokens', 'w-[8%]')}
{renderSortableHeader('chunkCount', 'Chunks', 'hidden w-[8%] lg:table-cell')}
{renderSortableHeader('uploadedAt', 'Uploaded', 'w-[16%]')}
{renderSortableHeader('processingStatus', 'Status', 'w-[12%]')}
<TableHead className='w-[14%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
{renderSortableHeader('uploadedAt', 'Uploaded', 'w-[11%]')}
{renderSortableHeader('processingStatus', 'Status', 'w-[10%]')}
<TableHead className='w-[12%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
Tags
</TableHead>
<TableHead className='w-[11%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
Actions
</TableHead>
</TableRow>
@@ -1135,20 +1144,16 @@ export function KnowledgeBase({
: '—'}
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
<div className='flex flex-col justify-center'>
<div className='flex items-center font-medium text-[12px]'>
<span>{format(new Date(doc.uploadedAt), 'h:mm a')}</span>
<span className='mx-[6px] hidden text-[var(--text-muted)] xl:inline'>
|
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='text-[12px] text-[var(--text-muted)]'>
{format(new Date(doc.uploadedAt), 'MMM d')}
</span>
<span className='hidden text-[var(--text-muted)] xl:inline'>
{format(new Date(doc.uploadedAt), 'MMM d, yyyy')}
</span>
</div>
<div className='mt-[2px] text-[12px] text-[var(--text-muted)] lg:hidden'>
{format(new Date(doc.uploadedAt), 'MMM d')}
</div>
</div>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{format(new Date(doc.uploadedAt), 'MMM d, yyyy h:mm a')}
</Tooltip.Content>
</Tooltip.Root>
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
{doc.processingStatus === 'failed' && doc.processingError ? (
@@ -1166,6 +1171,9 @@ export function KnowledgeBase({
<div className={statusDisplay.className}>{statusDisplay.text}</div>
)}
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
<DocumentTagsCell document={doc} tagDefinitions={tagDefinitions} />
</TableCell>
<TableCell className='py-[8px] pr-[4px] pl-[12px]'>
<div className='flex items-center gap-[4px]'>
{doc.processingStatus === 'failed' && (

View File

@@ -0,0 +1,163 @@
'use client'
import { useMemo } from 'react'
import { format } from 'date-fns'
import { Badge, Popover, PopoverAnchor, PopoverContent, Tooltip } from '@/components/emcn'
import type { TagDefinition } from '@/hooks/use-knowledge-base-tag-definitions'
import type { DocumentData } from '@/stores/knowledge/store'
/** All tag slot keys that can hold values */
const TAG_SLOTS = [
'tag1',
'tag2',
'tag3',
'tag4',
'tag5',
'tag6',
'tag7',
'number1',
'number2',
'number3',
'number4',
'number5',
'date1',
'date2',
'boolean1',
'boolean2',
'boolean3',
] as const
type TagSlot = (typeof TAG_SLOTS)[number]
interface TagValue {
slot: TagSlot
displayName: string
value: string
fieldType: string
}
interface DocumentTagsCellProps {
document: DocumentData
tagDefinitions: TagDefinition[]
}
/**
* Formats a tag value based on its field type
*/
function formatTagValue(value: unknown, fieldType: string): string {
if (value === null || value === undefined) return ''
switch (fieldType) {
case 'date':
try {
return format(new Date(value as string), 'MMM d, yyyy')
} catch {
return String(value)
}
case 'boolean':
return value ? 'Yes' : 'No'
case 'number':
return typeof value === 'number' ? value.toLocaleString() : String(value)
default:
return String(value)
}
}
/**
* Gets the field type for a tag slot
*/
function getFieldType(slot: TagSlot): string {
if (slot.startsWith('tag')) return 'text'
if (slot.startsWith('number')) return 'number'
if (slot.startsWith('date')) return 'date'
if (slot.startsWith('boolean')) return 'boolean'
return 'text'
}
/**
* Cell component that displays document tags as compact badges with overflow popover
*/
export function DocumentTagsCell({ document, tagDefinitions }: DocumentTagsCellProps) {
const tags = useMemo(() => {
const result: TagValue[] = []
for (const slot of TAG_SLOTS) {
const value = document[slot]
if (value === null || value === undefined) continue
const definition = tagDefinitions.find((def) => def.tagSlot === slot)
const fieldType = definition?.fieldType || getFieldType(slot)
const formattedValue = formatTagValue(value, fieldType)
if (!formattedValue) continue
result.push({
slot,
displayName: definition?.displayName || slot,
value: formattedValue,
fieldType,
})
}
return result
}, [document, tagDefinitions])
if (tags.length === 0) {
return <span className='text-[11px] text-[var(--text-muted)]'></span>
}
const visibleTags = tags.slice(0, 2)
const overflowTags = tags.slice(2)
const hasOverflow = overflowTags.length > 0
return (
<div className='flex items-center gap-[4px]' onClick={(e) => e.stopPropagation()}>
{visibleTags.map((tag) => (
<Tooltip.Root key={tag.slot}>
<Tooltip.Trigger asChild>
<Badge className='max-w-[80px] truncate px-[6px] py-[1px] text-[10px]'>
{tag.value}
</Badge>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{tag.displayName}: {tag.value}
</Tooltip.Content>
</Tooltip.Root>
))}
{hasOverflow && (
<Popover>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<PopoverAnchor asChild>
<Badge
variant='outline'
className='cursor-pointer px-[6px] py-[1px] text-[10px] hover:bg-[var(--surface-6)]'
>
+{overflowTags.length}
</Badge>
</PopoverAnchor>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{overflowTags.map((tag) => tag.displayName).join(', ')}
</Tooltip.Content>
</Tooltip.Root>
<PopoverContent side='bottom' align='start' maxWidth={220} minWidth={160}>
<div className='flex flex-col gap-[2px]'>
{tags.map((tag) => (
<div
key={tag.slot}
className='flex items-center justify-between gap-[8px] rounded-[4px] px-[6px] py-[4px] text-[11px]'
>
<span className='text-[var(--text-muted)]'>{tag.displayName}</span>
<span className='max-w-[100px] truncate text-[var(--text-primary)]'>
{tag.value}
</span>
</div>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
)
}

View File

@@ -1,3 +1,4 @@
export { ActionBar } from './action-bar/action-bar'
export { AddDocumentsModal } from './add-documents-modal/add-documents-modal'
export { BaseTagsModal } from './base-tags-modal/base-tags-modal'
export { DocumentTagsCell } from './document-tags-cell/document-tags-cell'