Skip to content

Commit d51a7cc

Browse files
andresdjassoclaude
authored andcommitted
feat(resources): empty-state graphics for knowledge, tables, logs, files, skills
Four of the resource pages (knowledge, tables, logs, files) had no empty state at all — `Resource.Table` painted column headers over a blank scroll area and stopped there. Skills had a `: null` branch for zero data. Adds a graphic per resource, drawn in the editor vignette's recipe: take the product's own primitives, shrink them, strip the content to skeletons, and let the composition bleed off the frame edges. - Knowledge — a document fanning into the chunks it is embedded as, using the editor's 6px smooth-step connector language in --workflow-edge - Tables — a sheet of cells running off two edges with one cell in an edit ring - Logs — runs stacked newest-first, their trace spans staggered into a waterfall - Files — a folder held open with one file still above its dashed landing slot - Skills — a skill card opened far enough to show the tools bundled inside `Resource.Table` gains a sanctioned `emptyState` slot rendered below the column headers when `rows` is empty, so the chrome guarantee still holds. Each page shows the graphic only for true zero-data — never for a search or filter that matched nothing, never inside an empty subfolder, and (logs) never before the first page of runs lands. Also ports the shared `EmptyState` frame from the editor branch so this branch stands alone, and adds a review-only /empty-states-preview gallery route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 10ff622 commit d51a7cc

16 files changed

