diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/documents-empty-state.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/documents-empty-state.tsx
new file mode 100644
index 00000000000..d94d5337eac
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/documents-empty-state.tsx
@@ -0,0 +1,140 @@
+import { Chip, cn } from '@sim/emcn'
+import { Plus } from '@sim/emcn/icons'
+import { EmptyState } from '@/components/empty-state/empty-state'
+import { EmptyStateDocsLink } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/docs-link'
+import { HAIRLINE } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline'
+import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask'
+
+const KNOWLEDGE_DOCS_URL = 'https://docs.sim.ai/knowledgebase'
+
+/**
+ * Ruled text — see the `INK` note in `tables-empty-state.tsx` for why not the surface
+ * ramp. The heading line takes the stronger mix so the sheet reads as a document
+ * rather than as blank ruling.
+ */
+const INK = {
+ heading: 'color-mix(in srgb, var(--text-secondary) 30%, transparent)',
+ body: 'color-mix(in srgb, var(--text-secondary) 15%, transparent)',
+} as const
+
+/** Widths of the ruled body lines, in viewBox units. */
+const BODY_LINES = [
+ { y: 84, width: 62 },
+ { y: 98, width: 54 },
+ { y: 112, width: 66 },
+] as const
+
+/**
+ * The front sheet, with the top-right corner turned back.
+ *
+ * The dog-ear is the one signifier no other graphic in the set uses — the folder is a
+ * container, the knowledge mark is a shelf of volumes, and this has to read as the
+ * pages inside one of them.
+ */
+const SHEET = [
+ 'M 58 34',
+ 'H 128',
+ 'L 148 54',
+ 'V 140',
+ 'Q 148 146 142 146',
+ 'H 58',
+ 'Q 52 146 52 140',
+ 'V 40',
+ 'Q 52 34 58 34',
+ 'Z',
+].join(' ')
+
+/** The turned-back corner itself, drawn over the sheet so its two edges both read. */
+const DOG_EAR = ['M 128 34', 'L 148 54', 'H 134', 'Q 128 54 128 48', 'Z'].join(' ')
+
+/**
+ * Dissolves upward, the direction the stack recedes — the same call the folder makes.
+ * The sheets behind are a repeating structure and cost nothing cropped, while the front
+ * sheet stays crisp where the copy begins beneath it.
+ */
+const STACK_FADE =
+ '[-webkit-mask-image:linear-gradient(to_top,#000_56%,transparent_100%)] [mask-image:linear-gradient(to_top,#000_56%,transparent_100%)]'
+
+/**
+ * The artwork's own bounds, so the frame follows the drawing rather than a round number.
+ *
+ * The sheets are authored around the front sheet's origin and step up and to the right,
+ * which leaves the drawn mass off-centre inside any round-numbered viewBox — enough that
+ * the mark sat visibly right of the copy beneath it. Framing on these bounds centres the
+ * two. They are read off the shapes below by hand, so retuning the stack means updating
+ * them here.
+ */
+const ART = { minX: 52, maxX: 166, minY: 18, maxY: 146 } as const
+const ART_PADDING = 10
+const VIEW_BOX_WIDTH = ART.maxX - ART.minX + ART_PADDING * 2
+const VIEW_BOX_HEIGHT = ART.maxY - ART.minY + ART_PADDING * 2
+const VIEW_BOX = `${ART.minX - ART_PADDING} ${ART.minY - ART_PADDING} ${VIEW_BOX_WIDTH} ${VIEW_BOX_HEIGHT}`
+
+/** Matches the other resource graphics, so the set centres as one collection. */
+const MARK_HEIGHT = 148
+
+/**
+ * A stack of sheets, the front one dog-eared and ruled. The two behind it step up and
+ * to the right, so the stack reads as depth rather than as a single thick page.
+ */
+function DocumentsGraphic() {
+ return (
+
+ )
+}
+
+interface DocumentsEmptyStateProps {
+ /** Opens the file picker — the same action the header's primary chip runs. */
+ onAddDocuments: () => void
+ /** Mirrors the header chip's disabled state: no edit rights on the workspace. */
+ addDisabled?: boolean
+}
+
+/** Empty state for a knowledge base that holds no documents yet. */
+export function DocumentsEmptyState({
+ onAddDocuments,
+ addDisabled = false,
+}: DocumentsEmptyStateProps) {
+ return (
+ }
+ title='Documents'
+ description='Upload documents so your agents can search this base.'
+ action={
+ <>
+
+ New documents
+
+
+ >
+ }
+ />
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state.tsx
index ae1735a7015..4fb2d63be38 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state.tsx
@@ -2,25 +2,11 @@ import { Chip, cn } from '@sim/emcn'
import { Upload } from '@sim/emcn/icons'
import { EmptyState } from '@/components/empty-state/empty-state'
import { EmptyStateDocsLink } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/docs-link'
+import { HAIRLINE } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline'
import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask'
const FILES_DOCS_URL = 'https://docs.sim.ai/files'
-/**
- * Hairline contours, matching the tables grid's 1px `--border` rules and the
- * knowledge mark's thinned strokes — the graphics sit one nav item apart, so a
- * heavier outline here would read as a different illustration system.
- *
- * Fills stay near the top of the surface ramp for the same reason: the border
- * draws the folder and the fill only has to separate one layer from the next. A
- * solid mid-grey body made this the heaviest thing on the page.
- */
-const HAIRLINE = {
- stroke: 'var(--border)',
- strokeWidth: 1.1,
- strokeLinejoin: 'round' as const,
-} as const
-
const FOLDER_BACK = [
'M 22 34',
'H 66',
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline.ts
new file mode 100644
index 00000000000..6047c31ba94
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline.ts
@@ -0,0 +1,15 @@
+/**
+ * The contour weight every drawn resource graphic shares.
+ *
+ * Matches the tables grid's 1px `--border` rules and the knowledge mark's thinned
+ * strokes — the graphics sit a nav level apart, so a heavier outline on any one of
+ * them reads as a different illustration system.
+ *
+ * Fills stay near the top of the surface ramp for the same reason: the border draws
+ * the shape and the fill only has to separate one layer from the next.
+ */
+export const HAIRLINE = {
+ stroke: 'var(--border)',
+ strokeWidth: 1.1,
+ strokeLinejoin: 'round' as const,
+} as const
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts
index 7b34c26e6b5..59e06a779dd 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts
@@ -1,3 +1,4 @@
+export { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/documents-empty-state'
export { FilesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state'
export { KnowledgeEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/knowledge-empty-state'
export { LogsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/logs-empty-state'
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
index a01bb376e57..7f963ea85c8 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
@@ -61,6 +61,7 @@ import type {
import {
FILTER_SECTION_LABEL_CLASS,
FloatingOverflowText,
+ isResourceListEmpty,
Resource,
} from '@/app/workspace/[workspaceId]/components'
import {
@@ -69,6 +70,7 @@ import {
folderedResourceListHref,
useFolderAncestors,
} from '@/app/workspace/[workspaceId]/components/folders'
+import { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
import { DocumentTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components'
import {
ActionBar,
@@ -411,6 +413,7 @@ export function KnowledgeBase({
const {
documents,
pagination,
+ isLoading: isLoadingDocuments,
isPlaceholderData: isPlaceholderDocuments,
error: documentsError,
hasProcessingDocuments,
@@ -1145,6 +1148,25 @@ export function KnowledgeBase({
[enabledFilter, tagFilterEntries]
)
+ /**
+ * The zero-data graphic is for a base that holds no documents — not a search or a
+ * filter that matched nothing, and not a page past the last one. Counts on the
+ * server's `total` rather than the visible rows, because this list is paginated:
+ * an empty page 2 is a paging position, not an empty base.
+ *
+ * The remaining gates mirror the workspace resource pages — the list must have
+ * arrived, must not be serving the previous query key's rows, and must not have
+ * failed. This list has no folders, so the folder arguments are omitted.
+ */
+ const showEmptyState = isResourceListEmpty({
+ rowCount: pagination.total,
+ isLoading: isLoadingDocuments,
+ isPlaceholderData: isPlaceholderDocuments,
+ error: documentsError,
+ search: debouncedSearchQuery,
+ filterCount: filterTags.length,
+ })
+
const selectableConfig: SelectableConfig = {
selectedIds: selectedDocuments,
onSelectRow: handleSelectDocument,
@@ -1271,6 +1293,14 @@ export function KnowledgeBase({
+ ) : undefined
+ }
selectable={selectableConfig}
onRowClick={handleDocumentClick}
onRowContextMenu={handleDocumentContextMenu}