Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<svg
viewBox={VIEW_BOX}
width={(MARK_HEIGHT * VIEW_BOX_WIDTH) / VIEW_BOX_HEIGHT}
height={MARK_HEIGHT}
fill='none'
aria-hidden='true'
focusable='false'
className={cn('block max-w-none shrink-0', STACK_FADE, MASK_NO_REPEAT)}
>
<rect x='70' y='18' width='96' height='112' rx='6' fill='var(--surface-4)' {...HAIRLINE} />
<rect x='61' y='26' width='96' height='112' rx='6' fill='var(--surface-3)' {...HAIRLINE} />

<path d={SHEET} fill='var(--surface-2)' {...HAIRLINE} />
<path d={DOG_EAR} fill='var(--surface-4)' {...HAIRLINE} />

<rect x='68' y='64' width='44' height='6' rx='3' fill={INK.heading} />
{BODY_LINES.map((line) => (
<rect
key={line.y}
x='68'
y={line.y}
width={line.width}
height='5'
rx='2.5'
fill={INK.body}
/>
))}
</svg>
)
}

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 (
<EmptyState
graphic={<DocumentsGraphic />}
title='Documents'
description='Upload documents so your agents can search this base.'
action={
<>
<Chip variant='primary' onClick={onAddDocuments} disabled={addDisabled} leftIcon={Plus}>
New documents
</Chip>
<EmptyStateDocsLink href={KNOWLEDGE_DOCS_URL} />
</>
}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
30 changes: 30 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import type {
import {
FILTER_SECTION_LABEL_CLASS,
FloatingOverflowText,
isResourceListEmpty,
Resource,
} from '@/app/workspace/[workspaceId]/components'
import {
Expand All @@ -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,
Expand Down Expand Up @@ -411,6 +413,7 @@ export function KnowledgeBase({
const {
documents,
pagination,
isLoading: isLoadingDocuments,
isPlaceholderData: isPlaceholderDocuments,
error: documentsError,
hasProcessingDocuments,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1271,6 +1293,14 @@ export function KnowledgeBase({
<Resource.Table
columns={DOCUMENT_COLUMNS}
rows={documentRows}
emptyState={
showEmptyState ? (
<DocumentsEmptyState
onAddDocuments={handleAddDocuments}
addDisabled={userPermissions.canEdit !== true}
/>
) : undefined
}
selectable={selectableConfig}
onRowClick={handleDocumentClick}
onRowContextMenu={handleDocumentContextMenu}
Expand Down
Loading