Lines changed: 558 additions & 2 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ResourceEmptyStateGallery } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/gallery-preview'
2+
3+
/** Review-only route for the resource empty-state graphics. Not linked in nav. */
4+
export default function EmptyStatesPreviewPage() {
5+
return <ResourceEmptyStateGallery />
6+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
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+
7+
/** Name and size skeleton widths for the files already sitting in the folder. */
8+
const FOLDER_FILES = [
9+
{ name: 84, size: 22 },
10+
{ name: 62, size: 18 },
11+
] as const
12+
13+
/**
14+
* A folder held open with one file still in the air above its dashed landing
15+
* slot — Files is the one resource whose empty state is really an invitation to
16+
* drop something, so the graphic draws the gesture rather than the result.
17+
*/
18+
function FilesGraphic() {
19+
return (
20+
<Vignette>
21+
<span className='absolute top-[30px] left-[54px] h-[16px] w-[64px] rounded-t-[8px] border-[var(--border-1)] border-x border-t bg-[var(--surface-3)]' />
22+
23+
<div className='absolute top-[42px] left-[54px] h-[92px] w-[208px] rounded-[8px] rounded-tl-none border border-[var(--border-1)] bg-[var(--surface-2)] px-3 pt-3'>
24+
{FOLDER_FILES.map((file) => (
25+
<div key={file.name} className='mb-[10px] flex items-center gap-2'>
26+
<span className='size-[14px] shrink-0 rounded-[3px] bg-[var(--surface-5)]' />
27+
<Bar className='h-2' style={{ width: file.name }} />
28+
<Bar className='ml-auto h-2' style={{ width: file.size }} />
29+
</div>
30+
))}
31+
32+
<span className='block h-[26px] w-full rounded-[6px] border border-[var(--brand-secondary)] border-dashed opacity-60' />
33+
</div>
34+
35+
<div className='-rotate-6 absolute top-[10px] left-[178px] flex h-[36px] w-[108px] items-center gap-2 rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-1)] px-2.5 shadow-lg'>
36+
<span className='size-[16px] shrink-0 rounded-[4px] bg-[color-mix(in_srgb,var(--brand-secondary)_28%,transparent)]' />
37+
<span className='flex flex-col gap-[5px]'>
38+
<Bar className='h-2 w-[54px]' />
39+
<Bar className='h-[6px] w-[32px]' />
40+
</span>
41+
</div>
42+
</Vignette>
43+
)
44+
}
45+
46+
/** Empty state for the files list when the workspace has none. */
47+
export function FilesEmptyState() {
48+
return (
49+
<EmptyState
50+
graphic={<FilesGraphic />}
51+
title='No files yet'
52+
description='Drop files here to share them across your team and every agent.'
53+
/>
54+
)
55+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client'
2+
3+
import {
4+
FilesEmptyState,
5+
KnowledgeEmptyState,
6+
LogsEmptyState,
7+
SkillsEmptyState,
8+
TablesEmptyState,
9+
} from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
10+
11+
const ENTRIES = [
12+
{ label: 'Knowledge', node: <KnowledgeEmptyState /> },
13+
{ label: 'Tables', node: <TablesEmptyState /> },
14+
{ label: 'Logs', node: <LogsEmptyState /> },
15+
{ label: 'Files', node: <FilesEmptyState /> },
16+
{ label: 'Skills', node: <SkillsEmptyState /> },
17+
]
18+
19+
/**
20+
* Review-only gallery: every resource empty state on one page so the family can
21+
* be judged side by side. Not linked from the product.
22+
*/
23+
export function ResourceEmptyStateGallery() {
24+
return (
25+
<div className='grid grid-cols-2 gap-6 bg-[var(--bg)] p-8'>
26+
{ENTRIES.map((entry) => (
27+
<div
28+
key={entry.label}
29+
className='flex min-h-[340px] flex-col rounded-[10px] border border-[var(--border-1)] bg-[var(--surface-1)]'
30+
>
31+
<p className='border-[var(--border-1)] border-b px-4 py-2 text-[var(--text-muted)] text-small'>
32+
{entry.label}
33+
</p>
34+
<div className='flex min-h-0 flex-1 flex-col'>{entry.node}</div>
35+
</div>
36+
))}
37+
</div>
38+
)
39+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { FilesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state'
2+
export { KnowledgeEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/knowledge-empty-state'
3+
export { LogsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/logs-empty-state'
4+
export { SkillsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/skills-empty-state'
5+
export { TablesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state'
6+
export {
7+
Bar,
8+
Vignette,
9+
} from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/vignette'
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
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+
7+
/**
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`).
11+
*/
12+
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',
16+
] as const
17+
18+
function KnowledgeGraphic() {
19+
return (
20+
<Vignette>
21+
<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+
{CHUNK_EDGE_PATHS.map((d) => (
36+
<path
37+
key={d}
38+
d={d}
39+
stroke='url(#knowledge-empty-edge)'
40+
strokeWidth='1.5'
41+
strokeLinecap='round'
42+
strokeLinejoin='round'
43+
/>
44+
))}
45+
</svg>
46+
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+
>
69+
<span
70+
className='absolute inset-[5px] rounded-[3px]'
71+
style={{
72+
background:
73+
index === 1
74+
? 'color-mix(in srgb, var(--brand-knowledge) 34%, transparent)'
75+
: 'var(--surface-5)',
76+
}}
77+
/>
78+
</span>
79+
))}
80+
</Vignette>
81+
)
82+
}
83+
84+
/** Empty state for the knowledge bases list when the workspace has none. */
85+
export function KnowledgeEmptyState() {
86+
return (
87+
<EmptyState
88+
graphic={<KnowledgeGraphic />}
89+
title='No knowledge bases yet'
90+
description='Upload documents to give your agents a memory they can search.'
91+
/>
92+
)
93+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
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+
7+
interface LogRow {
8+
status: 'success' | 'error'
9+
/** Width of the workflow-name skeleton. */
10+
name: number
11+
/** Trace span offset and width — together these read as a waterfall. */
12+
spanStart: number
13+
spanWidth: number
14+
duration: number
15+
}
16+
17+
const LOG_ROWS: LogRow[] = [
18+
{ status: 'success', name: 68, spanStart: 0, spanWidth: 118, duration: 22 },
19+
{ status: 'success', name: 52, spanStart: 12, spanWidth: 82, duration: 18 },
20+
{ status: 'error', name: 74, spanStart: 24, spanWidth: 50, duration: 26 },
21+
{ status: 'success', name: 58, spanStart: 14, spanWidth: 72, duration: 16 },
22+
{ status: 'success', name: 64, spanStart: 4, spanWidth: 102, duration: 20 },
23+
]
24+
25+
const SPAN_ORIGIN_X = 150
26+
27+
/**
28+
* Runs stacked newest-first, each one a status dot, a workflow name, and its
29+
* trace span — the spans stagger into the waterfall you get when you open a run.
30+
*/
31+
function LogsGraphic() {
32+
return (
33+
<Vignette>
34+
{LOG_ROWS.map((row, index) => (
35+
<div
36+
key={`${row.name}-${index}`}
37+
className='absolute left-0 flex h-[24px] w-full items-center'
38+
style={{ top: 12 + index * 26 }}
39+
>
40+
<span
41+
className='absolute size-[6px] rounded-full'
42+
style={{
43+
left: 44,
44+
background: row.status === 'error' ? 'var(--text-error)' : 'var(--text-success)',
45+
}}
46+
/>
47+
<Bar className='absolute h-2' style={{ left: 60, width: row.name }} />
48+
<span
49+
className='absolute h-[10px] rounded-[3px] bg-[var(--surface-5)]'
50+
style={{ left: SPAN_ORIGIN_X + row.spanStart, width: row.spanWidth }}
51+
/>
52+
<Bar className='absolute h-2' style={{ left: 286, width: row.duration }} />
53+
</div>
54+
))}
55+
</Vignette>
56+
)
57+
}
58+
59+
/** Empty state for the logs list when the workspace has no runs yet. */
60+
export function LogsEmptyState() {
61+
return (
62+
<EmptyState
63+
graphic={<LogsGraphic />}
64+
title='No runs yet'
65+
description='Every workflow execution lands here, traced block by block.'
66+
/>
67+
)
68+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
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+
7+
/** Tool-pill widths inside the skill card — a skill is a bundle of tools. */
8+
const TOOL_PILLS = [38, 30, 44] as const
9+
10+
/**
11+
* A skill card over its stack, opened far enough to show the tools bundled
12+
* inside it — the card is the unit you install, the pills are what it brings.
13+
*/
14+
function SkillsGraphic() {
15+
return (
16+
<Vignette>
17+
<span className='absolute top-[40px] left-[92px] h-[84px] w-[152px] rounded-[10px] border border-[var(--border-1)] bg-[var(--surface-1)] opacity-45' />
18+
<span className='absolute top-[34px] left-[86px] h-[84px] w-[160px] rounded-[10px] border border-[var(--border-1)] bg-[var(--surface-2)] opacity-75' />
19+
20+
<div className='absolute top-[26px] left-[76px] h-[92px] w-[168px] rounded-[10px] border border-[var(--border-1)] bg-[var(--surface-2)] p-3.5'>
21+
<div className='flex items-center gap-2.5'>
22+
<span className='size-[26px] shrink-0 rounded-[7px] bg-[var(--surface-5)]' />
23+
<span className='flex flex-col gap-[6px]'>
24+
<Bar className='h-2 w-[72px]' />
25+
<Bar className='h-[6px] w-[46px]' />
26+
</span>
27+
</div>
28+
29+
<div className='mt-3 space-y-[7px]'>
30+
<Bar className='h-2 w-[136px]' />
31+
<Bar className='h-2 w-[104px]' />
32+
</div>
33+
34+
<div className='mt-3 flex items-center gap-1.5'>
35+
{TOOL_PILLS.map((width) => (
36+
<span
37+
key={width}
38+
className='flex h-[14px] items-center gap-1 rounded-full bg-[var(--surface-4)] px-1.5'
39+
style={{ width }}
40+
>
41+
<span className='size-[6px] shrink-0 rounded-full bg-[var(--surface-6)]' />
42+
</span>
43+
))}
44+
</div>
45+
</div>
46+
</Vignette>
47+
)
48+
}
49+
50+
/** Empty state for the skills list when the workspace has none installed. */
51+
export function SkillsEmptyState() {
52+
return (
53+
<EmptyState
54+
graphic={<SkillsGraphic />}
55+
title='No skills yet'
56+
description='Add a skill to give your agents a packaged set of tools to work with.'
57+
/>
58+
)
59+
}

0 commit comments

Comments
 (0)