Skip to content

Commit 7302cdd

Browse files
committed
feat: improved initial loading states
1 parent 35c9142 commit 7302cdd

File tree

6 files changed

+270
-33
lines changed

6 files changed

+270
-33
lines changed

examples/react-chatbot/components/AIChatApp/AIChatApp.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
import { Chat, useCreateChatClient, useChatContext } from 'stream-chat-react';
1212
import { Sidebar } from '../Sidebar';
1313
import { ChatContainer } from '../ChatContainer';
14+
import { LoadingScreen } from '../LoadingScreen';
1415
import './AIChatApp.scss';
1516

1617
interface AIChatAppProps {
@@ -95,24 +96,53 @@ export const AIChatApp = ({
9596
sort,
9697
initialChannelId,
9798
}: AIChatAppProps) => {
99+
const [minTimeElapsed, setMinTimeElapsed] = useState(false);
100+
const [isFadingOut, setIsFadingOut] = useState(false);
101+
const [showLoadingScreen, setShowLoadingScreen] = useState(true);
102+
98103
const chatClient = useCreateChatClient({
99104
apiKey,
100105
tokenOrProvider: userToken,
101106
userData: { id: userId },
102107
});
103108

104-
if (!chatClient) return <>Loading...</>;
109+
// Ensure loading screen shows for at least 750ms
110+
useEffect(() => {
111+
const timer = setTimeout(() => {
112+
setMinTimeElapsed(true);
113+
}, 750);
114+
115+
return () => clearTimeout(timer);
116+
}, []);
117+
118+
// Handle fade-out when both conditions are met
119+
useEffect(() => {
120+
if (chatClient && minTimeElapsed && !isFadingOut) {
121+
// Start fade-out animation
122+
setIsFadingOut(true);
123+
124+
// Remove loading screen after animation completes (400ms)
125+
const fadeOutTimer = setTimeout(() => {
126+
setShowLoadingScreen(false);
127+
}, 400);
128+
129+
return () => clearTimeout(fadeOutTimer);
130+
}
131+
}, [chatClient, minTimeElapsed, isFadingOut]);
105132

106133
return (
107134
<div className="ai-demo-app">
108-
<Chat client={chatClient} isMessageAIGenerated={isMessageAIGenerated}>
109-
<ChatContent
110-
filters={filters}
111-
options={options}
112-
sort={sort}
113-
initialChannelId={initialChannelId}
114-
/>
115-
</Chat>
135+
{chatClient && (
136+
<Chat client={chatClient} isMessageAIGenerated={isMessageAIGenerated}>
137+
<ChatContent
138+
filters={filters}
139+
options={options}
140+
sort={sort}
141+
initialChannelId={initialChannelId}
142+
/>
143+
</Chat>
144+
)}
145+
{showLoadingScreen && <LoadingScreen isFadingOut={isFadingOut} />}
116146
</div>
117147
);
118148
};
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
.ai-demo-loading {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
height: 100%;
7+
display: flex;
8+
align-items: center;
9+
justify-content: center;
10+
background-color: var(--ai-demo-bg-primary);
11+
color: var(--ai-demo-text-primary);
12+
z-index: 1000;
13+
opacity: 1;
14+
transition: opacity 400ms ease-out;
15+
16+
&--fading-out {
17+
opacity: 0;
18+
}
19+
}
20+
21+
.ai-demo-loading__content {
22+
display: flex;
23+
flex-direction: column;
24+
align-items: center;
25+
gap: 1.5rem;
26+
padding: 2rem;
27+
max-width: 400px;
28+
text-align: center;
29+
}
30+
31+
.ai-demo-loading__icon-container {
32+
position: relative;
33+
width: 80px;
34+
height: 80px;
35+
display: flex;
36+
align-items: center;
37+
justify-content: center;
38+
}
39+
40+
.ai-demo-loading__icon {
41+
font-size: 48px;
42+
color: var(--ai-demo-accent);
43+
z-index: 1;
44+
animation: float 3s ease-in-out infinite;
45+
}
46+
47+
.ai-demo-loading__pulse {
48+
position: absolute;
49+
width: 80px;
50+
height: 80px;
51+
border-radius: 50%;
52+
background-color: var(--ai-demo-accent);
53+
opacity: 0.2;
54+
animation: pulse 2s ease-in-out infinite;
55+
}
56+
57+
.ai-demo-loading__title {
58+
font-size: 2rem;
59+
font-weight: 600;
60+
margin: 0;
61+
color: var(--ai-demo-text-primary);
62+
letter-spacing: -0.02em;
63+
}
64+
65+
.ai-demo-loading__subtitle {
66+
font-size: 1rem;
67+
margin: 0;
68+
color: var(--ai-demo-text-secondary);
69+
font-weight: 400;
70+
}
71+
72+
.ai-demo-loading__dots {
73+
display: flex;
74+
gap: 0.5rem;
75+
margin-top: 0.5rem;
76+
}
77+
78+
.ai-demo-loading__dot {
79+
width: 8px;
80+
height: 8px;
81+
border-radius: 50%;
82+
background-color: var(--ai-demo-accent);
83+
animation: bounce 1.4s ease-in-out infinite;
84+
85+
&:nth-child(1) {
86+
animation-delay: 0s;
87+
}
88+
89+
&:nth-child(2) {
90+
animation-delay: 0.2s;
91+
}
92+
93+
&:nth-child(3) {
94+
animation-delay: 0.4s;
95+
}
96+
}
97+
98+
@keyframes pulse {
99+
0%,
100+
100% {
101+
transform: scale(1);
102+
opacity: 0.2;
103+
}
104+
50% {
105+
transform: scale(1.3);
106+
opacity: 0.1;
107+
}
108+
}
109+
110+
@keyframes float {
111+
0%,
112+
100% {
113+
transform: translateY(0px);
114+
}
115+
50% {
116+
transform: translateY(-10px);
117+
}
118+
}
119+
120+
@keyframes bounce {
121+
0%,
122+
80%,
123+
100% {
124+
transform: scale(0.8);
125+
opacity: 0.5;
126+
}
127+
40% {
128+
transform: scale(1.2);
129+
opacity: 1;
130+
}
131+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import './LoadingScreen.scss';
2+
3+
interface LoadingScreenProps {
4+
isFadingOut?: boolean;
5+
}
6+
7+
export const LoadingScreen = ({ isFadingOut = false }: LoadingScreenProps) => {
8+
return (
9+
<div
10+
className={`ai-demo-loading ${isFadingOut ? 'ai-demo-loading--fading-out' : ''}`}
11+
>
12+
<div className="ai-demo-loading__content">
13+
<div className="ai-demo-loading__icon-container">
14+
<span className="material-symbols-rounded ai-demo-loading__icon">
15+
auto_awesome
16+
</span>
17+
<div className="ai-demo-loading__pulse" />
18+
</div>
19+
<h1 className="ai-demo-loading__title">Stream AI Chat</h1>
20+
<p className="ai-demo-loading__subtitle">
21+
Initializing Stream AI assistant...
22+
</p>
23+
<div className="ai-demo-loading__dots">
24+
<span className="ai-demo-loading__dot"></span>
25+
<span className="ai-demo-loading__dot"></span>
26+
<span className="ai-demo-loading__dot"></span>
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { LoadingScreen } from './LoadingScreen';
Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
'use client';
22

3-
import { useEffect, useState } from 'react';
3+
import { useEffect } from 'react';
44
import { useRouter, useSearchParams } from 'next/navigation';
55
import { getUserId } from './rateLimitUtils';
66

77
/**
88
* Client component that handles user initialization and URL synchronization
99
*/
1010
export const UserProvider = ({ children }: { children: React.ReactNode }) => {
11-
const [isInitialized, setIsInitialized] = useState(false);
1211
const router = useRouter();
1312
const searchParams = useSearchParams();
1413

@@ -22,13 +21,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
2221
params.set('user_id', currentUserId);
2322
router.replace(`?${params.toString()}`);
2423
}
25-
26-
setIsInitialized(true);
2724
}, [router, searchParams]);
2825

29-
if (!isInitialized) {
30-
return <>Loading...</>;
31-
}
32-
33-
return <>{children}</>;
26+
return children;
3427
};

0 commit comments

Comments
 (0)