diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 3949f1ee02..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,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() { - + - + - + - - - - | - - - - + + + + + + + @@ -118,22 +123,25 @@ function DocumentTableSkeleton({ rowCount = 5 }: { rowCount?: number }) { Name - + Size - + Tokens - + Chunks - + Uploaded - + Status - + + Tags + + Actions @@ -274,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({ @@ -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({ {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%]')} - + {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%]')} + + Tags + + Actions @@ -1071,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` @@ -1129,43 +1207,73 @@ export function KnowledgeBase({ — )} - + {doc.processingStatus === 'completed' ? doc.chunkCount.toLocaleString() : '—'} - - - {format(new Date(doc.uploadedAt), 'h:mm a')} - - | + + + + {format(new Date(doc.uploadedAt), 'MMM d')} - - {format(new Date(doc.uploadedAt), 'MMM d, yyyy')} - - - - {format(new Date(doc.uploadedAt), 'MMM d')} - - + + + {format(new Date(doc.uploadedAt), 'MMM d, yyyy h:mm a')} + + {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} + + ))} + + + + ) + })()} + {doc.processingStatus === 'failed' && (