Skip to content

Commit f721347

Browse files
authored
Better empty state for Assistant (#20214)
Signed-off-by: B-Step62 <yuki.watanabe@databricks.com>
1 parent 2071332 commit f721347

File tree

5 files changed

+585
-18
lines changed

5 files changed

+585
-18
lines changed

mlflow/server/js/src/assistant/AssistantChatPanel.tsx

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { AssistantSetupWizard } from './setup';
3030
import { GenAIMarkdownRenderer } from '../shared/web-shared/genai-markdown-renderer';
3131
import { useCopyController } from '../shared/web-shared/snippet/hooks/useCopyController';
3232
import { useAssistantPrompts } from '../common/utils/RoutingUtils';
33+
import { AssistantWelcomeCarousel } from './AssistantWelcomeCarousel';
3334

3435
type CurrentView = 'chat' | 'setup-wizard' | 'settings';
3536

@@ -470,7 +471,7 @@ const RemoteServerMessage = ({ onClose }: { onClose: () => void }) => {
470471

471472
/**
472473
* Setup prompt shown when assistant is not set up yet.
473-
* Shows description and setup button.
474+
* Shows empty state illustration and setup button.
474475
*/
475476
const SetupPrompt = ({ onSetup }: { onSetup: () => void }) => {
476477
const { theme } = useDesignSystemTheme();
@@ -483,23 +484,11 @@ const SetupPrompt = ({ onSetup }: { onSetup: () => void }) => {
483484
alignItems: 'center',
484485
justifyContent: 'center',
485486
flex: 1,
486-
padding: theme.spacing.lg,
487-
paddingBottom: theme.spacing.lg * 4,
488-
gap: theme.spacing.lg,
487+
padding: theme.spacing.sm,
488+
gap: theme.spacing.md,
489489
}}
490490
>
491-
<WrenchSparkleIcon color="ai" css={{ fontSize: 64, opacity: 0.75 }} />
492-
493-
<Typography.Text
494-
color="secondary"
495-
css={{
496-
fontSize: theme.typography.fontSizeMd,
497-
textAlign: 'center',
498-
maxWidth: 400,
499-
}}
500-
>
501-
Ask questions about your experiments, traces, evaluations, and more.
502-
</Typography.Text>
491+
<AssistantWelcomeCarousel />
503492

504493
<Button componentId="mlflow.assistant.chat_panel.setup" type="primary" onClick={onSetup}>
505494
Get Started
@@ -571,12 +560,15 @@ export const AssistantChatPanel = () => {
571560
);
572561
case 'chat':
573562
default:
574-
return setupComplete ? <ChatPanelContent /> : <SetupPrompt onSetup={handleStartSetup} />;
563+
if (!setupComplete) {
564+
return <SetupPrompt onSetup={handleStartSetup} />;
565+
}
566+
return <ChatPanelContent />;
575567
}
576568
};
577569

578570
// Determine if we should show the chat controls (new chat button)
579-
const showChatControls = Boolean(experimentId) && setupComplete && currentView === 'chat';
571+
const showChatControls = setupComplete && currentView === 'chat';
580572

