Skip to content

Commit e6c7bd3

Browse files
committed
feat(kb): added tags information to kb docs table
1 parent b7f6bab commit e6c7bd3

File tree

3 files changed

+199
-27
lines changed

3 files changed

+199
-27
lines changed

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

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
ActionBar,
4646
AddDocumentsModal,
4747
BaseTagsModal,
48+
DocumentTagsCell,
4849
} from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
4950
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
5051
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -53,6 +54,7 @@ import {
5354
useKnowledgeBaseDocuments,
5455
useKnowledgeBasesList,
5556
} from '@/hooks/use-knowledge'
57+
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
5658
import type { DocumentData } from '@/stores/knowledge/store'
5759

5860
const logger = createLogger('KnowledgeBase')
@@ -83,18 +85,17 @@ function DocumentTableRowSkeleton() {
8385
<Skeleton className='h-[15px] w-[24px]' />
8486
</TableCell>
8587
<TableCell className='px-[12px] py-[8px]'>
86-
<div className='flex flex-col justify-center'>
87-
<div className='flex items-center font-medium text-[12px]'>
88-
<Skeleton className='h-[15px] w-[50px]' />
89-
<span className='mx-[6px] hidden text-[var(--text-muted)] xl:inline'>|</span>
90-
<Skeleton className='hidden h-[15px] w-[70px] xl:inline-block' />
91-
</div>
92-
<Skeleton className='mt-[2px] h-[15px] w-[40px] lg:hidden' />
93-
</div>
88+
<Skeleton className='h-[15px] w-[60px]' />
9489
</TableCell>
9590
<TableCell className='px-[12px] py-[8px]'>
9691
<Skeleton className='h-[24px] w-[64px] rounded-md' />
9792
</TableCell>
93+
<TableCell className='px-[12px] py-[8px]'>
94+
<div className='flex items-center gap-[4px]'>
95+
<Skeleton className='h-[18px] w-[40px] rounded-full' />
96+
<Skeleton className='h-[18px] w-[40px] rounded-full' />
97+
</div>
98+
</TableCell>
9899
<TableCell className='py-[8px] pr-[4px] pl-[12px]'>
99100
<div className='flex items-center gap-[4px]'>
100101
<Skeleton className='h-[28px] w-[28px] rounded-[4px]' />
@@ -127,13 +128,16 @@ function DocumentTableSkeleton({ rowCount = 5 }: { rowCount?: number }) {
127128
<TableHead className='hidden w-[8%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)] lg:table-cell'>
128129
Chunks
129130
</TableHead>
130-
<TableHead className='w-[16%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
131+
<TableHead className='w-[11%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
131132
Uploaded
132133
</TableHead>
133-
<TableHead className='w-[12%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
134+
<TableHead className='w-[10%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
134135
Status
135136
</TableHead>
136-
<TableHead className='w-[14%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
137+
<TableHead className='w-[12%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
138+
Tags
139+
</TableHead>
140+
<TableHead className='w-[11%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
137141
Actions
138142
</TableHead>
139143
</TableRow>
@@ -379,6 +383,8 @@ export function KnowledgeBase({
379383
sortOrder,
380384
})
381385

386+
const { tagDefinitions } = useKnowledgeBaseTagDefinitions(id)
387+
382388
const router = useRouter()
383389

384390
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
@@ -1061,9 +1067,12 @@ export function KnowledgeBase({
10611067
{renderSortableHeader('fileSize', 'Size', 'w-[8%]')}
10621068
{renderSortableHeader('tokenCount', 'Tokens', 'w-[8%]')}
10631069
{renderSortableHeader('chunkCount', 'Chunks', 'hidden w-[8%] lg:table-cell')}
1064-
{renderSortableHeader('uploadedAt', 'Uploaded', 'w-[16%]')}
1065-
{renderSortableHeader('processingStatus', 'Status', 'w-[12%]')}
1066-
<TableHead className='w-[14%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
1070+
{renderSortableHeader('uploadedAt', 'Uploaded', 'w-[11%]')}
1071+
{renderSortableHeader('processingStatus', 'Status', 'w-[10%]')}
1072+
<TableHead className='w-[12%] px-[12px] py-[8px] text-[12px] text-[var(--text-secondary)]'>
1073+
Tags
1074+
</TableHead>
1075+
<TableHead className='w-[11%] py-[8px] pr-[4px] pl-[12px] text-[12px] text-[var(--text-secondary)]'>
10671076
Actions
10681077
</TableHead>
10691078
</TableRow>
@@ -1135,20 +1144,16 @@ export function KnowledgeBase({
11351144
: '—'}
11361145
</TableCell>
11371146
<TableCell className='px-[12px] py-[8px]'>
1138-
<div className='flex flex-col justify-center'>
1139-
<div className='flex items-center font-medium text-[12px]'>
1140-
<span>{format(new Date(doc.uploadedAt), 'h:mm a')}</span>
1141-
<span className='mx-[6px] hidden text-[var(--text-muted)] xl:inline'>
1142-
|
1143-
</span>
1144-
<span className='hidden text-[var(--text-muted)] xl:inline'>
1145-
{format(new Date(doc.uploadedAt), 'MMM d, yyyy')}
1147+
<Tooltip.Root>
1148+
<Tooltip.Trigger asChild>
1149+
<span className='text-[12px] text-[var(--text-muted)]'>
1150+
{format(new Date(doc.uploadedAt), 'MMM d')}
11461151
</span>
1147-
</div>
1148-
<div className='mt-[2px] text-[12px] text-[var(--text-muted)] lg:hidden'>
1149-
{format(new Date(doc.uploadedAt), 'MMM d')}
1150-
</div>
1151-
</div>
1152+
</Tooltip.Trigger>
1153+
<Tooltip.Content side='top'>
1154+
{format(new Date(doc.uploadedAt), 'MMM d, yyyy h:mm a')}
1155+
</Tooltip.Content>
1156+
</Tooltip.Root>
11521157
</TableCell>
11531158
<TableCell className='px-[12px] py-[8px]'>
11541159
{doc.processingStatus === 'failed' && doc.processingError ? (
@@ -1166,6 +1171,9 @@ export function KnowledgeBase({
11661171
<div className={statusDisplay.className}>{statusDisplay.text}</div>
11671172
)}
11681173
</TableCell>
1174+
<TableCell className='px-[12px] py-[8px]'>
1175+
<DocumentTagsCell document={doc} tagDefinitions={tagDefinitions} />
1176+
</TableCell>
11691177
<TableCell className='py-[8px] pr-[4px] pl-[12px]'>
11701178
<div className='flex items-center gap-[4px]'>
11711179
{doc.processingStatus === 'failed' && (
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
'use client'
2+
3+
import { useMemo } from 'react'
4+
import { format } from 'date-fns'
5+
import { Badge, Popover, PopoverAnchor, PopoverContent, Tooltip } from '@/components/emcn'
6+
import type { TagDefinition } from '@/hooks/use-knowledge-base-tag-definitions'
7+
import type { DocumentData } from '@/stores/knowledge/store'
8+
9+
/** All tag slot keys that can hold values */
10+
const TAG_SLOTS = [
11+
'tag1',
12+
'tag2',
13+
'tag3',
14+
'tag4',
15+
'tag5',
16+
'tag6',
17+
'tag7',
18+
'number1',
19+
'number2',
20+
'number3',
21+
'number4',
22+
'number5',
23+
'date1',
24+
'date2',
25+
'boolean1',
26+
'boolean2',
27+
'boolean3',
28+
] as const
29+
30+
type TagSlot = (typeof TAG_SLOTS)[number]
31+
32+
interface TagValue {
33+
slot: TagSlot
34+
displayName: string
35+
value: string
36+
fieldType: string
37+
}
38+
39+
interface DocumentTagsCellProps {
40+
document: DocumentData
41+
tagDefinitions: TagDefinition[]
42+
}
43+
44+
/**
45+
* Formats a tag value based on its field type
46+
*/
47+
function formatTagValue(value: unknown, fieldType: string): string {
48+
if (value === null || value === undefined) return ''
49+
50+
switch (fieldType) {
51+
case 'date':
52+
try {
53+
return format(new Date(value as string), 'MMM d, yyyy')
54+
} catch {
55+
return String(value)
56+
}
57+
case 'boolean':
58+
return value ? 'Yes' : 'No'
59+
case 'number':
60+
return typeof value === 'number' ? value.toLocaleString() : String(value)
61+
default:
62+
return String(value)
63+
}
64+
}
65+
66+
/**
67+
* Gets the field type for a tag slot
68+
*/
69+
function getFieldType(slot: TagSlot): string {
70+
if (slot.startsWith('tag')) return 'text'
71+
if (slot.startsWith('number')) return 'number'
72+
if (slot.startsWith('date')) return 'date'
73+
if (slot.startsWith('boolean')) return 'boolean'
74+
return 'text'
75+
}
76+
77+
/**
78+
* Cell component that displays document tags as compact badges with overflow popover
79+
*/
80+
export function DocumentTagsCell({ document, tagDefinitions }: DocumentTagsCellProps) {
81+
const tags = useMemo(() => {
82+
const result: TagValue[] = []
83+
84+
for (const slot of TAG_SLOTS) {
85+
const value = document[slot]
86+
if (value === null || value === undefined) continue
87+
88+
const definition = tagDefinitions.find((def) => def.tagSlot === slot)
89+
const fieldType = definition?.fieldType || getFieldType(slot)
90+
const formattedValue = formatTagValue(value, fieldType)
91+
92+
if (!formattedValue) continue
93+
94+
result.push({
95+
slot,
96+
displayName: definition?.displayName || slot,
97+
value: formattedValue,
98+
fieldType,
99+
})
100+
}
101+
102+
return result
103+
}, [document, tagDefinitions])
104+
105+
if (tags.length === 0) {
106+
return <span className='text-[11px] text-[var(--text-muted)]'></span>
107+
}
108+
109+
const visibleTags = tags.slice(0, 2)
110+
const overflowTags = tags.slice(2)
111+
const hasOverflow = overflowTags.length > 0
112+
113+
return (
114+
<div className='flex items-center gap-[4px]' onClick={(e) => e.stopPropagation()}>
115+
{visibleTags.map((tag) => (
116+
<Tooltip.Root key={tag.slot}>
117+
<Tooltip.Trigger asChild>
118+
<Badge className='max-w-[80px] truncate px-[6px] py-[1px] text-[10px]'>
119+
{tag.value}
120+
</Badge>
121+
</Tooltip.Trigger>
122+
<Tooltip.Content side='top'>
123+
{tag.displayName}: {tag.value}
124+
</Tooltip.Content>
125+
</Tooltip.Root>
126+
))}
127+
{hasOverflow && (
128+
<Popover>
129+
<Tooltip.Root>
130+
<Tooltip.Trigger asChild>
131+
<PopoverAnchor asChild>
132+
<Badge
133+
variant='outline'
134+
className='cursor-pointer px-[6px] py-[1px] text-[10px] hover:bg-[var(--surface-6)]'
135+
>
136+
+{overflowTags.length}
137+
</Badge>
138+
</PopoverAnchor>
139+
</Tooltip.Trigger>
140+
<Tooltip.Content side='top'>
141+
{overflowTags.map((tag) => tag.displayName).join(', ')}
142+
</Tooltip.Content>
143+
</Tooltip.Root>
144+
<PopoverContent side='bottom' align='start' maxWidth={220} minWidth={160}>
145+
<div className='flex flex-col gap-[2px]'>
146+
{tags.map((tag) => (
147+
<div
148+
key={tag.slot}
149+
className='flex items-center justify-between gap-[8px] rounded-[4px] px-[6px] py-[4px] text-[11px]'
150+
>
151+
<span className='text-[var(--text-muted)]'>{tag.displayName}</span>
152+
<span className='max-w-[100px] truncate text-[var(--text-primary)]'>
153+
{tag.value}
154+
</span>
155+
</div>
156+
))}
157+
</div>
158+
</PopoverContent>
159+
</Popover>
160+
)}
161+
</div>
162+
)
163+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { ActionBar } from './action-bar/action-bar'
22
export { AddDocumentsModal } from './add-documents-modal/add-documents-modal'
33
export { BaseTagsModal } from './base-tags-modal/base-tags-modal'
4+
export { DocumentTagsCell } from './document-tags-cell/document-tags-cell'

0 commit comments

Comments
 (0)