From 601029f102c1dc3d160ca74d463f76c42c15052b Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 26 Dec 2025 02:06:50 -0800 Subject: [PATCH 1/2] feat(kb): added tags information to kb docs table --- .../[workspaceId]/knowledge/[id]/base.tsx | 62 ++++--- .../document-tags-cell/document-tags-cell.tsx | 163 ++++++++++++++++++ .../knowledge/[id]/components/index.ts | 1 + 3 files changed, 199 insertions(+), 27 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 3949f1ee02..da3836c40e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -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() { -
-
- - | - -
- -
+
+ +
+ + +
+
@@ -127,13 +128,16 @@ function DocumentTableSkeleton({ rowCount = 5 }: { rowCount?: number }) { Chunks - + Uploaded - + Status - + + Tags + + Actions @@ -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%]')} - + {renderSortableHeader('uploadedAt', 'Uploaded', 'w-[11%]')} + {renderSortableHeader('processingStatus', 'Status', 'w-[10%]')} + + Tags + + Actions @@ -1135,20 +1144,16 @@ export function KnowledgeBase({ : '—'} -
-
- {format(new Date(doc.uploadedAt), 'h:mm a')} - - | - - - {format(new Date(doc.uploadedAt), 'MMM d, yyyy')} + + + + {format(new Date(doc.uploadedAt), 'MMM d')} -
-
- {format(new Date(doc.uploadedAt), 'MMM d')} -
-
+ + + {format(new Date(doc.uploadedAt), 'MMM d, yyyy h:mm a')} + +
{doc.processingStatus === 'failed' && doc.processingError ? ( @@ -1166,6 +1171,9 @@ export function KnowledgeBase({
{statusDisplay.text}
)}
+ + +
{doc.processingStatus === 'failed' && ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx new file mode 100644 index 0000000000..45dad2c1e8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx @@ -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 + } + + const visibleTags = tags.slice(0, 2) + const overflowTags = tags.slice(2) + const hasOverflow = overflowTags.length > 0 + + return ( +
e.stopPropagation()}> + {visibleTags.map((tag) => ( + + + + {tag.value} + + + + {tag.displayName}: {tag.value} + + + ))} + {hasOverflow && ( + + + + + + +{overflowTags.length} + + + + + {overflowTags.map((tag) => tag.displayName).join(', ')} + + + +
+ {tags.map((tag) => ( +
+ {tag.displayName} + + {tag.value} + +
+ ))} +
+
+
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts index 9f2f554128..f47ca8fc29 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts @@ -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' From 6b332eed1c5391f6894db0d721af49613ea85adc Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Fri, 26 Dec 2025 13:46:54 -0800 Subject: [PATCH 2/2] improvement(base): table and tags styling --- .../[workspaceId]/knowledge/[id]/base.tsx | 224 +++++++++++++----- .../document-tags-cell/document-tags-cell.tsx | 163 ------------- .../knowledge/[id]/components/index.ts | 1 - 3 files changed, 162 insertions(+), 226 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index da3836c40e..8274a9dd3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -18,6 +18,7 @@ import { } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { + Badge, Breadcrumb, Button, Modal, @@ -40,12 +41,12 @@ 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, 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' @@ -54,7 +55,10 @@ import { useKnowledgeBaseDocuments, useKnowledgeBasesList, } from '@/hooks/use-knowledge' -import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' +import { + type TagDefinition, + useKnowledgeBaseTagDefinitions, +} from '@/hooks/use-knowledge-base-tag-definitions' import type { DocumentData } from '@/stores/knowledge/store' const logger = createLogger('KnowledgeBase') @@ -75,13 +79,13 @@ function DocumentTableRowSkeleton() {
- + - + - + @@ -119,13 +123,13 @@ function DocumentTableSkeleton({ rowCount = 5 }: { rowCount?: number }) { Name - + Size - + Tokens - + Chunks @@ -278,56 +282,122 @@ 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 }) => ( + +) + +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 ( + + Pending + + ) case 'processing': - return { - text: ( - <> - - 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 ( + + Processing + + ) case 'failed': - return { - text: ( - <> - Failed - {doc.processingError && } - - ), - 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 ? ( + + Failed + + ) : ( + + Failed + + ) 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 ? ( + + Enabled + + ) : ( + + Disabled + + ) 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 ( + + Unknown + + ) + } +} + +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 = { + 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({ @@ -1064,9 +1134,9 @@ export function KnowledgeBase({
{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('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%]')} @@ -1080,7 +1150,6 @@ export function KnowledgeBase({ {documents.map((doc) => { const isSelected = selectedDocuments.has(doc.id) - const statusDisplay = getStatusDisplay(doc) return (
- + {formatFileSize(doc.fileSize)} - + {doc.processingStatus === 'completed' ? ( doc.tokenCount > 1000 ? ( `${(doc.tokenCount / 1000).toFixed(1)}k` @@ -1138,7 +1207,7 @@ export function KnowledgeBase({ )} - + {doc.processingStatus === 'completed' ? doc.chunkCount.toLocaleString() : '—'} @@ -1159,20 +1228,51 @@ export function KnowledgeBase({ {doc.processingStatus === 'failed' && doc.processingError ? ( -
- {statusDisplay.text} -
+
{getStatusBadge(doc)}
{doc.processingError}
) : ( -
{statusDisplay.text}
+ getStatusBadge(doc) )}
- + {(() => { + const tags = getDocumentTags(doc, tagDefinitions) + if (tags.length === 0) { + return + } + const displayText = tags.map((t) => t.value).join(', ') + return ( + + + e.stopPropagation()} + > + {displayText} + + + +
+ {tags.map((tag) => ( +
+ + {tag.displayName}: + {' '} + {tag.value} +
+ ))} +
+
+
+ ) + })()}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx deleted file mode 100644 index 45dad2c1e8..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-tags-cell/document-tags-cell.tsx +++ /dev/null @@ -1,163 +0,0 @@ -'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 - } - - const visibleTags = tags.slice(0, 2) - const overflowTags = tags.slice(2) - const hasOverflow = overflowTags.length > 0 - - return ( -
e.stopPropagation()}> - {visibleTags.map((tag) => ( - - - - {tag.value} - - - - {tag.displayName}: {tag.value} - - - ))} - {hasOverflow && ( - - - - - - +{overflowTags.length} - - - - - {overflowTags.map((tag) => tag.displayName).join(', ')} - - - -
- {tags.map((tag) => ( -
- {tag.displayName} - - {tag.value} - -
- ))} -
-
-
- )} -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts index f47ca8fc29..9f2f554128 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts @@ -1,4 +1,3 @@ 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'