diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index e15fa0220..f3d576b3e 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -5,6 +5,7 @@ import { z } from 'zod' import { getSession } from '@/lib/auth' import { bulkDocumentOperation, + bulkDocumentOperationByFilter, createDocumentRecords, createSingleDocument, getDocuments, @@ -57,13 +58,20 @@ const BulkCreateDocumentsSchema = z.object({ bulk: z.literal(true), }) -const BulkUpdateDocumentsSchema = z.object({ - operation: z.enum(['enable', 'disable', 'delete']), - documentIds: z - .array(z.string()) - .min(1, 'At least one document ID is required') - .max(100, 'Cannot operate on more than 100 documents at once'), -}) +const BulkUpdateDocumentsSchema = z + .object({ + operation: z.enum(['enable', 'disable', 'delete']), + documentIds: z + .array(z.string()) + .min(1, 'At least one document ID is required') + .max(100, 'Cannot operate on more than 100 documents at once') + .optional(), + selectAll: z.boolean().optional(), + enabledFilter: z.enum(['all', 'enabled', 'disabled']).optional(), + }) + .refine((data) => data.selectAll || (data.documentIds && data.documentIds.length > 0), { + message: 'Either selectAll must be true or documentIds must be provided', + }) export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { const requestId = randomUUID().slice(0, 8) @@ -90,14 +98,17 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: } const url = new URL(req.url) - const includeDisabled = url.searchParams.get('includeDisabled') === 'true' + const enabledFilter = url.searchParams.get('enabledFilter') as + | 'all' + | 'enabled' + | 'disabled' + | null const search = url.searchParams.get('search') || undefined const limit = Number.parseInt(url.searchParams.get('limit') || '50') const offset = Number.parseInt(url.searchParams.get('offset') || '0') const sortByParam = url.searchParams.get('sortBy') const sortOrderParam = url.searchParams.get('sortOrder') - // Validate sort parameters const validSortFields: DocumentSortField[] = [ 'filename', 'fileSize', @@ -105,6 +116,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: 'chunkCount', 'uploadedAt', 'processingStatus', + 'enabled', ] const validSortOrders: SortOrder[] = ['asc', 'desc'] @@ -120,7 +132,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: const result = await getDocuments( knowledgeBaseId, { - includeDisabled, + enabledFilter: enabledFilter || undefined, search, limit, offset, @@ -190,8 +202,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const createdDocuments = await createDocumentRecords( validatedData.documents, knowledgeBaseId, - requestId, - userId + requestId ) logger.info( @@ -250,16 +261,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: throw validationError } } else { - // Handle single document creation try { const validatedData = CreateDocumentSchema.parse(body) - const newDocument = await createSingleDocument( - validatedData, - knowledgeBaseId, - requestId, - userId - ) + const newDocument = await createSingleDocument(validatedData, knowledgeBaseId, requestId) try { const { PlatformEvents } = await import('@/lib/core/telemetry') @@ -294,7 +299,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: } catch (error) { logger.error(`[${requestId}] Error creating document`, error) - // Check if it's a storage limit error const errorMessage = error instanceof Error ? error.message : 'Failed to create document' const isStorageLimitError = errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit') @@ -331,16 +335,20 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id try { const validatedData = BulkUpdateDocumentsSchema.parse(body) - const { operation, documentIds } = validatedData + const { operation, documentIds, selectAll, enabledFilter } = validatedData try { - const result = await bulkDocumentOperation( - knowledgeBaseId, - operation, - documentIds, - requestId, - session.user.id - ) + let result + if (selectAll) { + result = await bulkDocumentOperationByFilter( + knowledgeBaseId, + operation, + enabledFilter, + requestId + ) + } else { + result = await bulkDocumentOperation(knowledgeBaseId, operation, documentIds!, requestId) + } return NextResponse.json({ success: true, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx index 9148ca5c7..e1faef399 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx @@ -61,6 +61,7 @@ export function EditChunkModal({ const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false) const [pendingNavigation, setPendingNavigation] = useState<(() => void) | null>(null) const [tokenizerOn, setTokenizerOn] = useState(false) + const [hoveredTokenIndex, setHoveredTokenIndex] = useState(null) const textareaRef = useRef(null) const error = mutationError?.message ?? null @@ -254,6 +255,8 @@ export function EditChunkModal({ style={{ backgroundColor: getTokenBgColor(index), }} + onMouseEnter={() => setHoveredTokenIndex(index)} + onMouseLeave={() => setHoveredTokenIndex(null)} > {token} @@ -281,6 +284,11 @@ export function EditChunkModal({
Tokenizer + {tokenizerOn && hoveredTokenIndex !== null && ( + + Token #{hoveredTokenIndex + 1} + + )}
{tokenCount.toLocaleString()} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index f32b37fd0..ad4750e85 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -36,6 +36,7 @@ import { import { Input } from '@/components/ui/input' import { SearchHighlight } from '@/components/ui/search-highlight' import { Skeleton } from '@/components/ui/skeleton' +import { formatAbsoluteDate, formatRelativeTime } from '@/lib/core/utils/formatting' import type { ChunkData } from '@/lib/knowledge/types' import { ChunkContextMenu, @@ -58,55 +59,6 @@ import { const logger = createLogger('Document') -/** - * Formats a date string to relative time (e.g., "2h ago", "3d ago") - */ -function formatRelativeTime(dateString: string): string { - const date = new Date(dateString) - const now = new Date() - const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000) - - if (diffInSeconds < 60) { - return 'just now' - } - if (diffInSeconds < 3600) { - const minutes = Math.floor(diffInSeconds / 60) - return `${minutes}m ago` - } - if (diffInSeconds < 86400) { - const hours = Math.floor(diffInSeconds / 3600) - return `${hours}h ago` - } - if (diffInSeconds < 604800) { - const days = Math.floor(diffInSeconds / 86400) - return `${days}d ago` - } - if (diffInSeconds < 2592000) { - const weeks = Math.floor(diffInSeconds / 604800) - return `${weeks}w ago` - } - if (diffInSeconds < 31536000) { - const months = Math.floor(diffInSeconds / 2592000) - return `${months}mo ago` - } - const years = Math.floor(diffInSeconds / 31536000) - return `${years}y ago` -} - -/** - * Formats a date string to absolute format for tooltip display - */ -function formatAbsoluteDate(dateString: string): string { - const date = new Date(dateString) - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) -} - interface DocumentProps { knowledgeBaseId: string documentId: string @@ -304,7 +256,6 @@ export function Document({ const [searchQuery, setSearchQuery] = useState('') const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') - const [isSearching, setIsSearching] = useState(false) const { chunks: initialChunks, @@ -344,7 +295,6 @@ export function Document({ const handler = setTimeout(() => { startTransition(() => { setDebouncedSearchQuery(searchQuery) - setIsSearching(searchQuery.trim().length > 0) }) }, 200) @@ -353,6 +303,7 @@ export function Document({ } }, [searchQuery]) + const isSearching = debouncedSearchQuery.trim().length > 0 const showingSearch = isSearching && searchQuery.trim().length > 0 && searchResults.length > 0 const SEARCH_PAGE_SIZE = 50 const maxSearchPages = Math.ceil(searchResults.length / SEARCH_PAGE_SIZE) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 81d30f53d..15d1d36d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -27,6 +27,10 @@ import { ModalContent, ModalFooter, ModalHeader, + Popover, + PopoverContent, + PopoverItem, + PopoverTrigger, Table, TableBody, TableCell, @@ -40,8 +44,11 @@ import { Input } from '@/components/ui/input' import { SearchHighlight } from '@/components/ui/search-highlight' import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/core/utils/cn' +import { formatAbsoluteDate, formatRelativeTime } from '@/lib/core/utils/formatting' +import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants' import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' import type { DocumentData } from '@/lib/knowledge/types' +import { formatFileSize } from '@/lib/uploads/utils/file-utils' import { ActionBar, AddDocumentsModal, @@ -189,8 +196,8 @@ function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps) -
- +
+
@@ -208,9 +215,12 @@ function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps) className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0' />
- +
+ + +
@@ -222,73 +232,11 @@ function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps) ) } -/** - * Formats a date string to relative time (e.g., "2h ago", "3d ago") - */ -function formatRelativeTime(dateString: string): string { - const date = new Date(dateString) - const now = new Date() - const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000) - - if (diffInSeconds < 60) { - return 'just now' - } - if (diffInSeconds < 3600) { - const minutes = Math.floor(diffInSeconds / 60) - return `${minutes}m ago` - } - if (diffInSeconds < 86400) { - const hours = Math.floor(diffInSeconds / 3600) - return `${hours}h ago` - } - if (diffInSeconds < 604800) { - const days = Math.floor(diffInSeconds / 86400) - return `${days}d ago` - } - if (diffInSeconds < 2592000) { - const weeks = Math.floor(diffInSeconds / 604800) - return `${weeks}w ago` - } - if (diffInSeconds < 31536000) { - const months = Math.floor(diffInSeconds / 2592000) - return `${months}mo ago` - } - const years = Math.floor(diffInSeconds / 31536000) - return `${years}y ago` -} - -/** - * Formats a date string to absolute format for tooltip display - */ -function formatAbsoluteDate(dateString: string): string { - const date = new Date(dateString) - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) -} - interface KnowledgeBaseProps { id: string knowledgeBaseName?: string } -function getFileIcon(mimeType: string, filename: string) { - const IconComponent = getDocumentIcon(mimeType, filename) - return -} - -function formatFileSize(bytes: number): string { - if (bytes === 0) return '0 Bytes' - const k = 1024 - const sizes = ['Bytes', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}` -} - const AnimatedLoader = ({ className }: { className?: string }) => ( ) @@ -336,53 +284,24 @@ const getStatusBadge = (doc: DocumentData) => { } } -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 + slot: AllTagSlot 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) { + for (const slot of ALL_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' + const fieldType = def?.fieldType || getFieldTypeForSlot(slot) || 'text' let value: string if (fieldType === 'date') { @@ -424,6 +343,8 @@ export function KnowledgeBase({ const [searchQuery, setSearchQuery] = useState('') const [showTagsModal, setShowTagsModal] = useState(false) + const [enabledFilter, setEnabledFilter] = useState<'all' | 'enabled' | 'disabled'>('all') + const [isFilterPopoverOpen, setIsFilterPopoverOpen] = useState(false) /** * Memoize the search query setter to prevent unnecessary re-renders @@ -434,6 +355,7 @@ export function KnowledgeBase({ }, []) const [selectedDocuments, setSelectedDocuments] = useState>(new Set()) + const [isSelectAllMode, setIsSelectAllMode] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [showAddDocumentsModal, setShowAddDocumentsModal] = useState(false) const [showDeleteDocumentModal, setShowDeleteDocumentModal] = useState(false) @@ -460,7 +382,6 @@ export function KnowledgeBase({ error: knowledgeBaseError, refresh: refreshKnowledgeBase, } = useKnowledgeBase(id) - const [hasProcessingDocuments, setHasProcessingDocuments] = useState(false) const { documents, @@ -469,6 +390,7 @@ export function KnowledgeBase({ isFetching: isFetchingDocuments, isPlaceholderData: isPlaceholderDocuments, error: documentsError, + hasProcessingDocuments, updateDocument, refreshDocuments, } = useKnowledgeBaseDocuments(id, { @@ -477,7 +399,14 @@ export function KnowledgeBase({ offset: (currentPage - 1) * DOCUMENTS_PER_PAGE, sortBy, sortOrder, - refetchInterval: hasProcessingDocuments && !isDeleting ? 3000 : false, + refetchInterval: (data) => { + if (isDeleting) return false + const hasPending = data?.documents?.some( + (doc) => doc.processingStatus === 'pending' || doc.processingStatus === 'processing' + ) + return hasPending ? 3000 : false + }, + enabledFilter, }) const { tagDefinitions } = useKnowledgeBaseTagDefinitions(id) @@ -543,52 +472,52 @@ export function KnowledgeBase({ ) - useEffect(() => { - const processing = documents.some( - (doc) => doc.processingStatus === 'pending' || doc.processingStatus === 'processing' - ) - setHasProcessingDocuments(processing) - - if (processing) { - checkForDeadProcesses() - } - }, [documents]) - /** * Checks for documents with stale processing states and marks them as failed */ - const checkForDeadProcesses = () => { - const now = new Date() - const DEAD_PROCESS_THRESHOLD_MS = 600 * 1000 // 10 minutes + const checkForDeadProcesses = useCallback( + (docsToCheck: DocumentData[]) => { + const now = new Date() + const DEAD_PROCESS_THRESHOLD_MS = 600 * 1000 // 10 minutes - const staleDocuments = documents.filter((doc) => { - if (doc.processingStatus !== 'processing' || !doc.processingStartedAt) { - return false - } - - const processingDuration = now.getTime() - new Date(doc.processingStartedAt).getTime() - return processingDuration > DEAD_PROCESS_THRESHOLD_MS - }) - - if (staleDocuments.length === 0) return - - logger.warn(`Found ${staleDocuments.length} documents with dead processes`) - - staleDocuments.forEach((doc) => { - updateDocumentMutation( - { - knowledgeBaseId: id, - documentId: doc.id, - updates: { markFailedDueToTimeout: true }, - }, - { - onSuccess: () => { - logger.info(`Successfully marked dead process as failed for document: ${doc.filename}`) - }, + const staleDocuments = docsToCheck.filter((doc) => { + if (doc.processingStatus !== 'processing' || !doc.processingStartedAt) { + return false } - ) - }) - } + + const processingDuration = now.getTime() - new Date(doc.processingStartedAt).getTime() + return processingDuration > DEAD_PROCESS_THRESHOLD_MS + }) + + if (staleDocuments.length === 0) return + + logger.warn(`Found ${staleDocuments.length} documents with dead processes`) + + staleDocuments.forEach((doc) => { + updateDocumentMutation( + { + knowledgeBaseId: id, + documentId: doc.id, + updates: { markFailedDueToTimeout: true }, + }, + { + onSuccess: () => { + logger.info( + `Successfully marked dead process as failed for document: ${doc.filename}` + ) + }, + } + ) + }) + }, + [id, updateDocumentMutation] + ) + + useEffect(() => { + if (hasProcessingDocuments) { + checkForDeadProcesses(documents) + } + }, [hasProcessingDocuments, documents, checkForDeadProcesses]) const handleToggleEnabled = (docId: string) => { const document = documents.find((doc) => doc.id === docId) @@ -748,6 +677,7 @@ export function KnowledgeBase({ setSelectedDocuments(new Set(documents.map((doc) => doc.id))) } else { setSelectedDocuments(new Set()) + setIsSelectAllMode(false) } } @@ -793,6 +723,26 @@ export function KnowledgeBase({ * Handles bulk enabling of selected documents */ const handleBulkEnable = () => { + if (isSelectAllMode) { + bulkDocumentMutation( + { + knowledgeBaseId: id, + operation: 'enable', + selectAll: true, + enabledFilter, + }, + { + onSuccess: (result) => { + logger.info(`Successfully enabled ${result.successCount} documents`) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + refreshDocuments() + }, + } + ) + return + } + const documentsToEnable = documents.filter( (doc) => selectedDocuments.has(doc.id) && !doc.enabled ) @@ -821,6 +771,26 @@ export function KnowledgeBase({ * Handles bulk disabling of selected documents */ const handleBulkDisable = () => { + if (isSelectAllMode) { + bulkDocumentMutation( + { + knowledgeBaseId: id, + operation: 'disable', + selectAll: true, + enabledFilter, + }, + { + onSuccess: (result) => { + logger.info(`Successfully disabled ${result.successCount} documents`) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + refreshDocuments() + }, + } + ) + return + } + const documentsToDisable = documents.filter( (doc) => selectedDocuments.has(doc.id) && doc.enabled ) @@ -845,18 +815,35 @@ export function KnowledgeBase({ ) } - /** - * Opens the bulk delete confirmation modal - */ const handleBulkDelete = () => { if (selectedDocuments.size === 0) return setShowBulkDeleteModal(true) } - /** - * Confirms and executes the bulk deletion of selected documents - */ const confirmBulkDelete = () => { + if (isSelectAllMode) { + bulkDocumentMutation( + { + knowledgeBaseId: id, + operation: 'delete', + selectAll: true, + enabledFilter, + }, + { + onSuccess: (result) => { + logger.info(`Successfully deleted ${result.successCount} documents`) + refreshDocuments() + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + }, + onSettled: () => { + setShowBulkDeleteModal(false) + }, + } + ) + return + } + const documentsToDelete = documents.filter((doc) => selectedDocuments.has(doc.id)) if (documentsToDelete.length === 0) return @@ -881,14 +868,17 @@ export function KnowledgeBase({ } const selectedDocumentsList = documents.filter((doc) => selectedDocuments.has(doc.id)) - const enabledCount = selectedDocumentsList.filter((doc) => doc.enabled).length - const disabledCount = selectedDocumentsList.filter((doc) => !doc.enabled).length + const enabledCount = isSelectAllMode + ? enabledFilter === 'disabled' + ? 0 + : pagination.total + : selectedDocumentsList.filter((doc) => doc.enabled).length + const disabledCount = isSelectAllMode + ? enabledFilter === 'enabled' + ? 0 + : pagination.total + : selectedDocumentsList.filter((doc) => !doc.enabled).length - /** - * Handle right-click on a document row - * If right-clicking on an unselected document, select only that document - * If right-clicking on a selected document with multiple selections, keep all selections - */ const handleDocumentContextMenu = useCallback( (e: React.MouseEvent, doc: DocumentData) => { const isCurrentlySelected = selectedDocuments.has(doc.id) @@ -1005,11 +995,13 @@ export function KnowledgeBase({
- {knowledgeBase?.description && ( -

- {knowledgeBase.description} -

- )} +
+ {knowledgeBase?.description && ( +

+ {knowledgeBase.description} +

+ )} +
@@ -1052,21 +1044,76 @@ export function KnowledgeBase({ ))}
- - - - - {userPermissions.canEdit !== true && ( - Write permission required to add documents - )} - +
+ + + + + +
+ { + setEnabledFilter('all') + setIsFilterPopoverOpen(false) + setCurrentPage(1) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + }} + > + All + + { + setEnabledFilter('enabled') + setIsFilterPopoverOpen(false) + setCurrentPage(1) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + }} + > + Enabled + + { + setEnabledFilter('disabled') + setIsFilterPopoverOpen(false) + setCurrentPage(1) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + }} + > + Disabled + +
+
+
+ + + + + + {userPermissions.canEdit !== true && ( + Write permission required to add documents + )} + +
{error && !isLoadingKnowledgeBase && ( @@ -1089,14 +1136,20 @@ export function KnowledgeBase({

- {searchQuery ? 'No documents found' : 'No documents yet'} + {searchQuery + ? 'No documents found' + : enabledFilter !== 'all' + ? 'Nothing matches your filter' + : 'No documents yet'}

{searchQuery ? 'Try a different search term' - : userPermissions.canEdit === true - ? 'Add documents to get started' - : 'Documents will appear here once added'} + : enabledFilter !== 'all' + ? 'Try changing the filter' + : userPermissions.canEdit === true + ? 'Add documents to get started' + : 'Documents will appear here once added'}

@@ -1120,7 +1173,7 @@ export function KnowledgeBase({ {renderSortableHeader('tokenCount', 'Tokens', 'hidden w-[8%] lg:table-cell')} {renderSortableHeader('chunkCount', 'Chunks', 'w-[8%]')} {renderSortableHeader('uploadedAt', 'Uploaded', 'w-[11%]')} - {renderSortableHeader('processingStatus', 'Status', 'w-[10%]')} + {renderSortableHeader('enabled', 'Status', 'w-[10%]')} Tags @@ -1164,7 +1217,10 @@ export function KnowledgeBase({
- {getFileIcon(doc.mimeType, doc.filename)} + {(() => { + const IconComponent = getDocumentIcon(doc.mimeType, doc.filename) + return + })()} setIsSelectAllMode(true)} + onClearSelectAll={() => { + setIsSelectAllMode(false) + setSelectedDocuments(new Set()) + }} /> void + onClearSelectAll?: () => void } export function ActionBar({ @@ -24,14 +29,21 @@ export function ActionBar({ disabledCount = 0, isLoading = false, className, + totalCount = 0, + isAllPageSelected = false, + isAllSelected = false, + onSelectAll, + onClearSelectAll, }: ActionBarProps) { const userPermissions = useUserPermissionsContext() - if (selectedCount === 0) return null + if (selectedCount === 0 && !isAllSelected) return null const canEdit = userPermissions.canEdit const showEnableButton = disabledCount > 0 && onEnable && canEdit const showDisableButton = enabledCount > 0 && onDisable && canEdit + const showSelectAllOption = + isAllPageSelected && !isAllSelected && totalCount > selectedCount && onSelectAll return (
- {selectedCount} selected + {isAllSelected ? totalCount : selectedCount} selected + {showSelectAllOption && ( + <> + {' · '} + + + )} + {isAllSelected && onClearSelectAll && ( + <> + {' · '} + + + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx index c76efaff2..3e3b5b59b 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx @@ -123,7 +123,11 @@ export function RenameDocumentModal({ > Cancel -
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx index b07e66dd1..a213b7431 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx @@ -3,6 +3,7 @@ import { useCallback, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { Badge, DocumentAttachment, Tooltip } from '@/components/emcn' +import { formatAbsoluteDate, formatRelativeTime } from '@/lib/core/utils/formatting' import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' @@ -21,55 +22,6 @@ interface BaseCardProps { onDelete?: (id: string) => Promise } -/** - * Formats a date string to relative time (e.g., "2h ago", "3d ago") - */ -function formatRelativeTime(dateString: string): string { - const date = new Date(dateString) - const now = new Date() - const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000) - - if (diffInSeconds < 60) { - return 'just now' - } - if (diffInSeconds < 3600) { - const minutes = Math.floor(diffInSeconds / 60) - return `${minutes}m ago` - } - if (diffInSeconds < 86400) { - const hours = Math.floor(diffInSeconds / 3600) - return `${hours}h ago` - } - if (diffInSeconds < 604800) { - const days = Math.floor(diffInSeconds / 86400) - return `${days}d ago` - } - if (diffInSeconds < 2592000) { - const weeks = Math.floor(diffInSeconds / 604800) - return `${weeks}w ago` - } - if (diffInSeconds < 31536000) { - const months = Math.floor(diffInSeconds / 2592000) - return `${months}mo ago` - } - const years = Math.floor(diffInSeconds / 31536000) - return `${years}y ago` -} - -/** - * Formats a date string to absolute format for tooltip display - */ -function formatAbsoluteDate(dateString: string): string { - const date = new Date(dateString) - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) -} - /** * Skeleton placeholder for a knowledge base card */ diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx index 0d8140ed0..70419c821 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx @@ -344,53 +344,51 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {