Skip to content

Commit 936e1ac

Browse files
authored
fix(knowledge): say what went wrong when a document's chunks fail to load (#6858)
* fix(knowledge): say what went wrong when a document's chunks fail to load `combinedError = documentError || searchError || initialError` collapsed three different failures into one, blanked the rows and stripped search, sort and filter — and said nothing. A failed read rendered as an empty table, which is the same thing the page shows when a document genuinely has no chunks. Stripping the search box was the worse half: when it was the *search* that failed, the control the user needed to clear it was the one that disappeared. The three are now told apart: - The document itself failing has no page left to draw, so it gets a full screen, matching the base page's 'Knowledge base not found' one level up. It has to run before the editor branches — `selectedChunkId` renders the chunk editor without checking for a document, so a deep link to a chunk of a deleted document sat on 'Loading chunk…' forever. - A failed chunk read keeps the document, so it keeps the chrome and the controls, and the message goes in the table body through the `emptyState` slot. Tinted with the error token, because at the weight the empty states use a failure is indistinguishable from 'nothing here yet'. - A failed search leaves the loaded chunks intact, so it says the search failed rather than claiming the chunks could not be loaded. `searchError` went through `instanceof Error ? .message : null`, so a rejection that was not an `Error` produced no message and fell back to the silent blank this commit exists to remove. It uses `getErrorMessage` now, like the chunk read beside it always did. Pagination is dropped on a failed read — it was counting pages nothing fetched — and the action bar reads the same value, so it no longer lifts itself clear of a bar that is not there. The not-found screen was about to be copied a second time, so it moves to `ResourceNotFound` and the base page adopts it. * fix(knowledge): let a processing document say so, not that it failed A document that is not `completed` rejects the chunk read by design — `requireChunkReadable` throws `KnowledgeDocumentNotReadyError` before it queries anything — so `initialError` is set for every pending, processing or failed document. Treating that as a load failure put "Couldn't load chunks" over a document that is simply still working. `chunkRows` already builds the right row for those states, and it turns out nothing could ever see it: the old `combinedError` blanked the rows on exactly the same condition, so "Document processing pending..." has been unreachable for as long as it has existed. Excluding not-ready documents from `chunkError` brings the row back and leaves the error state for reads that genuinely failed.
1 parent 9864f5c commit 936e1ac

4 files changed

Lines changed: 147 additions & 49 deletions

File tree

apps/sim/app/workspace/[workspaceId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { isResourceListEmpty } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty'
2+
export { ResourceNotFound } from '@/app/workspace/[workspaceId]/components/resource/resource-not-found'
23
export { ConversationListItem } from './conversation-list-item'
34
export type { ErrorBoundaryProps, ErrorStateProps } from './error'
45
export { ErrorShell, ErrorState } from './error'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ComponentType } from 'react'
2+
3+
interface ResourceNotFoundProps {
4+
icon: ComponentType<{ className?: string }>
5+
title: string
6+
description: string
7+
}
8+
9+
/**
10+
* Full-page screen for a resource that could not be loaded and has no shell left to
11+
* draw — a knowledge base or a document that was deleted or moved.
12+
*
13+
* Distinct from the `emptyState` slot on {@link Resource.Table}: that one keeps the
14+
* chrome and reports a failure *within* a page that still exists. This replaces the
15+
* page, so it is only right when the thing the page is about is the thing that is gone.
16+
*/
17+
export function ResourceNotFound({ icon: Icon, title, description }: ResourceNotFoundProps) {
18+
return (
19+
<div className='flex h-full flex-col items-center justify-center gap-3'>
20+
<Icon className='size-[32px] text-[var(--text-muted)]' />
21+
<div className='flex flex-col items-center gap-1'>
22+
<h2 className='text-[20px] text-[var(--text-secondary)]'>{title}</h2>
23+
<p className='text-[var(--text-muted)] text-small'>{description}</p>
24+
</div>
25+
</div>
26+
)
27+
}

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

Lines changed: 113 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ import {
77
ChevronUp,
88
Database,
99
FileText,
10+
FileX,
1011
Pencil,
1112
Plus,
1213
TagIcon,
1314
Trash,
15+
TriangleAlert,
1416
} from '@sim/emcn/icons'
1517
import { createLogger } from '@sim/logger'
18+
import { getErrorMessage } from '@sim/utils/errors'
1619
import { truncate } from '@sim/utils/string'
1720
import { useParams, useRouter } from 'next/navigation'
1821
import { useQueryStates } from 'nuqs'
22+
import { EmptyState } from '@/components/empty-state/empty-state'
1923
import type { ChunkData } from '@/lib/knowledge/types'
2024
import { formatTokenCount } from '@/lib/tokenization'
2125
import type {
@@ -29,7 +33,11 @@ import type {
2933
SelectableConfig,
3034
SortConfig,
3135
} from '@/app/workspace/[workspaceId]/components'
32-
import { EMPTY_CELL_PLACEHOLDER, Resource } from '@/app/workspace/[workspaceId]/components'
36+
import {
37+
EMPTY_CELL_PLACEHOLDER,
38+
Resource,
39+
ResourceNotFound,
40+
} from '@/app/workspace/[workspaceId]/components'
3341
import {
3442
FOLDERED_RESOURCE_HEADERS,
3543
folderBreadcrumbItems,
@@ -138,6 +146,41 @@ const CHUNK_COLUMNS: ResourceColumn[] = [
138146
{ id: 'status', header: 'Status', widthMultiplier: 0.75 },
139147
]
140148

149+
/** Stable identity for the error branch's empty row set. */
150+
const EMPTY_CHUNK_ROWS: ResourceRow[] = []
151+
152+
/** Longer than this and a server message pushes the frame taller than the table body. */
153+
const ERROR_MESSAGE_MAX_LENGTH = 160
154+
155+
interface ChunkLoadErrorProps {
156+
message: string
157+
/**
158+
* Which read failed. A failed search leaves the loaded chunks intact, so saying the
159+
* chunks could not be loaded would be untrue — it is the search that did not run.
160+
*/
161+
kind: 'load' | 'search'
162+
}
163+
164+
/**
165+
* A failed read, drawn where the rows would have been.
166+
*
167+
* The table keeps its chrome and its controls — the headers still render, and the search
168+
* box stays so a search that triggered the failure can be cleared. Only the body says
169+
* what went wrong, instead of the page going silently blank.
170+
*
171+
* Tinted with the error token: at the muted weight the other empty states use, a failure
172+
* is indistinguishable from "nothing here yet", which is the confusion this exists to end.
173+
*/
174+
function ChunkLoadError({ message, kind }: ChunkLoadErrorProps) {
175+
return (
176+
<EmptyState
177+
graphic={<TriangleAlert className='size-[24px] text-[var(--text-error)]' />}
178+
title={kind === 'search' ? 'Search failed' : "Couldn't load chunks"}
179+
description={truncate(message, ERROR_MESSAGE_MAX_LENGTH)}
180+
/>
181+
)
182+
}
183+
141184
export function Document({
142185
knowledgeBaseId,
143186
documentId,
@@ -242,7 +285,7 @@ export function Document({
242285
}
243286
)
244287

245-
const searchError = searchQueryError instanceof Error ? searchQueryError.message : null
288+
const searchError = searchQueryError ? getErrorMessage(searchQueryError) : null
246289

247290
const [selectedChunks, setSelectedChunks] = useState<Set<string>>(() => new Set())
248291

@@ -334,8 +377,6 @@ export function Document({
334377
closeMenu: closeContextMenu,
335378
} = useContextMenu()
336379

337-
const combinedError = documentError || searchError || initialError
338-
339380
const isConnectorDocument = Boolean(documentData?.connectorId)
340381
const effectiveDocumentName = documentData?.filename || documentName || 'Document'
341382
/**
@@ -351,6 +392,17 @@ export function Document({
351392
const DocumentIcon =
352393
ConnectorIcon || getDocumentIcon(documentData?.mimeType ?? '', effectiveDocumentName)
353394
const isCompleted = documentData?.processingStatus === 'completed'
395+
396+
/**
397+
* Kept separate from `documentError`: without the document there is no page to draw,
398+
* while a failed chunk read still has one to frame it.
399+
*
400+
* A document that is not `completed` is excluded, because the chunk read rejects for
401+
* those by design — `requireChunkReadable` throws `KnowledgeDocumentNotReadyError`
402+
* before it queries anything. That is the document's state, not a failure, and
403+
* `chunkRows` already renders a row saying which state it is in.
404+
*/
405+
const chunkError = isCompleted ? initialError || searchError : null
354406
const canEdit = userPermissions.canEdit === true
355407

356408
const isInEditorView = selectedChunkId !== null || isCreatingNewChunk
@@ -533,7 +585,7 @@ export function Document({
533585

534586
/**
535587
* `Knowledge Base / …the base's folders / <base> / <last>`. Every view on this route is that
536-
* trail with a different last crumb — the document, a chunk, an error, a loading placeholder
588+
* trail with a different last crumb — the document, a chunk, a loading placeholder
537589
* — so it is built once here rather than restated per view.
538590
*/
539591
const documentTrail = useCallback(
@@ -572,35 +624,30 @@ export function Document({
572624

573625
const breadcrumbs = useMemo<BreadcrumbItem[]>(
574626
() =>
575-
documentTrail(
576-
combinedError
577-
? { label: 'Error', terminal: true }
578-
: {
579-
label: documentCrumbLabel,
580-
icon: DocumentIcon,
581-
editing: docRename.editingId
582-
? {
583-
isEditing: true,
584-
value: docRename.editValue,
585-
onChange: docRename.setEditValue,
586-
onSubmit: docRename.submitRename,
587-
onCancel: docRename.cancelRename,
588-
disabled: docRename.isSaving,
589-
}
590-
: undefined,
591-
dropdownItems: [
592-
...(userPermissions.canEdit
593-
? [
594-
{ label: 'Rename', icon: Pencil, onClick: handleStartDocRename },
595-
{ label: 'Tags', icon: TagIcon, onClick: handleShowTags },
596-
{ label: 'Delete', icon: Trash, onClick: handleShowDeleteDoc },
597-
]
598-
: []),
599-
],
627+
documentTrail({
628+
label: documentCrumbLabel,
629+
icon: DocumentIcon,
630+
editing: docRename.editingId
631+
? {
632+
isEditing: true,
633+
value: docRename.editValue,
634+
onChange: docRename.setEditValue,
635+
onSubmit: docRename.submitRename,
636+
onCancel: docRename.cancelRename,
637+
disabled: docRename.isSaving,
600638
}
601-
),
639+
: undefined,
640+
dropdownItems: [
641+
...(userPermissions.canEdit
642+
? [
643+
{ label: 'Rename', icon: Pencil, onClick: handleStartDocRename },
644+
{ label: 'Tags', icon: TagIcon, onClick: handleShowTags },
645+
{ label: 'Delete', icon: Trash, onClick: handleShowDeleteDoc },
646+
]
647+
: []),
648+
],
649+
}),
602650
[
603-
combinedError,
604651
documentTrail,
605652
documentCrumbLabel,
606653
DocumentIcon,
@@ -914,6 +961,12 @@ export function Document({
914961
}
915962
: undefined
916963

964+
/**
965+
* A failed read paged nothing, so the bar would be counting pages that were never
966+
* fetched. Read by the table and by the action bar's offset, which has to agree.
967+
*/
968+
const tablePagination = chunkError ? undefined : paginationConfig
969+
917970
const sortConfig: SortConfig = useMemo(
918971
() => ({
919972
options: [
@@ -1099,6 +1152,21 @@ export function Document({
10991152
saveStatus,
11001153
])
11011154

1155+
/**
1156+
* Ahead of the editor branches on purpose — `selectedChunkId` renders the chunk editor
1157+
* without checking for a document, so a document that failed to load has to
1158+
* short-circuit before it. Mirrors the base page's 'not found' screen one level up.
1159+
*/
1160+
if (documentError && !documentData) {
1161+
return (
1162+
<ResourceNotFound
1163+
icon={FileX}
1164+
title='Document not found'
1165+
description='This document may have been deleted or moved'
1166+
/>
1167+
)
1168+
}
1169+
11021170
if (isCreatingNewChunk && documentData) {
11031171
return (
11041172
<>
@@ -1191,18 +1259,23 @@ export function Document({
11911259
]}
11921260
/>
11931261
<Resource.Options
1194-
search={combinedError ? undefined : searchConfig}
1195-
sort={combinedError ? undefined : sortConfig}
1196-
filterTags={combinedError ? undefined : filterTags}
1197-
filter={combinedError ? undefined : { content: filterContent }}
1262+
search={searchConfig}
1263+
sort={sortConfig}
1264+
filterTags={filterTags}
1265+
filter={{ content: filterContent }}
11981266
/>
11991267
<Resource.Table
12001268
columns={CHUNK_COLUMNS}
1201-
rows={combinedError ? [] : chunkRows}
1202-
selectable={combinedError ? undefined : selectableConfig}
1269+
rows={chunkError ? EMPTY_CHUNK_ROWS : chunkRows}
1270+
emptyState={
1271+
chunkError ? (
1272+
<ChunkLoadError message={chunkError} kind={initialError ? 'load' : 'search'} />
1273+
) : undefined
1274+
}
1275+
selectable={chunkError ? undefined : selectableConfig}
12031276
onRowClick={isCompleted ? handleChunkClick : undefined}
12041277
onRowContextMenu={isCompleted ? handleChunkContextMenu : undefined}
1205-
pagination={paginationConfig}
1278+
pagination={tablePagination}
12061279
/>
12071280
</Resource>
12081281

@@ -1223,7 +1296,7 @@ export function Document({
12231296
/>
12241297

12251298
<ActionBar
1226-
className={paginationConfig ? 'bottom-[72px]' : undefined}
1299+
className={tablePagination ? 'bottom-[72px]' : undefined}
12271300
selectedCount={selectedChunks.size}
12281301
onEnable={disabledCount > 0 && !isConnectorDocument ? handleBulkEnable : undefined}
12291302
onDisable={enabledCount > 0 && !isConnectorDocument ? handleBulkDisable : undefined}

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
FloatingOverflowText,
6464
isResourceListEmpty,
6565
Resource,
66+
ResourceNotFound,
6667
} from '@/app/workspace/[workspaceId]/components'
6768
import {
6869
FOLDERED_RESOURCE_HEADERS,
@@ -1258,15 +1259,11 @@ export function KnowledgeBase({
12581259

12591260
if (error && !knowledgeBase) {
12601261
return (
1261-
<div className='flex h-full flex-col items-center justify-center gap-3'>
1262-
<DatabaseX className='size-[32px] text-[var(--text-muted)]' />
1263-
<div className='flex flex-col items-center gap-1'>
1264-
<h2 className='text-[20px] text-[var(--text-secondary)]'>Knowledge base not found</h2>
1265-
<p className='text-[var(--text-muted)] text-small'>
1266-
This knowledge base may have been deleted or moved
1267-
</p>
1268-
</div>
1269-
</div>
1262+
<ResourceNotFound
1263+
icon={DatabaseX}
1264+
title='Knowledge base not found'
1265+
description='This knowledge base may have been deleted or moved'
1266+
/>
12701267
)
12711268
}
12721269

0 commit comments

Comments
 (0)