Skip to content
This repository was archived by the owner on Mar 29, 2026. It is now read-only.

Commit 4c5f704

Browse files
committed
feat: add TopLoader for loading
1 parent 57e0dfe commit 4c5f704

6 files changed

Lines changed: 125 additions & 24 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { motion } from 'framer-motion';
3+
4+
interface TopLoaderProps {
5+
isLoading: boolean;
6+
}
7+
8+
const TopLoader: React.FC<TopLoaderProps> = ({ isLoading }) => {
9+
const [isVisible, setIsVisible] = useState(isLoading);
10+
11+
// Sinkronkan isVisible dengan isLoading, tapi dengan kontrol untuk animasi keluar
12+
useEffect(() => {
13+
if (isLoading) {
14+
setIsVisible(true);
15+
}
16+
}, [isLoading]);
17+
18+
const handleAnimationComplete = () => {
19+
if (!isLoading) {
20+
setIsVisible(false); // Sembunyikan setelah animasi keluar selesai
21+
}
22+
};
23+
24+
return (
25+
<div className="fixed left-0 right-0 top-0 z-50 h-1 overflow-hidden">
26+
{isVisible && (
27+
<motion.div
28+
className="h-dvh bg-gradient-to-r from-black to-neutral-800 dark:from-white dark:to-neutral-200"
29+
initial={{ x: '-100%' }}
30+
animate={
31+
isLoading
32+
? { x: '200%' } // Bergerak ke kanan saat loading
33+
: { x: '200%', opacity: 0 } // Keluar ke kanan sambil memudar saat selesai
34+
}
35+
transition={
36+
isLoading
37+
? {
38+
x: {
39+
repeat: Infinity,
40+
repeatType: 'loop',
41+
duration: 1.5,
42+
ease: 'linear',
43+
},
44+
}
45+
: {
46+
x: {
47+
duration: 0.5, // Durasi keluar
48+
ease: 'easeOut',
49+
},
50+
opacity: {
51+
duration: 0.3, // Fade out lebih cepat
52+
ease: 'easeOut',
53+
},
54+
}
55+
}
56+
style={{
57+
width: '50%',
58+
}}
59+
onAnimationComplete={handleAnimationComplete}
60+
/>
61+
)}
62+
</div>
63+
);
64+
};
65+
66+
export default TopLoader;

apps/web/src/components/chat/ChatScreen.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React, { lazy, useCallback, useEffect, useRef, useState } from 'react';
22
import { useQuery } from '@tanstack/react-query';
3-
import { ChevronDown, ChevronUp } from 'lucide-react';
3+
import { ArrowDown, ChevronDown, ChevronUp } from 'lucide-react';
44
import { useParams } from 'react-router';
55

66
import { Avatar, AvatarImage } from '@/components/ui/avatar';
77
import { useChatStore } from '@/hooks/useChat';
88
import { API } from '@/lib/api';
99
import { ComponentLoader } from '../ComponentLoader';
10+
import { Button } from '../ui/button';
1011

1112
const Markdown = lazy(() => import('react-markdown'));
1213
const AiThinking = lazy(() => import('@/components/AiThinking'));
@@ -26,6 +27,7 @@ export default function ChatScreen() {
2627
? JSON.parse(window.localStorage.getItem('thinkingState')!)
2728
: {}
2829
);
30+
const [showScrollButton, setShowScrollButton] = useState(false);
2931

