feat(kb): added tags information to kb docs table (#2589)

* feat(kb): added tags information to kb docs table

* improvement(base): table and tags styling

---------

Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
This commit is contained in:
Waleed
2025-12-26 13:54:33 -08:00
committed by GitHub
parent 66b8434861
commit 7793a6d597

View File

@@ -18,6 +18,7 @@ import {
} from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import {
Badge,
Breadcrumb,
Button,
Modal,
@@ -40,6 +41,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table'
import { cn } from '@/lib/core/utils/cn'
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
import {
ActionBar,
@@ -53,6 +55,10 @@ import {
useKnowledgeBaseDocuments,
useKnowledgeBasesList,
} from '@/hooks/use-knowledge'
import {
type TagDefinition,
useKnowledgeBaseTagDefinitions,
} from '@/hooks/use-knowledge-base-tag-definitions'
import type { DocumentData } from '@/stores/knowledge/store'
const logger = createLogger('KnowledgeBase')
@@ -73,28 +79,27 @@ function DocumentTableRowSkeleton() {
<Skeleton className='h-[17px] w-[120px]' />
</div>
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
<TableCell className='hidden px-[12px] py-[8px] lg:table-cell'>
<Skeleton className='h-[15px] w-[48px]' />
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
<TableCell className='hidden px-[12px] py-[8px] lg:table-cell'>
<Skeleton className='h-[15px] w-[32px]' />
</TableCell>
<TableCell className='hidden px-[12px] py-[8px] lg:table-cell'>
<TableCell className='px-[12px] py-[8px]'>
<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]' />
@@ -118,22 +123,25 @@ function DocumentTableSkeleton({ rowCount = 5 }: { rowCount?: number }) {
<TableHead className='w-[180px] max-w-[180px] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
Name
</TableHead>
<TableHead className='w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
<TableHead className='hidden w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)] lg:table-cell'>
Size
</TableHead>
<TableHead className='w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
<TableHead className='hidden w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)] lg:table-cell'>
Tokens
</TableHead>
<TableHead className='hidden w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)] lg:table-cell'>
<TableHead className='w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
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>
@@ -274,58 +282,124 @@ function formatFileSize(bytes: number): string {
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
}
const getStatusDisplay = (doc: DocumentData) => {
// Consolidated status: show processing status when not completed, otherwise show enabled/disabled
const AnimatedLoader = ({ className }: { className?: string }) => (
<Loader2 className={cn(className, 'animate-spin')} />
)
const getStatusBadge = (doc: DocumentData) => {
switch (doc.processingStatus) {
case 'pending':
return {
text: 'Pending',
className:
'inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-300',
}
return (
<Badge variant='gray' size='sm'>
Pending
</Badge>
)
case 'processing':
return {
text: (
<>
<Loader2 className='mr-1.5 h-3 w-3 animate-spin' />
Processing
</>
),
className:
'inline-flex items-center rounded-md bg-purple-100 px-2 py-1 text-xs font-medium text-[var(--brand-primary-hex)] dark:bg-purple-900/30 dark:text-[var(--brand-primary-hex)]',
}
return (
<Badge variant='purple' size='sm' icon={AnimatedLoader}>
Processing
</Badge>
)
case 'failed':
return {
text: (
<>
Failed
{doc.processingError && <AlertCircle className='ml-1.5 h-3 w-3' />}
</>
),
className:
'inline-flex items-center rounded-md bg-red-100 px-2 py-1 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-300',
}
return doc.processingError ? (
<Badge variant='red' size='sm' icon={AlertCircle}>
Failed
</Badge>
) : (
<Badge variant='red' size='sm'>
Failed
</Badge>
)
case 'completed':
return doc.enabled
? {
text: 'Enabled',
className:
'inline-flex items-center rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 dark:bg-green-900/30 dark:text-green-400',
}
: {
text: 'Disabled',
className:
'inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-300',
}
return doc.enabled ? (
<Badge variant='green' size='sm'>
Enabled
</Badge>
) : (
<Badge variant='gray' size='sm'>
Disabled
</Badge>
)
default:
return {
text: 'Unknown',
className:
'inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-300',
}
return (
<Badge variant='gray' size='sm'>
Unknown
</Badge>
)
}
}
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
}
const TAG_FIELD_TYPES: Record<string, string> = {
tag: 'text',
number: 'number',
date: 'date',
boolean: 'boolean',
}
/**
* Computes tag values for a document
*/
function getDocumentTags(doc: DocumentData, definitions: TagDefinition[]): TagValue[] {
const result: TagValue[] = []
for (const slot of TAG_SLOTS) {
const raw = doc[slot]
if (raw == null) continue
const def = definitions.find((d) => d.tagSlot === slot)
const fieldType = def?.fieldType || TAG_FIELD_TYPES[slot.replace(/\d+$/, '')] || 'text'
let value: string
if (fieldType === 'date') {
try {
value = format(new Date(raw as string), 'MMM d, yyyy')
} catch {
value = String(raw)
}
} else if (fieldType === 'boolean') {
value = raw ? 'Yes' : 'No'
} else if (fieldType === 'number' && typeof raw === 'number') {
value = raw.toLocaleString()
} else {
value = String(raw)
}
if (value) {
result.push({ slot, displayName: def?.displayName || slot, value })
}
}
return result
}
export function KnowledgeBase({
id,
knowledgeBaseName: passedKnowledgeBaseName,
@@ -379,6 +453,8 @@ export function KnowledgeBase({
sortOrder,
})
const { tagDefinitions } = useKnowledgeBaseTagDefinitions(id)
const router = useRouter()
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
@@ -1058,12 +1134,15 @@ export function KnowledgeBase({
</div>
</TableHead>
{renderSortableHeader('filename', 'Name', 'w-[180px] max-w-[180px]')}
{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('fileSize', 'Size', 'hidden w-[8%] lg:table-cell')}
{renderSortableHeader('tokenCount', 'Tokens', 'hidden w-[8%] lg:table-cell')}
{renderSortableHeader('chunkCount', 'Chunks', 'w-[8%]')}
{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>
@@ -1071,7 +1150,6 @@ export function KnowledgeBase({
<TableBody>
{documents.map((doc) => {
const isSelected = selectedDocuments.has(doc.id)
const statusDisplay = getStatusDisplay(doc)
return (
<TableRow
@@ -1115,10 +1193,10 @@ export function KnowledgeBase({
</Tooltip.Root>
</div>
</TableCell>
<TableCell className='px-[12px] py-[8px] text-[12px] text-[var(--text-muted)]'>
<TableCell className='hidden px-[12px] py-[8px] text-[12px] text-[var(--text-muted)] lg:table-cell'>
{formatFileSize(doc.fileSize)}
</TableCell>
<TableCell className='px-[12px] py-[8px] text-[12px]'>
<TableCell className='hidden px-[12px] py-[8px] text-[12px] lg:table-cell'>
{doc.processingStatus === 'completed' ? (
doc.tokenCount > 1000 ? (
`${(doc.tokenCount / 1000).toFixed(1)}k`
@@ -1129,43 +1207,73 @@ export function KnowledgeBase({
<span className='text-[var(--text-muted)]'></span>
)}
</TableCell>
<TableCell className='hidden px-[12px] py-[8px] text-[12px] text-[var(--text-muted)] lg:table-cell'>
<TableCell className='px-[12px] py-[8px] text-[12px] text-[var(--text-muted)]'>
{doc.processingStatus === 'completed'
? doc.chunkCount.toLocaleString()
: '—'}
</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 ? (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div className={statusDisplay.className} style={{ cursor: 'help' }}>
{statusDisplay.text}
</div>
<div style={{ cursor: 'help' }}>{getStatusBadge(doc)}</div>
</Tooltip.Trigger>
<Tooltip.Content side='top' className='max-w-xs'>
{doc.processingError}
</Tooltip.Content>
</Tooltip.Root>
) : (
<div className={statusDisplay.className}>{statusDisplay.text}</div>
getStatusBadge(doc)
)}
</TableCell>
<TableCell className='px-[12px] py-[8px]'>
{(() => {
const tags = getDocumentTags(doc, tagDefinitions)
if (tags.length === 0) {
return <span className='text-[12px] text-[var(--text-muted)]'></span>
}
const displayText = tags.map((t) => t.value).join(', ')
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span
className='block max-w-full truncate text-[12px] text-[var(--text-secondary)]'
onClick={(e) => e.stopPropagation()}
>
{displayText}
</span>
</Tooltip.Trigger>
<Tooltip.Content
side='top'
className='max-h-[104px] max-w-[240px] overflow-y-auto'
>
<div className='flex flex-col gap-[2px]'>
{tags.map((tag) => (
<div key={tag.slot} className='text-[11px]'>
<span className='text-[var(--text-muted)]'>
{tag.displayName}:
</span>{' '}
{tag.value}
</div>
))}
</div>
</Tooltip.Content>
</Tooltip.Root>
)
})()}
</TableCell>
<TableCell className='py-[8px] pr-[4px] pl-[12px]'>
<div className='flex items-center gap-[4px]'>
{doc.processingStatus === 'failed' && (