FinanceGPT helps you take control of your finances. It offers personalized financial advice, similar to consulting with a human advisor. With features like AI chat and goal planning, FinanceGPT helps you achieve your financial goals. Built with Next.js and AI technology, this open-source template helps developers create their own financial advisory tools. As more people look for smart ways to manage their money, this template lets you build your own financial advisor app quickly and easily
1'use client'
2
3import React, { useState, useEffect, useRef } from 'react';
4import { useSelector, useDispatch } from 'react-redux';
5import { sendMessage, resetChat, getChatHistory } from '@/store/chatSlice';
6import { useToast } from '@chakra-ui/react';
7import ReactMarkdown from 'react-markdown';
8import LandingPage from '../components/LandingPage';
9import {
10 Box,
11 Container,
12 Heading,
13 Text,
14 VStack,
15 Textarea,
16 Button,
17 Flex,
18 Spinner,
19 useColorModeValue,
20 IconButton,
21} from '@chakra-ui/react';
22import { ChevronDownIcon } from '@chakra-ui/icons';
23import { motion, AnimatePresence } from 'framer-motion';
24import { AppDispatch, RootState } from '@/store';
25import OptionButtons from '../components/OptionButtons';
26import TypewriterText from '../components/Typewriter';
27
28// Apply motion to Chakra UI components
29const MotionBox = motion(Box as any);
30const MotionFlex = motion(Flex as any);
31const MotionIconButton = motion(IconButton as any);
32
33export default function Home() {
34 // Access Redux store and component state
35 const dispatch = useDispatch<AppDispatch>();
36 const chatState = useSelector((state: RootState) => state.chat);
37 const { loading, error } = chatState;
38 const [newMessage, setNewMessage] = useState('');
39 const [isTyping, setIsTyping] = useState(false);
40 const [currentTopic, setCurrentTopic] = useState<string | null>(null);
41 const [localMessages, setLocalMessages] = useState<Array<{ role: string; content: string }>>([]);
42 const chatContainerRef = useRef<HTMLDivElement>(null);
43 const [isNavigating, setIsNavigating] = useState(false);
44 const [showScrollButton, setShowScrollButton] = useState(false);
45 const { user } = useSelector((state: RootState) => state.auth);
46
47 const toast = useToast();
48
49 // Define color variables based on color mode
50 const bgColor = useColorModeValue('white', 'gray.900');
51 const textColor = useColorModeValue('gray.800', 'gray.100');
52 const accentColor = useColorModeValue('blue.500', 'blue.300');
53 const optionsBgColor = useColorModeValue('gray.50', 'gray.800');
54 const userBgColor = 'white';
55 const userTextColor = 'black';
56 const assistantBgColor = 'gray.100';
57 const assistantTextColor = 'black';
58
59 // Fetch chat history or start new chat based on login status
60 useEffect(() => {
61 if (user) {
62 const isFreshLogin = sessionStorage.getItem('isFreshLogin') === 'true';
63
64 if (isFreshLogin) {
65 setLocalMessages([]);
66 dispatch(resetChat());
67
68 }
69 else {
70 const fetchChatHistory = async () => {
71 try {
72 const history = await dispatch(getChatHistory()).unwrap();
73 setLocalMessages(history);
74 } catch (error) {
75 console.error('Failed to fetch chat history:', error);
76 toast({
77 title: "Error Loading Chat History",
78 description: "Could not load your chat history. Starting fresh chat.",
79 status: "error",
80 duration: 5000,
81 isClosable: true,
82 });
83 }
84 };
85 fetchChatHistory();
86 }
87 }
88 }, [dispatch, user]);
89
90 // Handle scroll events in the chat container
91 const handleScroll = () => {
92 if (chatContainerRef.current) {
93 const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current;
94 const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
95 setShowScrollButton(!isAtBottom);
96 }
97 };
98
99 // Add scroll event listener to the chat container
100 useEffect(() => {
101 const chatContainer = chatContainerRef.current;
102 if (chatContainer) {
103 chatContainer.addEventListener('scroll', handleScroll);
104 return () => chatContainer.removeEventListener('scroll', handleScroll);
105
106 }
107 }, []);
108
109 // Scroll to bottom when new messages are added or typing state changes
110 useEffect(() => {
111 if (chatContainerRef.current && !showScrollButton) {
112 scrollToBottom();
113 }
114 }, [localMessages, isTyping]);
115
116 // Scroll to the bottom of the chat container
117 const scrollToBottom = () => {
118 if (chatContainerRef.current) {
119 chatContainerRef.current.scrollTo({
120
121 top: chatContainerRef.current.scrollHeight,
122 behavior: 'smooth'
123
124 });
125 }
126 };
127
128 // Handle input changes in the message textarea
129 const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
130 setNewMessage(e.target.value);
131 };
132
133 // Send a new message to the assistant
134 const handleSendMessage = async (e?: React.FormEvent) => {
135 if (e) e.preventDefault();
136 if (newMessage.trim()) {
137 const userMessage = { role: 'user', content: newMessage };
138 setLocalMessages(prev => [...prev, userMessage]);
139 setNewMessage('');
140 setIsTyping(true);
141 sessionStorage.removeItem('isFreshLogin'); // Clear the flag
142 try {
143 const response = await dispatch(sendMessage({ message: userMessage.content, area: currentTopic || 'general' })).unwrap();
144 setIsTyping(false);
145 setLocalMessages(prev => [...prev, { role: 'assistant', content: response }]);
146 } catch (error) {
147 console.error('Error sending message:', error);
148 setIsTyping(false);
149 toast({
150 title: "Message Not Sent",
151 description: "We couldn't send your message. Please try again later or refresh the page.",
152 status: "error",
153 duration: 5000,
154 isClosable: true,
155 });
156 }
157 }
158 };
159
160 // Handle key down events in the message textarea
161 const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
162 if (e.key === 'Enter' && !e.shiftKey) {
163 e.preventDefault();
164 handleSendMessage();
165
166 }
167 };
168
169 // Handle option selection from the OptionButtons component
170 const handleSelectOption = (option: string) => {
171 setCurrentTopic(option);
172 setNewMessage(`Let's discuss ${option}`);
173 };
174
175 // Start a new chat session
176 const handleNewChat = () => {
177 dispatch(resetChat());
178 setLocalMessages([]);
179 setCurrentTopic(null);
180 setNewMessage('');
181 };
182
183 // Redirect to landing page if user is not logged in
184 if (!user) {
185 return <LandingPage />;
186 }
187
188 return (
189 <Box
190 bg={bgColor}
191 color={textColor}
192 height="calc(100vh - 150px - 120px)"
193 position="fixed"
194 top="70px"
195 left="0"
196 right="0"
197 overflow="hidden"
198 zIndex={1}
199 >
200 <Container maxW="800px" h="100%" position="relative">
201 {localMessages.length === 0 ? (
202 <Flex direction="column" align="center" justify="center" h="100%" py={8}>
203 <VStack spacing={6} mb={8}>
204 <Heading size="lg" textAlign="center">Welcome to FinanceGPT</Heading>
205 <TypewriterText
206 text="How can I help you today?"
207 fontSize="xl"
208 fontWeight="medium"
209 textAlign="center"
210 />
211 </VStack>
212
213 <Box w="full" maxW="700px">
214 <form onSubmit={handleSendMessage}>
215 <Flex mb={6}>
216 <Textarea
217 value={newMessage}
218 onChange={handleInputChange}
219 onKeyDown={handleKeyDown}
220 placeholder="Type your message here..."
221 mr={2}
222 rows={1}
223 resize="none"
224 borderRadius="2xl"
225 border="none"
226 boxShadow="1px 1px 1px 2px rgba(0,0,0,0.1)"
227 py={3}
228 _focus={{
229 boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
230 outline: "none",
231 border: "none"
232 }}
233 _hover={{
234 boxShadow: "0 3px 6px rgba(0,0,0,0.1)"
235 }}
236 />
237 <Button
238 type="submit"
239 colorScheme="blue"
240 bg={accentColor}
241 color="white"
242 isLoading={loading}
243 borderRadius="2xl"
244 px={6}
245 >
246 Send
247 </Button>
248 </Flex>
249 </form>
250
251 <Box
252 bg={"white"}
253 p={6}
254 borderRadius="2xl"
255 >
256 <OptionButtons onSelectOption={handleSelectOption} />
257 </Box>
258 </Box>
259 </Flex>
260 ) : (
261 <Flex direction="column" h="100%" position="relative">
262 <Box
263 flex={1}
264 overflowY="auto"
265 px={2}
266 ref={chatContainerRef}
267 pb="120px"
268 maxW="100%"
269 width="full"
270 sx={{
271 // Chrome, Safari, and Edge styling
272 '&::-webkit-scrollbar': {
273 width: '8px',
274 background: 'transparent'
275 },
276 '&::-webkit-scrollbar-track': {
277 background: 'transparent'
278 },
279 '&::-webkit-scrollbar-thumb': {
280 background: 'rgba(0, 0, 0, 0.2)',
281 borderRadius: '20px',
282 border: '2px solid transparent',
283 backgroundClip: 'padding-box'
284 },
285 '&::-webkit-scrollbar-thumb:hover': {
286 background: 'rgba(0, 0, 0, 0.3)',
287 borderRadius: '20px',
288 border: '2px solid transparent',
289 backgroundClip: 'padding-box'
290 },
291 // Firefox styling
292 scrollbarWidth: 'thin',
293 scrollbarColor: 'rgba(0, 0, 0, 0.2) transparent'
294 }}
295 >
296 <AnimatePresence>
297 {localMessages.map((message, index) => (
298 <MotionFlex
299 key={index}
300 initial={{ opacity: 0, y: 20 }}
301 animate={{ opacity: 1, y: 0 }}
302 exit={{ opacity: 0, y: -20 }}
303 transition={{ duration: 0.3 }}
304 justifyContent={message.role === 'user' ? 'flex-end' : 'flex-start'}
305 mb={4}
306 >
307 <Flex
308 bg={message.role === 'user' ? userBgColor : assistantBgColor}
309 color={message.role === 'user' ? userTextColor : assistantTextColor}
310 borderRadius="2xl"
311 py={2}
312 px={4}
313 maxWidth="70%"
314 boxShadow="sm"
315 borderWidth="1px"
316 borderColor={message.role === 'user' ? 'gray.300' : 'gray.200'}
317 flexDirection="column"
318 >
319 {message.role === 'user' ? (
320 <Text wordBreak="break-word">{message.content}</Text>
321 ) : (
322 <Box>
323 <ReactMarkdown components={{
324 p: (props) => <Text mb={2} {...props} />,
325 ul: (props) => <Box as="ul" pl={4} mb={2} {...props} />,
326 ol: (props) => <Box as="ol" pl={4} mb={2} {...props} />,
327 li: (props) => <Box as="li" mb={1} {...props} />,
328 }}>
329 {message.content}
330 </ReactMarkdown>
331 </Box>
332 )}
333 </Flex>
334 </MotionFlex>
335 ))}
336 </AnimatePresence>
337
338 {isTyping && (
339 <MotionFlex
340 initial={{ opacity: 0 }}
341 animate={{ opacity: 1 }}
342 alignSelf="flex-start"
343 bg={assistantBgColor}
344 color={assistantTextColor}
345 borderRadius="2xl"
346 py={2}
347 px={4}
348 mb={4}
349 maxWidth="70%"
350 boxShadow="sm"
351 borderWidth="1px"
352 borderColor="gray.200"
353 >
354 <Flex alignItems="center">
355 <Spinner size="sm" mr={2} />
356 <Text>AI Financial Advisor is typing...</Text>
357 </Flex>
358 </MotionFlex>
359 )}
360 </Box>
361
362 <AnimatePresence>
363 {showScrollButton && (
364 <MotionIconButton
365 icon={<ChevronDownIcon boxSize={6} />}
366 aria-label="Scroll to bottom"
367 position="fixed"
368 bottom="120px"
369 right="40px"
370 borderRadius="full"
371 boxShadow="lg"
372 onClick={scrollToBottom}
373 colorScheme="blue"
374 size="lg"
375 bg="white"
376 color="gray.600"
377 _hover={{
378 transform: "translateY(-2px)",
379 boxShadow: "xl"
380 }}
381 initial={{ opacity: 0, scale: 0.8 }}
382 animate={{ opacity: 1, scale: 1 }}
383 exit={{ opacity: 0, scale: 0.8 }}
384 transition={{ duration: 0.2 }}
385 zIndex={999}
386 />
387 )}
388 </AnimatePresence>
389
390 <Box
391 position="absolute"
392 bottom={0}
393 left={0}
394 right={0}
395 bg="transparent"
396 p={1}
397 zIndex={1}
398 bgColor={"white"}
399 borderRadius={"2xl"}
400 // boxShadow="0 -10px 20px rgba(0,0,0,0.1)"
401 _before={{
402 content: '""',
403 position: 'absolute',
404 top: 0,
405 left: 0,
406 right: 0,
407 bottom: 0,
408 bg: bgColor,
409 opacity: 0.8,
410 backdropFilter: 'blur(8px)',
411 zIndex: -1
412 }}
413 >
414 <form onSubmit={handleSendMessage} style={{ display: 'flex', width: '100%' }}>
415 <Textarea
416 value={newMessage}
417 onChange={handleInputChange}
418 onKeyDown={handleKeyDown}
419 placeholder="Type your message here (Press Enter to send, Shift+Enter for new line)"
420 mr={2}
421 flex={1}
422 rows={2}
423 h={"10"}
424 resize="none"
425 borderRadius="2xl"
426 bg = "white"
427 border="none"
428 boxShadow="1px 1px 1px 2px rgba(0,0,0,0.1)"
429 _focus={{
430 boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
431 outline: "none",
432 border: "none"
433 }}
434 _hover={{
435 boxShadow: "0 3px 6px rgba(0,0,0,0.1)"
436 }}
437 />
438 <Button
439 type="submit"
440 colorScheme="blue"
441 bg={accentColor}
442 color="white"
443 isLoading={loading}
444 borderRadius="2xl"
445 >
446 Send
447 </Button>
448 <Button
449 onClick={handleNewChat}
450 ml={2}
451 variant="outline"
452 borderRadius="2xl"
453 >
454 New Chat
455 </Button>
456 </form>
457 </Box>
458 </Flex>
459 )}
460 {error && <Text color="red.500" mt={2}>{error}</Text>}
461 </Container>
462 </Box>
463 );
464}
465
Last updated 2 months ago