3032
const toggleThinking = (id: string) => {
3133
setThinkingOpen((prev) => {
@@ -108,14 +110,15 @@ export default function ChatScreen() {
108110
scrollContainerRef.current;
109111
const isAtBottom = scrollHeight - scrollTop - clientHeight < 1;
110112
setShouldAutoScroll(isAtBottom);
113+
setShowScrollButton(!isAtBottom);
111114
}
112115
}, [scrollContainerRef, setShouldAutoScroll]);
113116

114117
return (
115118
<div
116119
ref={scrollContainerRef}
117120
onScroll={handleScroll}
118-
className="custom-scrollbar flex-1 overflow-y-auto">
121+
className="custom-scrollbar relative flex-1 overflow-y-auto">
119122
{isError && <div>{error.message}</div>}
120123

121124
<div className="mx-auto w-full max-w-3xl px-4 pt-4">
@@ -243,6 +246,30 @@ export default function ChatScreen() {
243246
)}
244247
<div ref={messagesEndRef} />
245248
</div>
249+
<ScrollToBottomButton
250+
scrollToBottom={scrollToBottom}
251+
isVisible={showScrollButton}
252+
/>
246253
</div>
247254
);
248255
}
256+
257+
const ScrollToBottomButton = ({
258+
scrollToBottom,
259+
isVisible,
260+
}: {
261+
scrollToBottom: () => void;
262+
isVisible: boolean;
263+
}) => {
264+
return (
265+
<Button
266+
variant="outline"
267+
size="circle"
268+
className={`sticky bottom-2 left-1/2 -translate-x-1/2 transition-opacity duration-300 ${
269+
isVisible ? 'opacity-100' : 'pointer-events-none opacity-0'
270+
}`}
271+
onClick={scrollToBottom}>
272+
<ArrowDown />
273+
</Button>
274+
);
275+
};

apps/web/src/components/chat/ChatWindow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
7979
};
8080

8181
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
82-
if (e.key === 'Enter' && !e.shiftKey) {
83-
e.preventDefault();
82+
if (e.key === 'Enter' && e.ctrlKey) {
83+
// e.preventDefault();
8484
onSubmit(e as unknown as React.FormEvent<HTMLFormElement>);
8585
}
8686
};

apps/web/src/layout/ChatLayout.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import { lazy, Suspense } from 'react';
2+
import { useIsFetching, useIsMutating } from '@tanstack/react-query';
23
import { Outlet } from 'react-router';
34

4-
import { ComponentLoader } from '@/components/ComponentLoader';
5+
import TopLoader from '@/components/TopLoader';
56
import { SidebarProvider } from '@/components/ui/sidebar';
67

78
const ChatSidebar = lazy(() => import('@/components/chat/sidebar/ChatSidebar'));
89