581573
return (
582574
<div
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* Assistant Welcome Carousel component.
3+
* Displays a carousel of feature images with dot navigation.
4+
*/
5+
6+
import { useEffect, useState } from 'react';
7+
import { ChevronLeftIcon, ChevronRightIcon, Typography, useDesignSystemTheme } from '@databricks/design-system';
8+
9+
import assistantDebugImg from '../common/static/assistant-debug.svg';
10+
import assistantEvalImg from '../common/static/assistant-evaluation.svg';
11+
import assistantTrendsImg from '../common/static/assistant-trends.svg';
12+
13+
interface CarouselSlide {
14+
image: string;
15+
title: string;
16+
description: string;
17+
}
18+
19+
const slides: CarouselSlide[] = [
20+
{
21+
image: assistantDebugImg,
22+
title: 'Debug Issues',
23+
description: 'Ask questions about errors, identify root causes, and get actionable fixes for failed traces.',
24+
},
25+
{
26+
image: assistantEvalImg,
27+
title: 'Set Up Evaluations',
28+
description: 'Configure evaluation criteria, run assessments on your agents, and track quality metrics.',
29+
},
30+
{
31+
image: assistantTrendsImg,
32+
title: 'Analyze Trends',
33+
description: 'Explore metrics trends, uncover optimization opportunities, and get insights on your experiments.',
34+
},
35+
];
36+
37+
const TRANSITION_DURATION_MS = 400;
38+
39+
export const AssistantWelcomeCarousel = () => {
40+
const { theme } = useDesignSystemTheme();
41+
const [currentIndex, setCurrentIndex] = useState(0);
42+
const [isTransitioning, setIsTransitioning] = useState(false);
43+
44+
const extendedSlides = [...slides, slides[0]];
45+
const displaySlideIndex = currentIndex % slides.length;
46+
47+
// Reset transitioning state after animation completes
48+
useEffect(() => {
49+
if (!isTransitioning) return;
50+
51+
const timeout = setTimeout(() => {
52+
// If we're at the clone (index 3), reset to first slide instantly
53+
if (currentIndex === slides.length) {
54+
setIsTransitioning(false);
55+
setCurrentIndex(0);
56+
} else {
57+
setIsTransitioning(false);
58+
}
59+
}, TRANSITION_DURATION_MS);
60+
return () => clearTimeout(timeout);
61+
}, [isTransitioning, currentIndex]);
62+
63+
const handleNextSlide = () => {
64+
if (isTransitioning) return;
65+
setIsTransitioning(true);
66+
setCurrentIndex((prev) => prev + 1);
67+
};
68+
69+
const handlePrevSlide = () => {
70+
if (isTransitioning) return;
71+
setIsTransitioning(true);
72+
setCurrentIndex((prev) => (prev - 1 + slides.length) % slides.length);
73+
};
74+
75+
const handleDotClick = (index: number) => {
76+
if (isTransitioning) return;
77+
setIsTransitioning(true);
78+
setCurrentIndex(index);
79+
};
80+
81+
const currentSlideData = slides[displaySlideIndex];
82+
83+
return (
84+
<div
85+
css={{
86+
display: 'flex',
87+
flexDirection: 'column',
88+
alignItems: 'center',
89+
gap: theme.spacing.lg,
90+
width: '100%',
91+
}}
92+
>
93+
{/* Welcome header */}
94+
<div
95+
css={{
96+
textAlign: 'center',
97+
display: 'flex',
98+
flexDirection: 'column',
99+
gap: theme.spacing.sm,
100+
padding: `0 ${theme.spacing.lg}px`,
101+
}}
102+
>
103+
<Typography.Title level={3} css={{ margin: 0 }}>
104+
Welcome to MLflow Assistant
105+
</Typography.Title>
106+
</div>
107+
108+
{/* Carousel container */}
109+
<div
110+
css={{
111+
position: 'relative',
112+
width: '100%',
113+
maxWidth: 480,
114+
}}
115+
>
116+
{/* Image area with arrow buttons */}
117+
<div
118+
css={{
119+
display: 'flex',
120+
alignItems: 'center',
121+
gap: theme.spacing.sm,
122+
}}
123+
>
124+
{/* Left arrow */}
125+
<button
126+
onClick={handlePrevSlide}
127+
aria-label="Previous slide"
128+
css={{
129+
display: 'flex',
130+
alignItems: 'center',
131+
justifyContent: 'center',
132+
width: theme.spacing.lg * 1.5,
133+
height: theme.spacing.lg * 1.5,
134+
borderRadius: '50%',
135+
border: 'none',
136+
backgroundColor: 'transparent',
137+
color: theme.colors.textSecondary,
138+
cursor: 'pointer',
139+
flexShrink: 0,
140+
transition: 'all 0.2s ease',
141+
'&:hover': {
142+
backgroundColor: theme.colors.actionDefaultBackgroundHover,
143+
color: theme.colors.textPrimary,
144+
},
145+
}}
146+
>
147+
<ChevronLeftIcon />
148+
</button>
149+
150+
{/* Slides */}
151+
<div
152+
css={{
153+
flex: 1,
154+
overflow: 'hidden',
155+
}}
156+
>
157+
<div
158+
css={{
159+
display: 'flex',
160+
transition: isTransitioning ? `transform ${TRANSITION_DURATION_MS}ms ease-in-out` : 'none',
161+
transform: `translateX(-${currentIndex * 100}%)`,
162+
}}
163+
>
164+
{extendedSlides.map((slide, index) => (
165+
<div
166+
key={`${slide.title}-${index}`}
167+
css={{
168+
flexShrink: 0,
169+
width: '100%',
170+
}}
171+
>
172+
<img
173+
src={slide.image}
174+
alt={slide.title}
175+
css={{
176+
width: '100%',
177+
height: 'auto',
178+
}}
179+
/>
180+
</div>
181+
))}
182+
</div>
183+
</div>
184+
185+
{/* Right arrow */}
186+
<button
187+
onClick={handleNextSlide}
188+
aria-label="Next slide"
189+
css={{
190+
display: 'flex',
191+
alignItems: 'center',
192+
justifyContent: 'center',
193+
width: theme.spacing.lg * 1.5,
194+
height: theme.spacing.lg * 1.5,
195+
borderRadius: '50%',
196+
border: 'none',
197+
backgroundColor: 'transparent',
198+
color: theme.colors.textSecondary,
199+
cursor: 'pointer',
200+
flexShrink: 0,
201+
transition: 'all 0.2s ease',
202+
'&:hover': {
203+
backgroundColor: theme.colors.actionDefaultBackgroundHover,
204+
color: theme.colors.textPrimary,
205+
},
206+
}}
207+
>
208+
<ChevronRightIcon />
209+
</button>
210+
</div>
211+
212+
{/* Slide title and description with numbering */}
213+
<div
214+
css={{
215+
textAlign: 'center',
216+
marginTop: theme.spacing.md,
217+
padding: `0 32px`,
218+
}}
219+
>
220+
<Typography.Text bold css={{ display: 'block', marginBottom: theme.spacing.xs }}>
221+
{displaySlideIndex + 1}. {currentSlideData.title}
222+
</Typography.Text>
223+
<Typography.Text color="secondary">{currentSlideData.description}</Typography.Text>
224+
</div>
225+
</div>
226+
227+
{/* Dot indicators */}
228+
<div
229+
css={{
230+
display: 'flex',
231+
gap: theme.spacing.sm,
232+
justifyContent: 'center',
233+
}}
234+
>
235+
{slides.map((slide, index) => (
236+
<button
237+
key={slide.title}
238+
onClick={() => handleDotClick(index)}
239+
aria-label={`Go to slide ${index + 1}: ${slide.title}`}
240+
css={{
241+
width: theme.spacing.sm,
242+
height: theme.spacing.sm,
243+
borderRadius: '50%',
244+
border: 'none',
245+
backgroundColor: theme.colors.textSecondary,
246+
opacity: index === displaySlideIndex ? 0.7 : 0.3,
247+
padding: 0,
248+
cursor: 'pointer',
249+
transition: 'all 0.2s ease',
250+
'&:hover': {
251+
opacity: 0.7,
252+
},
253+
}}
254+
/>
255+
))}
256+
</div>
257+
</div>
258+
);
259+
};

0 commit comments

Comments
 (0)