0xmetaschool/ FinanceGPT

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

src
animations
app
chat
financial-snapshot
goals
onboarding
profile
globals.css
layout.tsx
page.tsx
components
hooks
lib
models
store
middleware.ts
theme.js
.gitignore
LICENSE
next.config.mjs
package.json
postcss.config.mjs
README.md
tailwind.config.ts
tsconfig.json
vercel.json
srcseparatorappseparatorpage.tsx
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
Downloads

0

Version

1.0.0

License

MIT

Platform

OpenAI GPT-4

Contributors 4
aveek-goyal
fareeha25
gupta19esha
pashmeenanoor0x
Open in

Last updated 2 months ago

We are a open source platform and encourage community to contribute to these templates!