910
export default function ChatLayout() {
11+
const isMutating = useIsMutating();
12+
const isFetching = useIsFetching();
1013
return (
1114
<SidebarProvider>
12-
<div className="flex max-h-screen w-full overflow-hidden">
13-
<Suspense fallback={<ComponentLoader />}>
15+
<div className="flex max-h-dvh w-full overflow-hidden">
16+
<Suspense fallback={<TopLoader isLoading />}>
1417
<ChatSidebar />
1518
</Suspense>
1619
<main className="w-full min-w-0 max-w-full flex-1">
1720
<Outlet />
1821
</main>
1922
</div>
23+
<TopLoader isLoading={isMutating > 0 || isFetching > 0} />
2024
</SidebarProvider>
2125
);
2226
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { lazy, Suspense } from 'react';
2+
import { useIsFetching, useIsMutating } from '@tanstack/react-query';
23
import { Outlet } from 'react-router';
34

4-
import { ComponentLoader } from '@/components/ComponentLoader';
5+
import TopLoader from '@/components/TopLoader';
56
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
67

78
const AppSidebar = lazy(() => import('@/components/dashboard/sidebar/Sidebar'));
89
const Header = lazy(() => import('@/components/dashboard/sidebar/Header'));
910

1011
export default function DashboardLayout() {
12+
const isMutating = useIsMutating();
13+
const isFetching = useIsFetching();
1114
return (
1215
<SidebarProvider>
13-
<Suspense fallback={<ComponentLoader />}>
16+
<Suspense fallback={<TopLoader isLoading />}>
1417
<AppSidebar />
1518
</Suspense>
1619
<SidebarInset>
@@ -19,6 +22,7 @@ export default function DashboardLayout() {
1922
</Suspense>
2023
<Outlet />
2124
</SidebarInset>
25+
<TopLoader isLoading={isMutating > 0 || isFetching > 0} />
2226
</SidebarProvider>
2327
);
2428
}

apps/web/src/routes.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { lazy, Suspense } from 'react';
22
import { Route, Routes } from 'react-router';
33

4-
import { ComponentLoader } from '@/components/ComponentLoader';
4+
import TopLoader from '@/components/TopLoader';
55

66
const NotFound = lazy(() => import('@/components/NotFound'));
77
const ChatLayout = lazy(() => import('@/layout/ChatLayout'));
@@ -26,7 +26,7 @@ export function AppRoutes() {
2626
<Route
2727
index
2828
element={
29-
<Suspense fallback={<ComponentLoader />}>
29+
<Suspense fallback={<TopLoader isLoading />}>
3030
<HomePage />
3131
</Suspense>
3232
}
@@ -35,22 +35,22 @@ export function AppRoutes() {
3535
<Route
3636
path="chat"
3737
element={
38-
<Suspense fallback={<ComponentLoader />}>
38+
<Suspense fallback={<TopLoader isLoading />}>
3939
<ChatLayout />
4040
</Suspense>
4141
}>
4242
<Route
4343
index
4444
element={
45-
<Suspense fallback={<ComponentLoader />}>
45+
<Suspense fallback={<TopLoader isLoading />}>
4646
<NewChatPage />
4747
</Suspense>
4848
}
4949
/>
5050
<Route
5151
path=":conversationId"
5252
element={
53-
<Suspense fallback={<ComponentLoader />}>
53+
<Suspense fallback={<TopLoader isLoading />}>
5454
<ChatPage />
5555
</Suspense>
5656
}
@@ -68,29 +68,29 @@ export function AppRoutes() {
6868
<Route
6969
path="dashboard"
7070
element={
71-
<Suspense fallback={<ComponentLoader />}>
71+
<Suspense fallback={<TopLoader isLoading />}>
7272
<DashboardLayout />
7373
</Suspense>
7474
}>
7575
<Route
7676
index
7777
element={
78-
<Suspense fallback={<ComponentLoader />}>
78+
<Suspense fallback={<TopLoader isLoading />}>
7979
<OverviewPage />
8080
</Suspense>
8181
}
8282
/>
8383
<Route
8484
path="conversations"
8585
element={
86-
<Suspense fallback={<ComponentLoader />}>
86+
<Suspense fallback={<TopLoader isLoading />}>
8787
<ConversationsPage />
8888
</Suspense>
8989
}>
9090
<Route
9191
path=":id"
9292
element={
93-
<Suspense fallback={<ComponentLoader />}>
93+
<Suspense fallback={<TopLoader isLoading />}>
9494
<ConversationsPage />
9595
</Suspense>
9696
}
@@ -99,39 +99,39 @@ export function AppRoutes() {
9999
<Route
100100
path="agents"
101101
element={
102-
<Suspense fallback={<ComponentLoader />}>
102+
<Suspense fallback={<TopLoader isLoading />}>
103103
<AgentsPage />
104104
</Suspense>
105105
}
106106
/>
107107
<Route
108108
path="knowledge"
109109
element={
110-
<Suspense fallback={<ComponentLoader />}>
110+
<Suspense fallback={<TopLoader isLoading />}>
111111
<KnowledgeBasePage />
112112
</Suspense>
113113
}
114114
/>
115115
<Route
116116
path="integrations"
117117
element={
118-
<Suspense fallback={<ComponentLoader />}>
118+
<Suspense fallback={<TopLoader isLoading />}>
119119
<IntegrationsPage />
120120
</Suspense>
121121
}
122122
/>
123123
<Route
124124
path="builder"
125125
element={
126-
<Suspense fallback={<ComponentLoader />}>
126+
<Suspense fallback={<TopLoader isLoading />}>
127127
<BuilderPage />
128128
</Suspense>
129129
}
130130
/>
131131
<Route
132132
path="settings"
133133
element={
134-
<Suspense fallback={<ComponentLoader />}>
134+
<Suspense fallback={<TopLoader isLoading />}>
135135
<SettingsPage />
136136
</Suspense>
137137
}
@@ -142,7 +142,7 @@ export function AppRoutes() {
142142
<Route
143143
path="*"
144144
element={
145-
<Suspense fallback={<ComponentLoader />}>
145+
<Suspense fallback={<TopLoader isLoading />}>
146146
<NotFound />
147147
</Suspense>
148148
}

0 commit comments

Comments
 (0)