|
| 1 | +import { Chip, ChipLink, Plus } from '@sim/emcn' |
| 2 | +import { BookOpen } from '@sim/emcn/icons' |
1 | 3 | import { EmptyState } from '@/components/empty-state/empty-state' |
2 | | -import { |
3 | | - Bar, |
4 | | - Vignette, |
5 | | -} from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/vignette' |
6 | 4 |
|
7 | 5 | /** |
8 | | - * A document fanning into the chunks it is embedded as — the moment that makes a |
9 | | - * knowledge base a knowledge base, drawn in the editor vignette's connector |
10 | | - * language (6px-radius smooth steps in `--workflow-edge`). |
| 6 | + * Neutral ink at graded strengths. |
| 7 | + * |
| 8 | + * `--surface-4`/`--surface-5` are near-white in light mode (#f5f5f5/#f3f3f3), so |
| 9 | + * skeleton geometry built on them dissolves against the page. Mixing |
| 10 | + * `--text-secondary` into transparent gives a real mid-grey that inverts with the |
| 11 | + * theme — the idiom the workflow editor's vignette uses for the one bar it needs |
| 12 | + * you to actually see. |
| 13 | + */ |
| 14 | +const INK = { |
| 15 | + heading: 'color-mix(in srgb, var(--text-secondary) 30%, transparent)', |
| 16 | + line: 'color-mix(in srgb, var(--text-secondary) 15%, transparent)', |
| 17 | + chunk: 'color-mix(in srgb, var(--text-secondary) 11%, transparent)', |
| 18 | +} as const |
| 19 | + |
| 20 | +/** |
| 21 | + * Crisp at the top-left, dissolving through the bottom-right — the same |
| 22 | + * two-gradient intersect the landing page's workflow vignette uses, so the |
| 23 | + * geometry blends into the page rather than sitting on it as a card. |
| 24 | + * |
| 25 | + * Held back further than the tables grid on both axes: the document has to stay |
| 26 | + * whole for the graphic to mean anything, and only the chunks it fans into may |
| 27 | + * trail off. |
| 28 | + */ |
| 29 | +const CORNER_FADE = |
| 30 | + '[-webkit-mask-image:linear-gradient(to_right,#000_66%,transparent_100%),linear-gradient(to_bottom,#000_70%,transparent_100%)] [mask-image:linear-gradient(to_right,#000_66%,transparent_100%),linear-gradient(to_bottom,#000_70%,transparent_100%)] [-webkit-mask-composite:source-in] [mask-composite:intersect]' |
| 31 | + |
| 32 | +/** Text-line widths inside the document; the first reads as its heading. */ |
| 33 | +const DOCUMENT_LINES = [64, 78, 58, 72, 46] as const |
| 34 | + |
| 35 | +/** |
| 36 | + * Smooth steps at the editor vignette's 6px corner radius, fanning from the |
| 37 | + * document's right edge to the first column of chunks. |
11 | 38 | */ |
12 | 39 | const CHUNK_EDGE_PATHS = [ |
13 | | - 'M190,80 H207 Q213,80 213,74 V52 Q213,46 219,46 H236', |
14 | | - 'M190,80 H236', |
15 | | - 'M190,80 H207 Q213,80 213,86 V108 Q213,114 219,114 H236', |
| 40 | + 'M130,71 H141 Q147,71 147,65 V51 Q147,45 153,45 H164', |
| 41 | + 'M130,71 H164', |
| 42 | + 'M130,71 H141 Q147,71 147,77 V91 Q147,97 153,97 H164', |
16 | 43 | ] as const |
17 | 44 |
|
| 45 | +const CHUNK_COLUMNS = [164, 190, 216, 242, 268, 294] as const |
| 46 | +const CHUNK_ROWS = [35, 61, 87, 113] as const |
| 47 | + |
| 48 | +/** Rows the edges actually land on — only those chunks read as filled. */ |
| 49 | +const CONNECTED_ROWS = new Set([35, 61, 87]) |
| 50 | + |
| 51 | +/** A document, and the chunks it is embedded as running off the far corner. */ |
18 | 52 | function KnowledgeGraphic() { |
19 | 53 | return ( |
20 | | - <Vignette> |
| 54 | + <div aria-hidden='true' className={`relative h-[148px] w-[320px] ${CORNER_FADE}`}> |
| 55 | + <div className='absolute top-[34px] left-[26px] h-[74px] w-[104px] rounded-[6px] border border-[var(--border-1)] px-3 pt-[13px]'> |
| 56 | + {DOCUMENT_LINES.map((width, index) => ( |
| 57 | + <span |
| 58 | + key={width} |
| 59 | + className='mb-[9px] block h-[5px] rounded-full' |
| 60 | + style={{ width, background: index === 0 ? INK.heading : INK.line }} |
| 61 | + /> |
| 62 | + ))} |
| 63 | + </div> |
| 64 | + |
21 | 65 | <svg className='absolute inset-0' fill='none' viewBox='0 0 320 148' width='320' height='148'> |
22 | | - <defs> |
23 | | - <linearGradient |
24 | | - id='knowledge-empty-edge' |
25 | | - x1='190' |
26 | | - y1='0' |
27 | | - x2='236' |
28 | | - y2='0' |
29 | | - gradientUnits='userSpaceOnUse' |
30 | | - > |
31 | | - <stop stopColor='var(--workflow-edge)' /> |
32 | | - <stop offset='1' stopColor='var(--workflow-edge)' /> |
33 | | - </linearGradient> |
34 | | - </defs> |
35 | 66 | {CHUNK_EDGE_PATHS.map((d) => ( |
36 | 67 | <path |
37 | 68 | key={d} |
38 | 69 | d={d} |
39 | | - stroke='url(#knowledge-empty-edge)' |
| 70 | + stroke='var(--workflow-edge)' |
40 | 71 | strokeWidth='1.5' |
41 | 72 | strokeLinecap='round' |
42 | 73 | strokeLinejoin='round' |
43 | 74 | /> |
44 | 75 | ))} |
45 | 76 | </svg> |
46 | 77 |
|
47 | | - <span className='absolute top-[22px] left-[78px] h-[92px] w-[124px] rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-1)] opacity-40' /> |
48 | | - <span className='absolute top-[28px] left-[72px] h-[92px] w-[124px] rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)] opacity-70' /> |
49 | | - |
50 | | - <div className='absolute top-[34px] left-[66px] h-[92px] w-[124px] rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)]'> |
51 | | - <div className='flex items-center gap-2 px-3 pt-3 pb-2.5'> |
52 | | - <span className='size-[14px] shrink-0 rounded-[4px] bg-[var(--surface-5)]' /> |
53 | | - <Bar className='h-2 w-[62px]' /> |
54 | | - </div> |
55 | | - <div className='space-y-[7px] px-3'> |
56 | | - <Bar className='h-2 w-[98px]' /> |
57 | | - <Bar className='h-2 w-[84px]' /> |
58 | | - <span className='block h-2 w-[92px] rounded-[3px] bg-[color-mix(in_srgb,var(--brand-knowledge)_26%,transparent)]' /> |
59 | | - <Bar className='h-2 w-[70px]' /> |
60 | | - </div> |
61 | | - </div> |
62 | | - |
63 | | - {[35, 69, 103].map((top, index) => ( |
64 | | - <span |
65 | | - key={top} |
66 | | - className='absolute left-[236px] size-[22px] rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-3)]' |
67 | | - style={{ top }} |
68 | | - > |
| 78 | + {CHUNK_ROWS.map((top) => |
| 79 | + CHUNK_COLUMNS.map((left) => ( |
69 | 80 | <span |
70 | | - className='absolute inset-[5px] rounded-[3px]' |
| 81 | + key={`${left}-${top}`} |
| 82 | + className='absolute size-[20px] rounded-[5px] border border-[var(--border-1)]' |
71 | 83 | style={{ |
| 84 | + left, |
| 85 | + top, |
72 | 86 | background: |
73 | | - index === 1 |
74 | | - ? 'color-mix(in srgb, var(--brand-knowledge) 34%, transparent)' |
75 | | - : 'var(--surface-5)', |
| 87 | + left === CHUNK_COLUMNS[0] && CONNECTED_ROWS.has(top) ? INK.chunk : undefined, |
76 | 88 | }} |
77 | 89 | /> |
78 | | - </span> |
79 | | - ))} |
80 | | - </Vignette> |
| 90 | + )) |
| 91 | + )} |
| 92 | + </div> |
81 | 93 | ) |
82 | 94 | } |
83 | 95 |
|
| 96 | +const KNOWLEDGE_DOCS_URL = 'https://docs.sim.ai/knowledgebase' |
| 97 | + |
| 98 | +interface KnowledgeEmptyStateProps { |
| 99 | + /** Opens the create-base modal — the same action the header's primary chip runs. */ |
| 100 | + onCreate: () => void |
| 101 | + /** Mirrors the header chip's disabled state: no edit rights on the workspace. */ |
| 102 | + createDisabled?: boolean |
| 103 | +} |
| 104 | + |
84 | 105 | /** Empty state for the knowledge bases list when the workspace has none. */ |
85 | | -export function KnowledgeEmptyState() { |
| 106 | +export function KnowledgeEmptyState({ |
| 107 | + onCreate, |
| 108 | + createDisabled = false, |
| 109 | +}: KnowledgeEmptyStateProps) { |
86 | 110 | return ( |
87 | 111 | <EmptyState |
88 | 112 | graphic={<KnowledgeGraphic />} |
89 | 113 | title='No knowledge bases yet' |
90 | 114 | description='Upload documents to give your agents a memory they can search.' |
| 115 | + action={ |
| 116 | + <div className='flex items-center gap-2'> |
| 117 | + <ChipLink |
| 118 | + href={KNOWLEDGE_DOCS_URL} |
| 119 | + target='_blank' |
| 120 | + rel='noopener noreferrer' |
| 121 | + variant='border' |
| 122 | + leftIcon={BookOpen} |
| 123 | + > |
| 124 | + Docs |
| 125 | + </ChipLink> |
| 126 | + <Chip variant='primary' onClick={onCreate} disabled={createDisabled} leftIcon={Plus}> |
| 127 | + New base |
| 128 | + </Chip> |
| 129 | + </div> |
| 130 | + } |
91 | 131 | /> |
92 | 132 | ) |
93 | 133 | } |
0 commit comments