Skip to content

Commit 911cc40

Browse files
feat: FIT-629: Create a reusable Empty State component (#8278)
Co-authored-by: ricardoantoniocm <[email protected]> Co-authored-by: Andrew <[email protected]>
1 parent e10e09b commit 911cc40

File tree

9 files changed

+1299
-55
lines changed

9 files changed

+1299
-55
lines changed

web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/MachineLearningSettings.jsx

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { useCallback, useContext, useEffect, useState } from "react";
22
import { NavLink } from "react-router-dom";
3-
import { Button, Typography, Spinner } from "@humansignal/ui";
3+
import { Button, Typography, Spinner, EmptyState, SimpleCard } from "@humansignal/ui";
44
import { Form, Label, Toggle } from "../../../components/Form";
55
import { modal } from "../../../components/Modal/Modal";
6-
import { EmptyState } from "../../../components/EmptyState/EmptyState";
7-
import { IconModels } from "@humansignal/icons";
6+
import { IconModels, IconExternal } from "@humansignal/icons";
87
import { useAPI } from "../../../providers/ApiProvider";
98
import { ProjectContext } from "../../../providers/ProjectProvider";
109
import { MachineLearningList } from "./MachineLearningList";
@@ -95,31 +94,48 @@ export const MachineLearningSettings = () => {
9594

9695
return (
9796
<section>
98-
<div className="w-[40rem]">
97+
<div className="w-[42rem]">
9998
<Typography variant="headline" size="medium" className="mb-base">
10099
Model
101100
</Typography>
102101
{loading && <Spinner size={32} />}
103102
{loaded && backends.length === 0 && (
104-
<EmptyState
105-
icon={<IconModels />}
106-
title="Let’s connect your first model"
107-
description="Connect a machine learning model to generate predictions. These predictions can be compared side by side, used for efficient pre‒labeling and, to aid in active learning, directing users to the most impactful labeling tasks."
108-
action={
109-
<Button primary onClick={() => showMLFormModal()} aria-label="Add machine learning model">
110-
Connect Model
111-
</Button>
112-
}
113-
footer={
114-
<div>
115-
Need help?
116-
<br />
117-
<a href="https://labelstud.io/guide/ml" target="_blank" rel="noreferrer">
118-
Learn more about connecting models in our docs
119-
</a>
120-
</div>
121-
}
122-
/>
103+
<SimpleCard title="" className="bg-primary-background border-primary-border-subtler p-base">
104+
<EmptyState
105+
size="medium"
106+
variant="primary"
107+
icon={<IconModels />}
108+
title="Let's connect your first model"
109+
description="Connect a machine learning model to generate live predictions for your project. Compare predictions, accelerate labeling with automatic prelabeling, and direct your team to the most impactful tasks through active learning."
110+
actions={
111+
<Button
112+
variant="primary"
113+
look="filled"
114+
onClick={() => showMLFormModal()}
115+
aria-label="Add machine learning model"
116+
>
117+
Connect Model
118+
</Button>
119+
}
120+
footer={
121+
!window.APP_SETTINGS?.whitelabel_is_active && (
122+
<Typography variant="label" size="small" className="text-primary-link">
123+
<a
124+
href="https://labelstud.io/guide/ml"
125+
target="_blank"
126+
rel="noopener noreferrer"
127+
data-testid="ml-help-link"
128+
aria-label="Learn more about machine learning models (opens in new window)"
129+
className="inline-flex items-center gap-1 hover:underline"
130+
>
131+
Learn more
132+
<IconExternal width={16} height={16} />
133+
</a>
134+
</Typography>
135+
)
136+
}
137+
/>
138+
</SimpleCard>
123139
)}
124140
<MachineLearningList
125141
onEdit={(backend) => showMLFormModal(backend)}

web/apps/labelstudio/src/pages/Settings/PredictionsSettings/PredictionsSettings.jsx

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useContext, useEffect, useState } from "react";
22
import { Divider } from "../../../components/Divider/Divider";
3-
import { EmptyState } from "../../../components/EmptyState/EmptyState";
4-
import { IconPredictions, Typography } from "@humansignal/ui";
3+
import { EmptyState, SimpleCard } from "@humansignal/ui";
4+
import { IconPredictions, Typography, IconExternal } from "@humansignal/ui";
55
import { useAPI } from "../../../providers/ApiProvider";
66
import { ProjectContext } from "../../../providers/ProjectProvider";
77
import { Spinner } from "../../../components/Spinner/Spinner";
@@ -59,20 +59,32 @@ export const PredictionsSettings = () => {
5959
)}
6060

6161
{loaded && versions.length === 0 && (
62-
<EmptyState
63-
icon={<IconPredictions />}
64-
title="No predictions yet uploaded"
65-
description="Predictions could be used to prelabel the data, or validate the model. You can upload and select predictions from multiple model versions. You can also connect live models in the Model tab."
66-
footer={
67-
<div>
68-
Need help?
69-
<br />
70-
<a href="https://labelstud.io/guide/predictions" target="_blank" rel="noreferrer">
71-
Learn more on how to upload predictions in our docs
72-
</a>
73-
</div>
74-
}
75-
/>
62+
<SimpleCard title="" className="bg-primary-background border-primary-border-subtler p-base">
63+
<EmptyState
64+
size="medium"
65+
variant="primary"
66+
icon={<IconPredictions />}
67+
title="No predictions uploaded yet"
68+
description="Upload predictions to automatically prelabel your data and speed up annotation. Import predictions from multiple model versions to compare their performance, or connect live models from the Model page to generate predictions on demand."
69+
footer={
70+
!window.APP_SETTINGS?.whitelabel_is_active && (
71+
<Typography variant="label" size="small" className="text-primary-link">
72+
<a
73+
href="https://labelstud.io/guide/predictions"
74+
target="_blank"
75+
rel="noopener noreferrer"
76+
data-testid="predictions-help-link"
77+
aria-label="Learn more about predictions (opens in new window)"
78+
className="inline-flex items-center gap-1 hover:underline"
79+
>
80+
Learn more
81+
<IconExternal width={16} height={16} />
82+
</a>
83+
</Typography>
84+
)
85+
}
86+
/>
87+
</SimpleCard>
7688
)}
7789

7890
<PredictionsList project={project} versions={versions} fetchVersions={fetchVersions} />
Lines changed: 16 additions & 16 deletions
Loading

web/libs/ui/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./lib/checkbox/checkbox";
1010
export * from "./lib/code-block/code-block";
1111
export * from "./lib/code-block/code-block";
1212
export * from "./lib/code-editor/code-editor";
13+
export * from "./lib/empty-state/empty-state";
1314
export * from "./lib/enterprise-badge/enterprise-badge";
1415
export * from "./lib/label/label";
1516
export * from "./lib/select/select";
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/* Empty State Base Styles */
2+
.base {
3+
@apply w-full h-full flex flex-col items-center justify-center text-center;
4+
}
5+
6+
.icon {
7+
@apply aspect-square p-tight;
8+
}
9+
10+
.title {
11+
@apply text-center;
12+
}
13+
14+
/* Size Styles */
15+
16+
/* Large Size - Data Manager style */
17+
.size-large {
18+
@apply gap-tight;
19+
20+
.icon {
21+
@apply mb-tight;
22+
}
23+
24+
.description {
25+
@apply max-w-xl mb-tight;
26+
}
27+
28+
.actions {
29+
@apply mt-base;
30+
}
31+
32+
.footer {
33+
@apply mt-base;
34+
}
35+
}
36+
37+
/* Medium Size - Home page style */
38+
.size-medium {
39+
@apply gap-tight;
40+
41+
.icon {
42+
@apply mb-tight;
43+
}
44+
45+
.description {
46+
@apply max-w-lg;
47+
}
48+
49+
.additional {
50+
@apply mt-base mb-tight;
51+
}
52+
53+
.actions {
54+
@apply mt-base;
55+
}
56+
57+
.footer {
58+
@apply mt-wide;
59+
}
60+
}
61+
62+
/* Small Size - Sidepanel style */
63+
.size-small {
64+
@apply gap-tighter;
65+
66+
.icon {
67+
@apply mb-tighter;
68+
}
69+
70+
.description {
71+
@apply text-center w-full;
72+
}
73+
74+
/* Text container wrapper for title and description */
75+
.text-wrapper {
76+
@apply flex flex-col items-center w-full gap-tightest;
77+
}
78+
79+
.additional {
80+
@apply mt-tight;
81+
}
82+
83+
.actions {
84+
@apply mt-base;
85+
}
86+
87+
.footer {
88+
@apply pt-tight mt-tight;
89+
}
90+
}
91+
92+
/* Actions Sizes - different max widths for different sizes */
93+
.actions-large {
94+
@apply max-w-lg;
95+
}
96+
97+
.actions-medium {
98+
@apply max-w-md;
99+
}
100+
101+
.actions-small {
102+
@apply max-w-sm;
103+
}
104+
105+
/* Color Variants */
106+
107+
/* Primary Variant - Blue theme */
108+
.variant-primary {
109+
.icon {
110+
@apply bg-primary-emphasis text-primary-icon;
111+
}
112+
}
113+
114+
/* Neutral Variant - Gray theme */
115+
.variant-neutral {
116+
.icon {
117+
@apply bg-neutral-emphasis text-neutral-icon;
118+
}
119+
}
120+
121+
/* Negative Variant - Red theme */
122+
.variant-negative {
123+
.icon {
124+
@apply bg-negative-emphasis text-negative-icon;
125+
}
126+
}
127+
128+
/* Positive Variant - Green theme */
129+
.variant-positive {
130+
.icon {
131+
@apply bg-positive-emphasis text-positive-icon;
132+
}
133+
}
134+
135+
/* Warning Variant - Orange/Yellow theme */
136+
.variant-warning {
137+
.icon {
138+
@apply bg-warning-emphasis text-warning-icon;
139+
}
140+
}
141+
142+
/* Gradient Variant - AI theme with gradient and animation */
143+
.variant-gradient {
144+
.icon {
145+
background: linear-gradient(109deg, var(--color-accent-canteloupe-base) 0%, var(--color-accent-persimmon-base) 51.56%, var(--color-accent-plum-base) 100%), var(--color-accent-grape-subtle);
146+
box-shadow: 0 0 16px 0 rgb(255 166 99 / 50%), 0 0 48px 0 rgb(255 117 87 / 60%), 0 0 128px 0 rgb(227 123 211 / 50%);
147+
animation: pulsate 4s infinite;
148+
149+
@apply text-neutral-on-dark-content;
150+
}
151+
}
152+
153+
@keyframes pulsate {
154+
0% {
155+
transform: scale(1);
156+
opacity: 1;
157+
}
158+
159+
50% {
160+
transform: scale(1.05);
161+
opacity: 0.9;
162+
}
163+
164+
100% {
165+
transform: scale(1);
166+
opacity: 1;
167+
}
168+
}
169+
170+
/* Element Styles - All styling is handled by variants and utility classes */

0 commit comments

Comments
 (0)