diff --git a/.firebaserc b/.firebaserc index a665c764a..1b456d84d 100644 --- a/.firebaserc +++ b/.firebaserc @@ -11,5 +11,6 @@ } } }, - "etags": {} + "etags": {}, + "dataconnectEmulatorConfig": {} } \ No newline at end of file diff --git a/frontend/assets/svg/ReadyPlayerMeAvatar.svg b/frontend/assets/svg/ReadyPlayerMeAvatar.svg new file mode 100644 index 000000000..430d51ebb --- /dev/null +++ b/frontend/assets/svg/ReadyPlayerMeAvatar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/assets/svg/StarGroupIcon.svg b/frontend/assets/svg/StarGroupIcon.svg new file mode 100644 index 000000000..553fd36ce --- /dev/null +++ b/frontend/assets/svg/StarGroupIcon.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/assets/svg/Union.svg b/frontend/assets/svg/Union.svg new file mode 100644 index 000000000..821c193c4 --- /dev/null +++ b/frontend/assets/svg/Union.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/assets/svg/UnionPurple.svg b/frontend/assets/svg/UnionPurple.svg new file mode 100644 index 000000000..45e12fb08 --- /dev/null +++ b/frontend/assets/svg/UnionPurple.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/assets/svg/add-block2.svg b/frontend/assets/svg/add-block2.svg new file mode 100644 index 000000000..52530a107 --- /dev/null +++ b/frontend/assets/svg/add-block2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/constants/personas.js b/frontend/constants/personas.js new file mode 100644 index 000000000..d06eb8c23 --- /dev/null +++ b/frontend/constants/personas.js @@ -0,0 +1,24 @@ +const categorizePersonas = [ + { + title: 'Math Tutor', + description: + 'From now on, I want you to act as a math tutor. I will be asking you questions related to various mathematical concepts, including algebra, geometry, calculus, and statistics. Please provide detailed explanations, step-by-step solutions, and relevant examples for each topic we discuss.', + }, + { + title: 'Biology Tutor', + description: + 'Please act as my biology tutor. I will ask you about topics such as cell biology, genetics, evolution, ecology, and human anatomy. Provide comprehensive explanations, diagrams, and examples to help me understand these biological concepts.', + }, + { + title: 'Programming Tutor', + description: + 'I want you to be my programming tutor. I will ask you about various programming languages, coding concepts, algorithms, and debugging techniques. Provide clear explanations, code examples, and step-by-step guidance for writing and understanding code.', + }, + { + title: 'Music Tutor', + description: + 'Act as a music tutor for our conversation. I will ask you about music theory, instruments, composition, and performance techniques. Provide detailed explanations, sheet music examples, and exercises to help me understand and improve my musical abilities.', + }, +]; + +export default categorizePersonas; diff --git a/frontend/constants/routes_ids.js b/frontend/constants/routes_ids.js new file mode 100644 index 000000000..7ebeeabfb --- /dev/null +++ b/frontend/constants/routes_ids.js @@ -0,0 +1,6 @@ +const ROUTES_IDS = { + HOME: 'home', + DISCOVERY: 'discovery', + CHAT: 'chat', +}; +export default ROUTES_IDS; diff --git a/frontend/layouts/MainAppLayout/ MainAppLayout.jsx b/frontend/layouts/MainAppLayout/ MainAppLayout.jsx index 5b5d995dc..7e9b19130 100644 --- a/frontend/layouts/MainAppLayout/ MainAppLayout.jsx +++ b/frontend/layouts/MainAppLayout/ MainAppLayout.jsx @@ -1,14 +1,20 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Grid, useMediaQuery } from '@mui/material'; import Head from 'next/head'; +import { useRouter } from 'next/router'; import { useDispatch, useSelector } from 'react-redux'; import AppDisabled from '@/components/AppDisabled'; import Loader from '@/components/Loader'; +import DiscoveryLibraryWindow from '@/templates/Chat/DiscoveryLibraryWindow'; + +import ROUTES from '@/constants/routes'; + import NavBar from './NavBar'; + import styles from './styles'; import { setLoading } from '@/redux/slices/authSlice'; @@ -28,7 +34,8 @@ const MainAppLayout = (props) => { const auth = useSelector((state) => state.auth); const user = useSelector((state) => state.user); - + const [isDiscoveryOpen, setDiscoveryOpen] = useState(false); + const router = useRouter(); const isTabletScreen = useMediaQuery((theme) => theme.breakpoints.down('laptop') ); @@ -49,12 +56,24 @@ const MainAppLayout = (props) => { ); }; + const handleDiscoveryToggle = () => { + setDiscoveryOpen((prev) => !prev); + }; + + const isDiscoveryPage = router.pathname === ROUTES.DISCOVERY; + const renderApp = () => { return ( <> - + - {children} + {children}{' '} + {isDiscoveryPage && ( + + )} ); diff --git a/frontend/layouts/MainAppLayout/NavBar/Navbar.jsx b/frontend/layouts/MainAppLayout/NavBar/Navbar.jsx index 21d47507d..e8dc450d5 100644 --- a/frontend/layouts/MainAppLayout/NavBar/Navbar.jsx +++ b/frontend/layouts/MainAppLayout/NavBar/Navbar.jsx @@ -1,15 +1,20 @@ -import { Avatar, Button, Grid, Typography } from '@mui/material'; +import { useState } from 'react'; + +import { Avatar, Button, Grid, Slide, Typography } from '@mui/material'; import { signOut } from 'firebase/auth'; import { useRouter } from 'next/router'; import { useSelector } from 'react-redux'; +import DiscoveryLibrary from '@/templates/Chat/DiscoveryLibrary'; + import ChatIcon from '@/assets/svg/ChatIcon.svg'; -// import DiscoveryIcon from '@/assets/svg/DiscoveryIcon.svg'; +import DiscoveryIcon from '@/assets/svg/DiscoveryIcon.svg'; import HomeIcon from '@/assets/svg/HomeMenuIcon.svg'; import LogoutIcon from '@/assets/svg/LogoutIcon.svg'; import ROUTES from '@/constants/routes'; +import ROUTES_IDS from '@/constants/routes_ids'; import styles from './styles'; @@ -22,19 +27,19 @@ const PAGES = [ name: 'Home', link: ROUTES.HOME, icon: , - id: 'home', + id: ROUTES_IDS.HOME, }, - /* { + { name: 'Discovery', link: ROUTES.DISCOVERY, icon: , - id: 'discovery', - }, */ + id: ROUTES_IDS.DISCOVERY, + }, { name: 'Chat', link: ROUTES.CHAT, icon: , - id: 'chat', + id: ROUTES_IDS.CHAT, }, ]; @@ -46,7 +51,8 @@ const PAGES = [ * @component * @returns {JSX.Element} The navigation bar component. */ -const NavBar = () => { +const NavBar = (props) => { + const { isDiscoveryOpen, toggleDiscovery } = props; const router = useRouter(); const user = useSelector((state) => state.user.data); @@ -122,13 +128,13 @@ const NavBar = () => { chatRegex.test(pathname) || discoveryRegex.test(pathname), ].includes(true); - if (id === 'home') + if (id === ROUTES_IDS.HOME) return isNotHomePage ? false : homeRegex.test(pathname); // TODO: Once Discovery Feature is ready, uncomment below statement. - // if (id === 'discovery') return discoveryRegex.test(pathname); + if (id === ROUTES_IDS.DISCOVERY) return discoveryRegex.test(pathname); - if (id === 'chat') return chatRegex.test(pathname); + if (id === ROUTES_IDS.CHAT) return chatRegex.test(pathname); return false; }; @@ -139,8 +145,12 @@ const NavBar = () => { * @param {string} link - The route to navigate to. * @returns {void} */ - const handleRoute = (link) => { - router.push(link); + const handleRoute = (link, id) => { + if (id === ROUTES_IDS.DISCOVERY && router.pathname === ROUTES.DISCOVERY) { + toggleDiscovery(); + } else { + router.push(link); + } }; return ( @@ -158,7 +168,6 @@ const NavBar = () => { ); }; - return ( {renderLogo()} diff --git a/frontend/pages/discovery/index.jsx b/frontend/pages/discovery/index.jsx new file mode 100644 index 000000000..0a779bc91 --- /dev/null +++ b/frontend/pages/discovery/index.jsx @@ -0,0 +1,12 @@ +import MainAppLayout from '@/layouts/MainAppLayout'; +import DiscoveryLibrary from '@/templates/Chat/DiscoveryLibrary'; + +const MarvelDiscovery = () => { + return ; +}; + +MarvelDiscovery.getLayout = function getLayout(page) { + return {page}; +}; + +export default MarvelDiscovery; diff --git a/frontend/redux/slices/personaSlice.js b/frontend/redux/slices/personaSlice.js new file mode 100644 index 000000000..907f2f316 --- /dev/null +++ b/frontend/redux/slices/personaSlice.js @@ -0,0 +1,31 @@ +import { createSlice } from '@reduxjs/toolkit'; + +import fetchPersonas from '../thunks/fetchPersona'; + +const personasSlice = createSlice({ + name: 'personas', + initialState: { + data: [], + loading: false, + error: null, + }, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchPersonas.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchPersonas.fulfilled, (state, action) => { + state.loading = false; + state.data = action.payload; + }) + .addCase(fetchPersonas.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message; + }); + }, +}); + +export const { setPersonas } = personasSlice.actions; +export default personasSlice.reducer; diff --git a/frontend/redux/store.js b/frontend/redux/store.js index 8f61a3e60..3ec1451bb 100644 --- a/frontend/redux/store.js +++ b/frontend/redux/store.js @@ -8,6 +8,7 @@ import { connectFunctionsEmulator, getFunctions } from 'firebase/functions'; import authReducer from './slices/authSlice'; import chatReducer from './slices/chatSlice'; import historyReducer from './slices/historySlice'; +import personasReducer from './slices/personaSlice'; import toolsReducer from './slices/toolsSlice'; import userReducer from './slices/userSlice'; @@ -33,6 +34,7 @@ const store = configureStore({ tools: toolsReducer, chat: chatReducer, history: historyReducer, + personas: personasReducer, }, }); diff --git a/frontend/redux/thunks/addPersonas.js b/frontend/redux/thunks/addPersonas.js new file mode 100644 index 000000000..9d17d26a5 --- /dev/null +++ b/frontend/redux/thunks/addPersonas.js @@ -0,0 +1,28 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +import { addDoc, collection, getDocs, getFirestore } from 'firebase/firestore'; + +import categorizePersonas from '@/constants/personas'; + +const addPersonas = createAsyncThunk('personas/add', async () => { + try { + const db = getFirestore(); + const personasRef = collection(db, 'personas'); + const querySnapshot = await getDocs(personasRef); + if (querySnapshot.empty) { + const addOperations = categorizePersonas.forEach(async (persona) => { + try { + const docRef = await addDoc(personasRef, persona); + return docRef; + } catch (err) { + throw new Error('Error adding persona:', err); + } + }); + await Promise.all(addOperations); + } + } catch (err) { + throw new Error(err); + } +}); + +export default addPersonas; diff --git a/frontend/redux/thunks/fetchPersona.js b/frontend/redux/thunks/fetchPersona.js new file mode 100644 index 000000000..44429837c --- /dev/null +++ b/frontend/redux/thunks/fetchPersona.js @@ -0,0 +1,22 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { collection, getDocs, getFirestore } from 'firebase/firestore'; + +const fetchPersonas = createAsyncThunk('personas/fetch', async () => { + try { + const db = getFirestore(); + const personasRef = collection(db, 'personas'); + const querySnapshot = await getDocs(personasRef); + if (querySnapshot.empty) { + throw new Error('No personas found!'); + } + const personas = querySnapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); + return personas; + } catch (err) { + throw new Error(err); + } +}); + +export default fetchPersonas; diff --git a/frontend/templates/Chat/Chat.jsx b/frontend/templates/Chat/Chat.jsx index 92cbad710..8c40a6ed5 100644 --- a/frontend/templates/Chat/Chat.jsx +++ b/frontend/templates/Chat/Chat.jsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { ArrowDownwardOutlined, @@ -27,6 +27,7 @@ import { MESSAGE_ROLE, MESSAGE_TYPES } from '@/constants/bots'; import ChatHistoryWindow from './ChatHistoryWindow'; import ChatSpinner from './ChatSpinner'; import DefaultPrompt from './DefaultPrompt'; +import DiscoveryLibraryWindow from './DiscoveryLibraryWindow'; import Message from './Message'; import QuickActions from './QuickActions'; import styles from './styles'; @@ -103,7 +104,6 @@ const ChatInterface = () => { type: 'chat', message, }; - // Send a chat session const { status, data } = await createChatSession(chatPayload, dispatch); diff --git a/frontend/templates/Chat/DiscoveryLibrary/DiscoveryLibrary.jsx b/frontend/templates/Chat/DiscoveryLibrary/DiscoveryLibrary.jsx new file mode 100644 index 000000000..33f6657ff --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibrary/DiscoveryLibrary.jsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; + +import { + Card, + CardActionArea, + CardContent, + Grid, + IconButton, + Typography, +} from '@mui/material'; + +import { useDispatch, useSelector } from 'react-redux'; + +import UnionPurpleIcon from '@/assets/svg//UnionPurple.svg'; +import DiscoveryIcon from '@/assets/svg/add-block2.svg'; + +import AvatarImage from '@/assets/svg/ReadyPlayerMeAvatar.svg'; +import StarGroupIcon from '@/assets/svg/starGroupIcon.svg'; +import UnionIcon from '@/assets/svg/Union.svg'; + +import styles from './styles'; + +import { resetChat } from '@/redux/slices/chatSlice'; +import addPersonas from '@/redux/thunks/addPersonas'; +import fetchPersonas from '@/redux/thunks/fetchPersona'; + +const DiscoveryLibrary = (props) => { + const { show, selectedPrompt } = props; + const { data: user } = useSelector((state) => state.user); + const personas = useSelector((state) => state.personas?.data || []); + const loading = useSelector((state) => state.personas?.loading); + const error = useSelector((state) => state.personas?.error); + const dispatch = useDispatch(); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + dispatch(fetchPersonas()); + dispatch(addPersonas()); + }, [dispatch]); + useEffect(() => { + if (personas && Array.isArray(personas) && personas.length > 0) { + setIsLoaded(true); + } + }, [personas]); + + useEffect(() => {}, [isLoaded]); + useEffect(() => {}, [loading]); + useEffect(() => {}, [error]); + + const handlePersonaClick = (persona) => { + selectedPrompt(persona); + dispatch(resetChat()); + }; + + const renderContent = () => { + if (loading) { + return ( + + Loading... + + ); + } + + if (error) { + return ( + + Error loading data + + ); + } + + if (isLoaded && personas.length > 0) { + return ( + + {personas.map((persona, index) => ( + handlePersonaClick(persona)}> + + + + + {persona.title} + + + + + + + + + ))} + + ); + } + + return null; + }; + + return ( + + + + + + + Discovery + + + + + + + + Welcome Back, {user?.fullName || 'User'}! + + + + + + + + + + + + + AI Custom Course Creator + + + Have Kai help you build your class from scratch! + + + + {renderContent()} + + + ); +}; + +export default DiscoveryLibrary; diff --git a/frontend/templates/Chat/DiscoveryLibrary/index.js b/frontend/templates/Chat/DiscoveryLibrary/index.js new file mode 100644 index 000000000..d210741fb --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibrary/index.js @@ -0,0 +1 @@ +export { default } from './DiscoveryLibrary'; diff --git a/frontend/templates/Chat/DiscoveryLibrary/styles.js b/frontend/templates/Chat/DiscoveryLibrary/styles.js new file mode 100644 index 000000000..f2aa504f1 --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibrary/styles.js @@ -0,0 +1,293 @@ +const styles = { + iconButtonProps: { + sx: { + padding: '8px', + }, + }, + buttonTextProps: { + sx: { + padding: '8px', + }, + }, + menuListProps: { + sx: { + display: 'flex', + flexDirection: 'row', + padding: 1, + margin: '15px', + }, + }, + paperProps: { + sx: { + backgroundColor: 'transparent !important', + boxShadow: 'none', + }, + }, + menuItemProps: (disabled) => ({ + sx: (theme) => ({ + borderRadius: '18px', + margin: '0 5px', + + borderColor: theme.palette.Background.purple3, + background: theme.palette.Background.purple3, + color: theme.palette.Common.White['100p'], + textTransform: 'none', + ':hover': { + backgroundColor: disabled ? 'none' : '#B791FF', + borderColor: disabled + ? theme.palette.Background.purple3 + : theme.palette.Background.purple, + color: disabled + ? theme.palette.Common.White['60p'] + : theme.palette.Common.White['100p'], + }, + padding: '5px 20px', + opacity: disabled ? 0.5 : 1, + cursor: disabled ? 'not-allowed' : 'pointer', + + fontSize: { laptop: '13px', desktop: '12px', desktopMedium: '14px' }, + pl: { laptop: 1, desktop: 1, desktopMedium: 1 }, + pr: { laptop: 1, desktop: 1, desktopMedium: 1 }, + }), + }), + discoveryContainerGrid: (show) => ({ + container: true, + item: true, + mobileSmall: true, + position: 'absolute', + top: -80, + left: { laptop: -35, desktop: 20 }, + height: '90%', + width: 'fit-content', + display: show ? 'block' : 'none', + backgroundColor: 'transparent !important', + }), + discoveryGridProps: { + container: true, + item: true, + mobileSmall: true, + rowGap: { laptop: 2, desktop: 4 }, + flexDirection: 'column', + + width: { desktopMedium: '37%', desktop: '37.5%', laptop: '100%' }, + sx: { + transform: { + laptop: 'scale(0.8 )', + desktop: 'scale(0.8)', + desktopMedium: 'scale(0.8)', + }, + borderRadius: '15px', + borderColor: '#B791FF', + }, + }, + discoveryPanelProps: { + sx: (theme) => ({ + px: 2, + py: 2, + background: theme.palette.Common.Black['100p'], + borderRadius: '16px 16px 0 0', + width: { desktop: '350px', mobile: '338px', mobileSmall: '330px' }, + }), + }, + unionIconGridProps: { + container: true, + alignItems: 'center', + sx: { + width: { desktop: '350px', mobile: '338px', mobileSmall: '330px' }, + mt: { laptop: -1, desktop: -3 }, + px: 2, + py: 2, + }, + }, + unionIconTextProps: { + sx: { + mb: 1, + color: '#9E94A5', + }, + }, + discoveryIconProps: { + sx: { + fontSize: '24px', + }, + }, + discoveryPanelTextProps: { + sx: (theme) => ({ + mt: 1, + mb: 1, + color: theme.palette.Common.White['100p'], + }), + }, + avatarGridProps: { + container: true, + item: true, + positive: 'relative', + alignItems: 'center', + flexDirection: 'column', + justifyContent: 'center', + sx: (theme) => ({ + ml: 2, + px: 2, + py: 2, + mt: { laptop: 1, desktop: -1 }, + background: theme.palette.Background.gradient.purple, + borderRadius: '16px', + width: { desktop: '320px', mobile: '308px', mobileSmall: '130px' }, + height: { desktop: '190px', mobile: '178px', mobileSmall: '170px' }, + }), + }, + avatarHeaderTextProps: { + fontSize: '18px', + fontWeight: 'bold', + }, + avatarSubTextProps: { + fontSize: '11px', + }, + avatarTextBoxProps: { + item: true, + alignItems: 'center', + sx: (theme) => ({ + background: theme.palette.Background.gradient.grey, + borderRadius: '5px', + padding: '10px 15px 10px 25px', + color: theme.palette.Common.White['100p'], + }), + right: { laptop: 240, desktop: 241 }, + top: { laptop: 40, desktop: 44 }, + position: 'relative', + zIndex: 2, + width: { laptop: '280px', desktop: '300px' }, + height: { laptop: '70px', desktop: '80px' }, + }, + + avatarImageGridProps: { + item: true, + sx: { + position: 'relative', + top: { laptop: -14, desktop: -17 }, + right: { laptop: 3, desktop: -3 }, + svg: { + transform: { laptop: 'scale(1.2)', desktop: 'scale(1.30)' }, + }, + }, + }, + starGroupIconGridProps: { + item: true, + sx: { + position: 'relative', + top: { laptop: -40, desktop: -44 }, + left: { laptop: 50, desktop: 60 }, + }, + }, + cardGridProps: { + container: true, + display: 'flex', + alignItems: 'center', + spacing: 1, + width: { desktop: '320px', mobile: '308px', mobileSmall: '130px' }, + height: { laptop: '44vh', desktop: '42vh' }, + overflow: 'scroll', + sx: { + position: 'relative', + top: { laptop: -5, desktop: -20, desktopMedium: -20 }, + right: { laptop: -25, desktop: -25, desktopMedium: -25 }, + pb: 1, + }, + }, + cardProps: { + sx: { + backgroundColor: 'transparent', + color: (theme) => `${theme.palette.Common.White['100p']}`, + border: '2px solid #9E86FF', + borderRadius: '8px', + boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)', + cursor: 'pointer', + transition: '0.3s', + '&:hover': { + boxShadow: '0px 6px 12px rgba(0, 0, 0, 0.2)', + }, + width: { laptop: '142px', desktop: '148px' }, + height: { laptop: '150px', desktop: '150px' }, + }, + }, + cardTitleProps: { + sx: { + alignItems: 'center', + fontSize: '11px', + fontWeight: 'bold', + }, + }, + cardDescriptionProps: { + sx: (theme) => ({ + fontSize: '12px', + color: theme.palette.Greyscale[400], + }), + }, + backImageProps: { + width: '100%', + height: '100%', + }, + chatBoxGridProps: { + container: true, + item: true, + alignItems: 'center', + flexDirection: 'column', + justifyContent: 'center', + }, + chatBoxProps: { + width: { + laptop: '59%', + desktop: '64%', + desktopMedium: '70%', + desktopLarge: '75%', + }, + sx: { + position: 'relative', + top: { laptop: 80, desktop: 70, desktopMedium: 60 }, + left: { laptop: 340, desktop: 350, desktopMedium: 350 }, + }, + }, + CenterChatContentGridProps: { + sx: { + overflow: 'scroll', + height: { laptop: '500px', desktop: '700px' }, + width: { + laptop: '50%', + desktop: '60%', + desktopMedium: '65%', + desktopLarge: '72%', + }, + left: { laptop: 200, desktop: 200 }, + }, + }, + starIconProps: { + item: true, + sx: { + position: 'relative', + top: { laptop: 62, desktop: 64 }, + left: { laptop: 95, desktop: 105 }, + }, + }, + + loadingGridProps: { + container: true, + display: 'flex', + alignItems: 'center', + }, + errorGridProps: { + container: true, + display: 'flex', + alignItems: 'center', + }, + loadingProps: { + sx: (theme) => ({ + color: theme.palette.Common.White['100p'], + }), + }, + + errorProps: { + sx: (theme) => ({ + color: theme.palette.Background.red, + }), + }, +}; +export default styles; diff --git a/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx b/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx new file mode 100644 index 000000000..ddfe7dfa9 --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx @@ -0,0 +1,375 @@ +import { useEffect, useRef, useState } from 'react'; + +import { + ArrowDownwardOutlined, + InfoOutlined, + Settings, +} from '@mui/icons-material'; +import AddIcon from '@mui/icons-material/Add'; +import { + Button, + Fade, + Grid, + InputAdornment, + Slide, + TextField, + Typography, +} from '@mui/material'; +import IconButton from '@mui/material/IconButton'; +import { useRouter } from 'next/router'; + +import { useDispatch, useSelector } from 'react-redux'; + +import NavigationIcon from '@/assets/svg/Navigation.svg'; +import UnionPurpleIcon from '@/assets/svg/UnionPurple.svg'; + +import { MESSAGE_ROLE, MESSAGE_TYPES } from '@/constants/bots'; + +import ROUTES from '@/constants/routes'; + +import ChatSpinner from '../ChatSpinner'; +import DiscoveryLibrary from '../DiscoveryLibrary'; +import Message from '../Message'; +import QuickActions from '../QuickActions'; + +import TextMessage from '../TextMessage'; + +import styles from './styles'; + +import { + openInfoChat, + resetChat, + setActionType, + setChatSession, + setDisplayQuickActions, + setError, + setFullyScrolled, + setInput, + setMessages, + setMore, + setSessionLoaded, + setStreaming, + setStreamingDone, + setTyping, +} from '@/redux/slices/chatSlice'; + +import createChatSession from '@/services/chatbot/createChatSession'; +import sendMessage from '@/services/chatbot/sendMessage'; + +const DiscoveryLibraryWindow = (props) => { + const { isDiscoveryOpen } = props; + const dispatch = useDispatch(); + const { + more, + input, + typing, + chat, + sessionLoaded, + openSettingsChat, + infoChatOpened, + fullyScrolled, + streamingDone, + streaming, + error, + displayQuickActions, + actionType, + } = useSelector((state) => state.chat); + const { data: userData } = useSelector((state) => state.user); + const messagesContainerRef = useRef(); + const sessionId = localStorage.getItem('sessionId'); + + const currentSession = chat; + const chatMessages = currentSession?.messages; + const showNewMessageIndicator = !fullyScrolled && streamingDone; + const router = useRouter(); + const isDiscoveryPage = router.pathname === ROUTES.DISCOVERY; + const [selectedPrompt, setSelectedPrompt] = useState(null); + const startConversation = async (message) => { + dispatch( + setMessages({ + role: MESSAGE_ROLE.HUMAN, + message, + }) + ); + + dispatch(setTyping(true)); + + const chatPayload = { + user: { + id: userData?.id, + fullName: userData?.fullName, + email: userData?.email, + }, + type: 'chat', + message, + }; + + const { status, data } = await createChatSession(chatPayload, dispatch); + + dispatch(setTyping(false)); + if (status === 'created') dispatch(setStreaming(true)); + + dispatch(setChatSession(data)); + dispatch(setSessionLoaded(true)); + setSelectedPrompt(null); + }; + + const handleSendMessage = async () => { + if (!input) { + dispatch(setError('Please enter a message')); + setTimeout(() => { + dispatch(setError(null)); + }, 3000); + return; + } + + dispatch(setStreaming(true)); + + const message = { + role: MESSAGE_ROLE.HUMAN, + type: MESSAGE_TYPES.TEXT, + payload: { + text: input, + action: actionType, + }, + }; + + if (!chatMessages) { + await startConversation(message); + return; + } + + dispatch( + setMessages({ + role: MESSAGE_ROLE.HUMAN, + message, + }) + ); + + dispatch(setTyping(true)); + + setTimeout(async () => { + await sendMessage({ message, id: sessionId }, dispatch); + }, 0); + dispatch(setActionType(null)); + }; + + const handleQuickReply = async (option) => { + dispatch(setInput(option)); + dispatch(setStreaming(true)); + + const message = { + role: MESSAGE_ROLE.HUMAN, + type: MESSAGE_TYPES.QUICK_REPLY, + payload: { + text: option, + action: actionType, + }, + }; + + dispatch( + setMessages({ + role: MESSAGE_ROLE.HUMAN, + }) + ); + dispatch(setTyping(true)); + + await sendMessage({ message, id: currentSession?.id }, dispatch); + + dispatch(setActionType(null)); + }; + + const handleOnScroll = () => { + const scrolled = + Math.abs( + messagesContainerRef.current.scrollHeight - + messagesContainerRef.current.clientHeight - + messagesContainerRef.current.scrollTop + ) <= 1; + + if (fullyScrolled !== scrolled) dispatch(setFullyScrolled(scrolled)); + }; + + const handleScrollToBottom = () => { + messagesContainerRef.current?.scrollTo( + 0, + messagesContainerRef.current?.scrollHeight, + { + behavior: 'smooth', + } + ); + + dispatch(setStreamingDone(false)); + }; + + const keyDownHandler = async (e) => { + if (typing || !input || streaming) return; + if (e.keyCode === 13) handleSendMessage(); + }; + + const renderQuickAction = () => { + return ( + + dispatch(setDisplayQuickActions(!displayQuickActions))} + {...styles.quickActionButton} + > + + Actions + + + ); + }; + + const renderSendIcon = () => { + return ( + + + + + + ); + }; + + const renderCustomPrompt = () => { + if (selectedPrompt) { + return ( + dispatch(setMore({ role: 'shutdown' }))} + {...styles.centerChat.centerChatGridProps} + > + + + + {selectedPrompt.title} + + + + + + + + ); + } + return null; + }; + + const renderStartChatMessage = () => { + return ( + + ); + }; + + const renderCenterChatContent = () => { + if (selectedPrompt) { + return renderCustomPrompt(); + } + return ( + dispatch(setMore({ role: 'shutdown' }))} + {...styles.centerChat.centerChatGridProps} + > + + {(chatMessages?.length === 0 || !chatMessages) && + !infoChatOpened && + renderStartChatMessage()} + {chatMessages?.map( + (message, index) => + message?.role !== MESSAGE_ROLE.SYSTEM && ( + + ) + )} + {typing && } + + + ); + }; + + const renderNewMessageIndicator = () => { + return ( + +