From a4735e207c907c2b144604d1f7420cf5ef057cc1 Mon Sep 17 00:00:00 2001 From: Viraj Patel Date: Thu, 25 Jul 2024 13:12:46 -0400 Subject: [PATCH 1/7] Merge branches 'es1-chat-history-feature' and 'es2-quick-actions-feature' into es4-discovery-library --- .firebaserc | 8 +- firestore.rules | 5 +- frontend/constants/bots.js | 28 +- frontend/redux/slices/chatSlice.js | 30 +++ frontend/redux/slices/historySlice.js | 67 +++++ frontend/redux/store.js | 2 + frontend/redux/thunks/fetchChat.js | 34 +++ frontend/redux/thunks/fetchHistory.js | 60 +++++ .../CenterChatContentNoMessages/styles.js | 6 +- frontend/templates/Chat/Chat.jsx | 115 ++++++-- .../Chat/ChatHistory/ChatHistory.jsx | 253 ++++++++++++++++++ frontend/templates/Chat/ChatHistory/index.js | 1 + frontend/templates/Chat/ChatHistory/styles.js | 48 ++++ .../ChatHistoryWindow/ChatHistoryWindow.jsx | 112 ++++++++ .../templates/Chat/ChatHistoryWindow/index.js | 1 + .../Chat/ChatHistoryWindow/styles.js | 170 ++++++++++++ .../Chat/DefaultPrompt/DefaultPrompt.jsx | 90 +++++++ .../templates/Chat/DefaultPrompt/index.js | 1 + .../templates/Chat/DefaultPrompt/styles.js | 62 +++++ .../Chat/QuickActions/QuickActions.jsx | 113 ++++++++ frontend/templates/Chat/QuickActions/index.js | 1 + .../templates/Chat/QuickActions/styles.js | 40 +++ frontend/templates/Chat/styles.js | 41 ++- functions/controllers/kaiAIController.js | 18 +- 24 files changed, 1275 insertions(+), 31 deletions(-) create mode 100644 frontend/redux/slices/historySlice.js create mode 100644 frontend/redux/thunks/fetchChat.js create mode 100644 frontend/redux/thunks/fetchHistory.js create mode 100644 frontend/templates/Chat/ChatHistory/ChatHistory.jsx create mode 100644 frontend/templates/Chat/ChatHistory/index.js create mode 100644 frontend/templates/Chat/ChatHistory/styles.js create mode 100644 frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx create mode 100644 frontend/templates/Chat/ChatHistoryWindow/index.js create mode 100644 frontend/templates/Chat/ChatHistoryWindow/styles.js create mode 100644 frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx create mode 100644 frontend/templates/Chat/DefaultPrompt/index.js create mode 100644 frontend/templates/Chat/DefaultPrompt/styles.js create mode 100644 frontend/templates/Chat/QuickActions/QuickActions.jsx create mode 100644 frontend/templates/Chat/QuickActions/index.js create mode 100644 frontend/templates/Chat/QuickActions/styles.js diff --git a/.firebaserc b/.firebaserc index dc6ce8ee4..a665c764a 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,15 +1,15 @@ { "projects": { - "default": "kai-ai-f63c8" + "default": "kai-ai-b8ac9" }, "targets": { - "kai-ai-f63c8": { + "kai-ai-b8ac9": { "hosting": { "next": [ - "kai-ai-f63c8" + "kai-ai-b8ac9" ] } } }, "etags": {} -} +} \ No newline at end of file diff --git a/firestore.rules b/firestore.rules index a72aef320..89f559c52 100644 --- a/firestore.rules +++ b/firestore.rules @@ -1,9 +1,8 @@ rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { - match /{document=**} { - allow read, write: if - request.time < timestamp.date(2023, 6, 16); + match /chatSessions/{id} { + allow read, write: if request.auth != null && resource.data.user.id == request.auth.uid; } } } \ No newline at end of file diff --git a/frontend/constants/bots.js b/frontend/constants/bots.js index 39abb9565..3279b07f8 100644 --- a/frontend/constants/bots.js +++ b/frontend/constants/bots.js @@ -1,3 +1,23 @@ +const ACTION_TYPES = { + SUMMARIZE_CONTENT: { + actionType: 'Summarize Content', + description: 'Summarize key concepts and ideas for a given topic.', + }, + GENERATE_QUIZ: { + actionType: 'Generate Quiz', + description: 'Create quiz on a given topic to test students understanding.', + }, + ACTIVITY_RECOMMENDATIONS: { + actionType: 'Activity Recommendations', + description: 'Offer engaging activities based on the current subject.', + }, + INTERACTIVE_TECHNIQUES: { + actionType: 'Interactive Techniques', + description: + 'Suggest ideas for making lessons more interactive and engaging.', + }, +}; + const BOT_TYPE = { MISSION: 'mission', TEACH_ME: 'teach_me', @@ -6,6 +26,12 @@ const BOT_TYPE = { HACKATHON: 'hackathon', }; +const DEFAULT_PROMPTS = [ + 'Design an engaging class activity.', + 'Recommend resources for effective teaching.', + 'Strategies to encourage student participation.', +]; + const MESSAGE_ROLE = { SYSTEM: 'system', HUMAN: 'human', @@ -22,4 +48,4 @@ const MESSAGE_TYPES = { QUICK_REPLY: 'quick_reply', }; -export { BOT_TYPE, MESSAGE_ROLE, MESSAGE_TYPES }; +export { ACTION_TYPES, BOT_TYPE, DEFAULT_PROMPTS, MESSAGE_ROLE, MESSAGE_TYPES }; diff --git a/frontend/redux/slices/chatSlice.js b/frontend/redux/slices/chatSlice.js index 9f2c47f50..17d48d4e3 100644 --- a/frontend/redux/slices/chatSlice.js +++ b/frontend/redux/slices/chatSlice.js @@ -2,6 +2,8 @@ import { createSlice } from '@reduxjs/toolkit'; import { MESSAGE_ROLE, MESSAGE_TYPES } from '@/constants/bots'; +import fetchChat from '../thunks/fetchChat'; + const initialState = { input: '', error: null, @@ -20,6 +22,8 @@ const initialState = { historyLoaded: false, streamingDone: false, streaming: false, + displayQuickActions: false, + actionType: null, }; const chatSlice = createSlice({ @@ -125,6 +129,30 @@ const chatSlice = createSlice({ setExerciseId: (state, action) => { state.exerciseId = action.payload; }, + setDisplayQuickActions: (state, action) => { + state.displayQuickActions = action.payload; + }, + setActionType: (state, action) => { + state.actionType = action.payload; + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchChat.pending, (state) => { + state.sessionLoaded = false; + state.error = null; + }) + .addCase(fetchChat.fulfilled, (state, action) => { + state.chat = action.payload; + state.sessionLoaded = true; + state.error = null; + }) + .addCase(fetchChat.rejected, (state, action) => { + state.chat = {}; + state.sessionLoaded = true; + console.error(action.error); + state.error = 'Could not fetch chat. Please try again.'; + }); }, }); @@ -151,6 +179,8 @@ export const { resetExplainMyAnswer, setStreaming, setHistoryLoaded, + setDisplayQuickActions, + setActionType, } = chatSlice.actions; export default chatSlice.reducer; diff --git a/frontend/redux/slices/historySlice.js b/frontend/redux/slices/historySlice.js new file mode 100644 index 000000000..272f9cbe3 --- /dev/null +++ b/frontend/redux/slices/historySlice.js @@ -0,0 +1,67 @@ +import { createSlice } from '@reduxjs/toolkit'; + +import fetchHistory from '../thunks/fetchHistory'; + +/** + * Slice for managing the user's chat history. + */ +const historySlice = createSlice({ + name: 'history', + initialState: { + history: [], // The user's chat history. + historyLoaded: false, // Indicates whether the history has been loaded from the server. + error: null, // Error message indicating a failure to fetch history. + }, + reducers: { + /** + * Adds a new chat history entry to the beginning of the state's history array. + */ + addHistoryEntry: (state, action) => { + // Add the new chat history entry to the beginning of the history array. + state.history.unshift(action.payload); + }, + /** + * Updates a chat history entry in the state's history array. + */ + updateHistoryEntry: (state, action) => { + // Destructure the payload object from the action + const { id, updatedAt } = action.payload; + + // Find the index of the chat history entry with the matching ID + const index = state.history.findIndex((item) => item.id === id); + + // If a matching chat history entry is found + if (index !== -1) { + // Store a reference to the old chat history data + const oldHistoryData = state.history[index]; + + // Remove the old chat history entry from the history array + state.history.splice(index, 1); + + // Add a new chat history entry to the beginning of the history array with the updated timestamp + state.history.unshift({ ...oldHistoryData, updatedAt }); + } + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchHistory.pending, (state) => { + state.historyLoaded = false; + state.error = null; + }) + .addCase(fetchHistory.fulfilled, (state, action) => { + state.historyLoaded = true; + state.history = action.payload; + state.error = null; + }) + .addCase(fetchHistory.rejected, (state, action) => { + state.historyLoaded = true; + console.error(action.error); + state.error = 'Could not fetch history. Please try again.'; + }); + }, +}); + +export const { addHistoryEntry, updateHistoryEntry } = historySlice.actions; + +export default historySlice.reducer; diff --git a/frontend/redux/store.js b/frontend/redux/store.js index 2e5b0cd5a..8f61a3e60 100644 --- a/frontend/redux/store.js +++ b/frontend/redux/store.js @@ -7,6 +7,7 @@ import { connectFunctionsEmulator, getFunctions } from 'firebase/functions'; import authReducer from './slices/authSlice'; import chatReducer from './slices/chatSlice'; +import historyReducer from './slices/historySlice'; import toolsReducer from './slices/toolsSlice'; import userReducer from './slices/userSlice'; @@ -31,6 +32,7 @@ const store = configureStore({ user: userReducer, tools: toolsReducer, chat: chatReducer, + history: historyReducer, }, }); diff --git a/frontend/redux/thunks/fetchChat.js b/frontend/redux/thunks/fetchChat.js new file mode 100644 index 000000000..efa5f6e08 --- /dev/null +++ b/frontend/redux/thunks/fetchChat.js @@ -0,0 +1,34 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { doc, getDoc, getFirestore } from 'firebase/firestore'; + +/** + * Async thunk to fetch a chat session from Firestore. + * + * @param {string} chatId The ID of the chat session to fetch. + */ +const fetchChat = createAsyncThunk('chat/fetchChat', async (chatId) => { + // Get the Firestore instance + const firestore = getFirestore(); + + // Get the document reference for the chat session + const docID = doc(firestore, 'chatSessions', chatId); + + // Retrieve the chat session data from the document + const chatSessionRef = await getDoc(docID); + const chatSession = chatSessionRef.data(); + + // Convert Firestore timestamps to JavaScript Date objects and format as ISO strings + chatSession.createdAt = chatSession.createdAt.toDate().toISOString(); + chatSession.updatedAt = chatSession.updatedAt.toDate().toISOString(); + + // Convert each message timestamp from a Firestore Timestamp object to a JavaScript Date object and format it as an ISO string. + chatSession.messages.forEach((message) => { + // Convert Firestore timestamp to JavaScript Date object and format as ISO string + message.timestamp = message.timestamp.toDate().toISOString(); + }); + + // Return the fetched chat session + return chatSession; +}); + +export default fetchChat; diff --git a/frontend/redux/thunks/fetchHistory.js b/frontend/redux/thunks/fetchHistory.js new file mode 100644 index 000000000..e6ded24e2 --- /dev/null +++ b/frontend/redux/thunks/fetchHistory.js @@ -0,0 +1,60 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { + collection, + getDocs, + getFirestore, + orderBy, + query, + where, +} from 'firebase/firestore'; + +/** + * Async thunk to fetch chat history from Firestore. + * + * @param {string} userId The ID of the user whose history is being fetched. + */ +const fetchHistory = createAsyncThunk( + 'history/fetchHistory', + async (userId) => { + // Get the Firestore instance + const firestore = getFirestore(); + + // Create a query to fetch chat sessions for the given user ID + const histQuery = query( + collection(firestore, 'chatSessions'), + where('user.id', '==', userId), + orderBy('updatedAt', 'desc') + ); + + // Execute the query and retrieve the chat sessions + const querySnapshot = await getDocs(histQuery); + + // Initialize an empty array to store the chat history + const history = []; + + // Iterate over the retrieved chat sessions + querySnapshot.forEach((doc) => { + // Extract the relevant data from each chat session + const data = doc.data(); + const { id, createdAt, updatedAt, messages } = data; + + // Create an object to store the chat history for each chat session + const chatHistory = { + id, + // Convert Firestore timestamps to JavaScript Date objects and format as ISO strings + createdAt: createdAt.toDate().toISOString(), + updatedAt: updatedAt.toDate().toISOString(), + // Extract the title from the first message in the chat session + title: messages[0]?.payload?.text, + }; + + // Add the chat history object to the history array + history.push(chatHistory); + }); + + // Return the fetched chat history + return history; + } +); + +export default fetchHistory; diff --git a/frontend/templates/Chat/CenterChatContentNoMessages/styles.js b/frontend/templates/Chat/CenterChatContentNoMessages/styles.js index 2f82be5ca..0e2eca93c 100644 --- a/frontend/templates/Chat/CenterChatContentNoMessages/styles.js +++ b/frontend/templates/Chat/CenterChatContentNoMessages/styles.js @@ -6,7 +6,7 @@ const styles = { flexDirection: 'column', justifyContent: 'center', zIndex: 0, - mt: 10, + mt: 2.5, px: { laptop: 2, desktop: 2.5, desktopMedium: 3 }, sx: { overflowY: 'auto', @@ -76,8 +76,8 @@ const styles = { item: true, justifyContent: 'center', alignItems: 'center', - mobileSmall: 6, - maxWidth: '600px', + mobileSmall: 9, + maxWidth: '80%', }, }; diff --git a/frontend/templates/Chat/Chat.jsx b/frontend/templates/Chat/Chat.jsx index a439a3b05..3edc40c77 100644 --- a/frontend/templates/Chat/Chat.jsx +++ b/frontend/templates/Chat/Chat.jsx @@ -5,6 +5,8 @@ import { InfoOutlined, Settings, } from '@mui/icons-material'; +import AddIcon from '@mui/icons-material/Add'; + import { Button, Fade, @@ -23,14 +25,19 @@ import NavigationIcon from '@/assets/svg/Navigation.svg'; import { MESSAGE_ROLE, MESSAGE_TYPES } from '@/constants/bots'; import CenterChatContentNoMessages from './CenterChatContentNoMessages'; +import ChatHistoryWindow from './ChatHistoryWindow'; import ChatSpinner from './ChatSpinner'; +import DefaultPrompt from './DefaultPrompt'; import Message from './Message'; +import QuickActions from './QuickActions'; import styles from './styles'; import { openInfoChat, resetChat, + setActionType, setChatSession, + setDisplayQuickActions, setError, setFullyScrolled, setInput, @@ -41,6 +48,10 @@ import { setStreamingDone, setTyping, } from '@/redux/slices/chatSlice'; +import { + addHistoryEntry, + updateHistoryEntry, +} from '@/redux/slices/historySlice'; import { firestore } from '@/redux/store'; import createChatSession from '@/services/chatbot/createChatSession'; import sendMessage from '@/services/chatbot/sendMessage'; @@ -61,6 +72,8 @@ const ChatInterface = () => { streamingDone, streaming, error, + displayQuickActions, + actionType, } = useSelector((state) => state.chat); const { data: userData } = useSelector((state) => state.user); @@ -70,7 +83,6 @@ const ChatInterface = () => { const chatMessages = currentSession?.messages; const showNewMessageIndicator = !fullyScrolled && streamingDone; - const startConversation = async (message) => { // Optionally dispatch a temporary message for the user's input dispatch( @@ -79,9 +91,9 @@ const ChatInterface = () => { message, }) ); - + dispatch(setTyping(true)); - + // Define the chat payload const chatPayload = { user: { @@ -92,19 +104,40 @@ const ChatInterface = () => { type: 'chat', message, }; - + // Send a chat session const { status, data } = await createChatSession(chatPayload, dispatch); - + // Remove typing bubble dispatch(setTyping(false)); if (status === 'created') dispatch(setStreaming(true)); - + // Set chat session dispatch(setChatSession(data)); dispatch(setSessionLoaded(true)); + + /** + * Creates a new entry in the history store. + * The entry contains the session ID, the first message of the session, + * the creation and update timestamps of the session. + * + * @param {Object} data The session data object. + */ + const newEntry = { + // The ID of the session. + id: data?.id, + // The first message of the session. + title: data?.messages[0]?.payload?.text, + // The timestamp of session creation. + createdAt: data?.createdAt, + // The timestamp of session last update. + updatedAt: data?.updatedAt, + }; + + // Add the new history entry to the Redux store. + dispatch(addHistoryEntry(newEntry)); }; - + useEffect(() => { return () => { localStorage.removeItem('sessionId'); @@ -136,6 +169,19 @@ const ChatInterface = () => { const updatedMessages = updatedData.messages; const lastMessage = updatedMessages[updatedMessages.length - 1]; + // Convert Firestore timestamp to JavaScript Date object and format it as an ISO string. + lastMessage.timestamp = lastMessage.timestamp + .toDate() + .toISOString(); + + // Update the history entry with the latest timestamp. + dispatch( + updateHistoryEntry({ + id: sessionId, + updatedAt: updatedData.updatedAt.toDate().toISOString(), + // updatedAt: lastMessage.timestamp, + }) + ); if (lastMessage?.role === MESSAGE_ROLE.AI) { dispatch( @@ -195,9 +241,10 @@ const ChatInterface = () => { type: MESSAGE_TYPES.TEXT, payload: { text: input, + action: actionType, }, }; - + if (!chatMessages) { // Start a new conversation if there are no existing messages await startConversation(message); @@ -208,7 +255,7 @@ const ChatInterface = () => { dispatch( setMessages({ role: MESSAGE_ROLE.HUMAN, - message + message, }) ); @@ -218,6 +265,7 @@ const ChatInterface = () => { setTimeout(async () => { await sendMessage({ message, id: sessionId }, dispatch); }, 0); + dispatch(setActionType(null)); }; const handleQuickReply = async (option) => { @@ -229,6 +277,7 @@ const ChatInterface = () => { type: MESSAGE_TYPES.QUICK_REPLY, payload: { text: option, + action: actionType, }, }; @@ -240,6 +289,8 @@ const ChatInterface = () => { dispatch(setTyping(true)); await sendMessage({ message, id: currentSession?.id }, dispatch); + + dispatch(setActionType(null)); }; /* Push Enter */ @@ -340,10 +391,39 @@ const ChatInterface = () => { ); }; + /** + * Render the Quick Action component as an InputAdornment. + * This component is used to toggle the display of the Quick Actions. + * + * @return {JSX.Element} The rendered Quick Action component. + */ + const renderQuickAction = () => { + // Render the Quick Action component as an InputAdornment. + return ( + + {/* The Grid component used to display the Quick Action. */} + dispatch(setDisplayQuickActions(!displayQuickActions))} + {...styles.quickActionButton} + > + {/* Render the AddIcon component. */} + + {/* Render the Typography component to display the text. */} + Actions + + + ); + }; + const renderBottomChatContent = () => { if (!openSettingsChat && !infoChatOpened) return ( + {/* Default Prompt Component */} + + {/* Quick Actions Component */} + { disabled={!!error} focused={false} {...styles.bottomChatContent.chatInputProps( + renderQuickAction, renderSendIcon, !!error, input @@ -367,12 +448,16 @@ const ChatInterface = () => { }; return ( - - {renderMoreChat()} - {renderCenterChatContent()} - {renderCenterChatContentNoMessages()} - {renderNewMessageIndicator()} - {renderBottomChatContent()} + + + {renderMoreChat()} + {renderCenterChatContent()} + {renderCenterChatContentNoMessages()} + {renderNewMessageIndicator()} + {renderBottomChatContent()} + + {/* ChatHistoryWindow component displays a sidebar that contains chat history. This component is rendered on the right side of the chat interface. */} + ); }; diff --git a/frontend/templates/Chat/ChatHistory/ChatHistory.jsx b/frontend/templates/Chat/ChatHistory/ChatHistory.jsx new file mode 100644 index 000000000..c504ed916 --- /dev/null +++ b/frontend/templates/Chat/ChatHistory/ChatHistory.jsx @@ -0,0 +1,253 @@ +import { useEffect } from 'react'; + +import { + Grid, + List, + ListItem, + ListSubheader, + Skeleton, + Typography, +} from '@mui/material'; +import moment from 'moment'; +import { useDispatch, useSelector } from 'react-redux'; + +import styles from './styles'; + +import fetchChat from '@/redux/thunks/fetchChat'; +import fetchHistory from '@/redux/thunks/fetchHistory'; + +/** + * Categorizes chat history into different time periods. + * + * @param {Array} history The chat history to categorize. + * @return {Object} An object with properties representing different time periods and containing arrays of chat sessions within each period. + */ +const categorizeHistory = (history) => { + // Get the start of the current day + const today = moment().startOf('day'); + + // Get the start of the day before today + const yesterday = moment().subtract(1, 'days').startOf('day'); + + // Get the start of the week before today + const startOfWeek = moment().subtract(7, 'days').startOf('day'); + + // Get the start of the month before today + const startOfMonth = moment().subtract(30, 'days').startOf('day'); + + // Initialize an object to hold the categorized chat history + const categorizedHistory = { + today: [], + yesterday: [], + thisWeek: [], + thisMonth: [], + older: [], + }; + + // Loop through each chat session in the history + history.forEach((session) => { + // Get the timestamp of the last update to the chat session + const updatedAt = moment(session.updatedAt); + + // Determine the time period in which the chat session was last updated + if (updatedAt.isSame(today, 'day')) { + // The chat session was updated today + categorizedHistory.today.push(session); + } else if (updatedAt.isSame(yesterday, 'day')) { + // The chat session was updated yesterday + categorizedHistory.yesterday.push(session); + } else if (updatedAt.isBetween(startOfWeek, today, null, '[]')) { + // The chat session was updated within the past week + categorizedHistory.thisWeek.push(session); + } else if (updatedAt.isBetween(startOfMonth, today, null, '[]')) { + // The chat session was updated within the past month + categorizedHistory.thisMonth.push(session); + } else { + // The chat session was updated before the past month + categorizedHistory.older.push(session); + } + }); + + // Return the categorized chat history + return categorizedHistory; +}; + +/** + * ChatHistory component displays a list of chat entries. + * @return {JSX.Element} The rendered ChatHistory component. + */ +const ChatHistory = () => { + // The current chat session from the Redux store. + const chat = useSelector((state) => state.chat.chat); + + // Get the current chat session ID from the Redux store. The ID of the current chat session. + const currentChatSessionId = chat.id; + + // The ID of the user from the Redux store. + const userId = useSelector((state) => state.user.data.id); + + // The dispatch function from Redux. + const dispatch = useDispatch(); + + // The state of whether the history is loaded from the Redux store. + const historyLoaded = useSelector((state) => state.history.historyLoaded); + + // The chat history from the Redux store. + const chatHistory = useSelector((state) => state.history.history); + + // The error message indicating a failure to fetch history from the Redux store. + const historyError = useSelector((state) => state.history.error); + + /** + * Fetches the chat history when the component mounts. + * It dispatches the fetchHistory thunk with the userId. + */ + useEffect(() => { + // Dispatch the fetchHistory thunk to fetch the chat history from the server. + dispatch(fetchHistory(userId)); + }, []); // Empty dependency array means the effect runs only once, on mount. + + /** + * Select a chat session by its document ID and update the local storage. + * + * @param {string} docId - The document ID of the chat session. + * @return {void} No return value. + */ + const selectChat = (docId) => { + // Dispatch the fetch chat action to update the chat session in the Redux store. + dispatch(fetchChat(docId)); + + // Store the selected chat session's document ID in the local storage. + localStorage.setItem('sessionId', docId); + }; + + /** + * Render the chat history into a categorized list. + * + * @return {JSX.Element} The rendered chat history list. + */ + const renderHistory = () => { + // Categorize the chat history into different time periods + const categorizedHistory = categorizeHistory(chatHistory); + + // Define the category names for the different time periods + const categoryName = { + today: 'Today', + yesterday: 'Yesterday', + thisWeek: 'This Week', + thisMonth: 'This month', + older: 'Older', + }; + + // Map over the categorized history and render a ListItem component for each entry + return ( + + { + /* Iterate over the categorized history */ + Object.entries(categorizedHistory).map( + // For each category + ([categoryKey, categorySessions]) => + // If the category has items + categorySessions.length !== 0 ? ( + {categoryName[categoryKey]} + } + // Use dense style for the List + dense + > + { + /* Map over the items in the category */ + categorySessions.map((session) => ( + selectChat(session.id)} + > + {/* Render the text of the first message in the item */} + + {session.title} + + + )) + } + + ) : null + ) + } + + ); + }; + + /** + * Render a NoChatHistory component if the history array is empty. + * @return {JSX.Element} The rendered NoChatHistory component. + */ + const NoChatHistory = () => ( + // Grid container to center the component + + {/* Typography component to display the message */} + + {/* Display the message */} + No Chat History + + + ); + + /** + * Render a ErrorMessage component with an error message. + * + * @param {string} errorMessage The error message to display. + * @return {JSX.Element} The rendered ErrorMessage component. + */ + const ErrorMessage = (errorMessage) => ( + + {/* Typography component to display the error message */} + {errorMessage} + + ); + + /** + * Generates a skeleton component to be displayed while the chat history is being loaded. + * @return {JSX.Element} A Grid container containing Skeleton components. + */ + const skeleton = () => ( + // Grid container with 100% height + + {/* Map over an array of length 3 */} + {Array.from({ length: 3 }).map((_, index) => ( + // Skeleton component with specified props + + ))} + + ); + + // Handles rendering of chat history, error messages, and skeleton loader + if (historyLoaded) { + if (historyError) { + // If there's an error, display the error message + return ErrorMessage(historyError); + } + + // Render the chat history component if the history is loaded + if (chatHistory.length === 0) { + // If there's no chat history, display "No Chat History". + return NoChatHistory(); + } + + // Otherwise, render the chat history component + return renderHistory(); + } + + // Render the skeleton loader if the history is not loaded + return skeleton(); +}; + +export default ChatHistory; diff --git a/frontend/templates/Chat/ChatHistory/index.js b/frontend/templates/Chat/ChatHistory/index.js new file mode 100644 index 000000000..16deff6d8 --- /dev/null +++ b/frontend/templates/Chat/ChatHistory/index.js @@ -0,0 +1 @@ +export { default } from './ChatHistory'; diff --git a/frontend/templates/Chat/ChatHistory/styles.js b/frontend/templates/Chat/ChatHistory/styles.js new file mode 100644 index 000000000..ee52b31c0 --- /dev/null +++ b/frontend/templates/Chat/ChatHistory/styles.js @@ -0,0 +1,48 @@ +/** + * Styles for the chat history list item text. + */ +const styles = { + /** + * Styles for the chat history text. + */ + chatHistoryText: { + sx: { + width: '100%', // Set the width of the chat history list item text to 100%. + whiteSpace: 'nowrap', // Prevent wrapping of the text to the next line. + overflow: 'hidden', // Hide any overflowing content. + textOverflow: 'ellipsis', // If the text overflows, hide the part that does and add ellipsis at the end to indicate that the text has been truncated. + padding: '5px 0', // Set the padding to 5px 0. + cursor: 'pointer', // Set the cursor to pointer. + }, + }, + /** + * Style for the chat history text that is currently selected. + */ + chatHistoryTextCurrent: { + sx: { + color: 'rgba(115,80,255,255)', // Set the font color to rgba(115,80,255,255). + width: '100%', // Set the width of the chat history list item text to 100%. + whiteSpace: 'nowrap', // Prevent wrapping of the text to the next line. + overflow: 'hidden', // Hide any overflowing content. + textOverflow: 'ellipsis', // If the text overflows, hide the part that does and add ellipsis at the end to indicate that the text has been truncated. + padding: '5px 0', // Set the padding to 5px 0. + cursor: 'pointer', // Set the cursor to pointer. + }, + }, + + /** + * Style for centering a chat message. + */ + centerChatMessage: { + container: true, + justifyContent: 'center', // Center the container vertically. + alignItems: 'center', // Center the container horizontally. + sx: { + width: '100%', // Set the width of the container to 100%. + height: '100%', // Set the height of the container to 100%. + textAlign: 'center', // Center the text within the container. + }, + }, +}; + +export default styles; diff --git a/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx new file mode 100644 index 000000000..c5633fd39 --- /dev/null +++ b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; + +import AddIcon from '@mui/icons-material/Add'; +import ChatIcon from '@mui/icons-material/Chat'; +import RemoveIcon from '@mui/icons-material/Remove'; +import { Button, Fab, Grid, Tooltip, Typography } from '@mui/material'; + +import { useDispatch } from 'react-redux'; + +import ChatHistory from '../ChatHistory'; + +import styles from './styles'; + +import { resetChat } from '@/redux/slices/chatSlice'; + +/** + * ChatHistoryWindow component displays a sidebar that contains chat history. + * The sidebar is toggled by clicking on the toggle button. + */ +const ChatHistoryWindow = () => { + // State variable to track whether the chat history sidebar is shown or hidden. Initially set to false. + const [showHistorySidebar, setShowHistorySidebar] = useState(false); + + // The dispatch function from the Redux store. Used to dispatch actions to the store. + const dispatch = useDispatch(); + + /** + * Toggles the visibility of the chat history sidebar. + * + * @return {void} No return value. + */ + const toggleHistorySidebar = () => setShowHistorySidebar((prev) => !prev); + + /** + * Resets the chat state and removes the session ID from local storage. + * + * @return {void} No return value. + */ + const newChat = () => { + // Dispatch the resetChat action to reset the chat state + dispatch(resetChat()); + + // Remove the session ID from local storage + localStorage.removeItem('sessionId'); + }; + + return ( + // Conditionally render the chat history sidebar based on the showHistorySidebar state + !showHistorySidebar ? ( + // Render the open chat history button + + + + ) : ( + // Render the chat history sidebar + + {/* Header of the sidebar */} + + + newChat()} + > + + + + + + {/* Title of the chat history sidebar */} + + {/* Display the title of the chat history sidebar */} + + {/* Display 'Chat History' */} + Chat History + + + {/* Close chat history button */} + + + + + + {/* Add new chat button */} + {/* Add new chat button with onClick event handler to reset the state of the chat reducer. */} + {/* + + */} + + {/* Chat history section of the sidebar */} + + {/* Render the ChatHistory component */} + + + + ) + ); +}; + +export default ChatHistoryWindow; diff --git a/frontend/templates/Chat/ChatHistoryWindow/index.js b/frontend/templates/Chat/ChatHistoryWindow/index.js new file mode 100644 index 000000000..4d40b91a9 --- /dev/null +++ b/frontend/templates/Chat/ChatHistoryWindow/index.js @@ -0,0 +1 @@ +export { default } from './ChatHistoryWindow'; diff --git a/frontend/templates/Chat/ChatHistoryWindow/styles.js b/frontend/templates/Chat/ChatHistoryWindow/styles.js new file mode 100644 index 000000000..8d878b9da --- /dev/null +++ b/frontend/templates/Chat/ChatHistoryWindow/styles.js @@ -0,0 +1,170 @@ +/** + * Contains styles for various components related to the chat history window. + */ +const styles = { + /** + * Styles for the chat history sidebar. + */ + historySideBar: { + item: true, + sx: { + // Set the display property to flex and arrange the children in a column. + display: 'flex', + // Set the flex direction to column. + flexDirection: 'column', + // Set the border to 5px solid rgba(115,80,255,255). + border: '5px solid rgba(115,80,255,255)', + // Set the border radius. + borderRadius: '15px', + // Set the background to black. + backgroundColor: '#000000', + // Set the min-width to 25%. + minWidth: '25%', + // Set the height to 100%. + height: '100%', + // Set the color to white. + color: '#ffffff', + // Set the max width of the sidebar to 25%. + maxWidth: '25%', + }, + }, + /** + * Styles for the header of the chat history sidebar. + */ + historySideBarHeader: { + sx: { + // Set the display property to flex and arrange the children in a row. + display: 'flex', + // Set the flex direction to row. + flexDirection: 'row', + // Align the children along the space-around of the header. + justifyContent: 'space-around', + // Align the children along the center of the header vertically. + alignItems: 'center', + }, + }, + /** + * Styles for the title of the chat history sidebar. + */ + historySideBarTitle: { + sx: { + // Display the title only when the chat history sidebar is shown. + display: 'flex', + // The flexGrow property is set to 1 to make the title span the remaining space in the chat history sidebar header. + flexGrow: 1, + // Center the title horizontally. + justifyContent: 'center', + }, + }, + /** + * Styles for the title text of the chat history sidebar. + */ + historySideBarTitleText: { + // Center the title text horizontally. + textAlign: 'center', + }, + /** + * Styles for the toggle history button. + * + * @param {boolean} showHistorySidebar Whether the chat history sidebar is shown or hidden. + */ + toggleHistoryButton: (showHistorySidebar) => ({ + sx: { + // Set the background color to black. + backgroundColor: '#000000', + // Set the text color to a custom color. + color: 'rgba(115,80,255,255)', + // Adjust the border of the toggle button based on the value of showHistorySidebar. + // If showHistorySidebar is true, set the border to none. + // If showHistorySidebar is false, set the border to 5px solid rgba(115,80,255,255). + border: showHistorySidebar ? 'none' : '5px solid rgba(115,80,255,255)', + // Styles for the toggle history button when it is being hovered over. + '&:hover': { + // Set the background color to black on hover. + backgroundColor: '#000000', + // Set the text color on hover. + color: 'rgba(115,80,255,255)', + }, + }, + }), + /** + * Styles for the chat history. + */ + chatHistory: { + item: true, + sx: { + // Display the sidebar only when showHistorySidebar is true + display: 'block', + // Set the background color to rgba(24,26,32,255) + backgroundColor: 'rgba(24,26,32,255)', + // Set the height of the sidebar to 100% + height: '100%', + // Set the width of the sidebar to 100% + width: '100%', + // Enable vertical scrolling if the content overflows + overflowY: 'auto', + // Set the border radius. + borderRadius: '0px 0px 15px 15px', + // Add a smooth transition when the sidebar is opened or closed + transition: 'all 0.3s ease', + }, + }, + + /** + * Styles for the new chat container. This container is used to center the new chat button vertically and horizontally. + */ + newChatContainer: { + item: true, + sx: { + // Set the display property to flex and arrange the children in a column. + display: 'flex', + // Center the children horizontally. + justifyContent: 'center', + // Center the children vertically. + alignItems: 'center', + // Set the width of the container to 100%. + width: '100%', + // Set the padding of the container to 5px at the top and bottom. + // padding: '5px 0', + }, + }, + + /** + * Styles for the new chat button. This button is used to open the new chat modal. + */ + newChatButton: { + sx: { + // Set the display property to flex and arrange the children in a row. + display: 'flex', + // Align the children along the center of the button horizontally. + justifyContent: 'space-around', + // Set the color of the button to white. + color: '#ffffff', + // Set the background color of the button to rgba(115,80,255,255). + backgroundColor: 'rgba(115,80,255,255)', + width: '100%', + // Set the color of the button to white when hovered over. + '&:hover': { + color: '#ffffff', + backgroundColor: 'rgba(115,80,255,255)', + }, + }, + }, + newChatIcon: { + sx: { + // Set the background color to black. + backgroundColor: '#000000', + // Set the text color to a custom color. + color: 'rgba(115,80,255,255)', + // Styles for the toggle history button when it is being hovered over. + '&:hover': { + // Set the background color to black on hover. + backgroundColor: '#000000', + // Set the text color on hover. + color: 'rgba(115,80,255,255)', + }, + }, + }, +}; + +export default styles; diff --git a/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx new file mode 100644 index 000000000..b0bc0ed61 --- /dev/null +++ b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from 'react'; + +import { Grid, Typography } from '@mui/material'; + +import { useDispatch, useSelector } from 'react-redux'; + +import { DEFAULT_PROMPTS } from '@/constants/bots'; + +import styles from './styles'; + +import { setInput } from '@/redux/slices/chatSlice'; + +/** + * Renders the default prompt component. + * + * This component displays a set of pre-defined prompts for the user to choose from. + * + * @param {function} handleSendMessage - The function to handle sending a message when a prompt is selected. + * @return {JSX.Element|null} The default prompt component or null if there is user input. + */ +const DefaultPrompt = ({ handleSendMessage }) => { + // Get the user input and typing from the Redux store + const { + input: userInput, // The user's input + typing, // Whether the user is typing + sessionLoaded, // Whether the session is loaded + } = useSelector((state) => state.chat); + + const [readyToSend, setReadyToSend] = useState(false); // Whether the prompt is ready to be sent + + // Get the dispatch function from Redux + const dispatch = useDispatch(); + + /** + * Handles the click event of a default prompt. + * + * @param {string} promptSelected - The selected default prompt. + */ + const handleClick = (promptSelected) => { + // Set the user input to the selected default prompt + dispatch(setInput(promptSelected)); + + // Set the readyToSend state to true + setReadyToSend(true); + }; + + useEffect(() => { + if (readyToSend) { + // Handle sending the message when the prompt is ready to be sent + handleSendMessage(); + setReadyToSend(false); + } + }, [readyToSend]); + + // Define the default prompts + const defaultPrompts = DEFAULT_PROMPTS; + /* const defaultPrompts = [ + 'Help me sound like an expert for an upcoming meeting', + 'Suggest a way to organize my code in Github', + 'Brainstorm presentation ideas about a topic', + 'List power words for my resume that show teamwork', + ]; */ + + /** + * Renders the default prompt component. + * + * @return {JSX.Element|null} The default prompt component or null if there is user input. + */ + return !sessionLoaded && !typing && userInput.length === 0 ? ( + // Render the grid container for the default prompts if there is no user input + + {/* Map over the default prompts and render a grid for each prompt */} + {defaultPrompts.map((prompt, key) => { + return ( + // Render a grid item for each default prompt + handleClick(prompt)} + {...styles.defaultPrompt} + > + {/* Render the prompt text */} + {prompt} + + ); + })} + + ) : null; +}; + +export default DefaultPrompt; diff --git a/frontend/templates/Chat/DefaultPrompt/index.js b/frontend/templates/Chat/DefaultPrompt/index.js new file mode 100644 index 000000000..db9f63946 --- /dev/null +++ b/frontend/templates/Chat/DefaultPrompt/index.js @@ -0,0 +1 @@ +export { default } from './DefaultPrompt'; diff --git a/frontend/templates/Chat/DefaultPrompt/styles.js b/frontend/templates/Chat/DefaultPrompt/styles.js new file mode 100644 index 000000000..603afdb62 --- /dev/null +++ b/frontend/templates/Chat/DefaultPrompt/styles.js @@ -0,0 +1,62 @@ +/** + * Styles for the DefaultPrompt component + */ +const styles = { + /** + * Styles for the container of the default prompt + */ + defaultPromptsGridContainer: { + container: true, + sx: { + width: '90%', + display: 'flex', + flexDirection: 'row', + gap: '10px', + marginBottom: '10px', + }, + }, + /** + * Styles for a single default prompt + */ + defaultPrompt: { + container: true, + item: true, + sx: { + position: 'relative', + padding: '5px 10px', + paddingLeft: '40px', + display: 'flex', + flexDirection: 'row', + flex: '1', + backgroundColor: 'transparent', + color: '#5e20f4', + border: '2px solid #5e20f4', + borderRadius: '10px', + cursor: 'pointer', + alignItems: 'center', + transition: 'background-color 0.3s ease, color 0.3s ease', + '&:hover': { + backgroundColor: '#5e20f4', + color: 'white', + '&::before': { + background: 'white', + transition: 'background-color 0.3s ease, color 0.3s ease', + }, + }, + '&::before': { + content: '""', + position: 'absolute', + width: '23px', + height: '23px', + left: '8px', + top: '50%', + transform: 'translateY(-50%)', + background: '#5e20f4', + clipPath: + 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)', + }, + }, + }, +}; + +export default styles; diff --git a/frontend/templates/Chat/QuickActions/QuickActions.jsx b/frontend/templates/Chat/QuickActions/QuickActions.jsx new file mode 100644 index 000000000..018c57422 --- /dev/null +++ b/frontend/templates/Chat/QuickActions/QuickActions.jsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react'; + +import { ClickAwayListener, Grid, Typography } from '@mui/material'; + +import { useDispatch, useSelector } from 'react-redux'; + +import { ACTION_TYPES } from '@/constants/bots'; + +import styles from './styles'; + +import { + setActionType, + setDisplayQuickActions, + setInput, +} from '@/redux/slices/chatSlice'; + +/** + * QuickActions component that displays a list of quick actions that the user can select. + * If displayQuickActions is true, the QuickActions component is rendered. + * If displayQuickActions is false, null is returned. + * + * @param {function} handleSendMessage The function to handle sending a message when an action is selected. + * @return {JSX.Element|null} The QuickActions component or null if displayQuickActions is false. + */ +const QuickActions = ({ handleSendMessage }) => { + // Get the state variables from Redux store + const { input, displayQuickActions } = useSelector((state) => state.chat); + + // State variable to track whether the action is ready to be sent + const [readyToSend, setReadyToSend] = useState(false); + + // The function to dispatch Redux actions. + const dispatch = useDispatch(); + + // The list of quick action types. + const quickActions = ACTION_TYPES; + + // Effect to handle sending the message when readyToSend is true + useEffect(() => { + if (readyToSend) { + handleSendMessage(); + + setReadyToSend(false); + } + }, [readyToSend]); + + /** + * Handle action click event of the QuickActions component. + * When an action is clicked, the QuickActions component is closed and the selected action is dispatched as input to the chat. + * + * @param {string} quickActionSelected The selected quick action. + * @return {void} + */ + const handleActionClick = (quickActionSelected) => { + // Construct the new input string + const newInput = `${input}\n\n${quickActionSelected.description}`; + + // Dispatch the selected action as input to the chat + dispatch(setInput(newInput)); + + // Dispatch the selected action as the current action type + dispatch(setActionType(quickActionSelected.actionType)); + + // Close the QuickActions component + dispatch(setDisplayQuickActions(false)); + + setReadyToSend(true); + }; + + /** + * Handle close event of the QuickActions component. + * Closes the QuickActions component by dispatching an action to Redux. + * + * @return {void} + */ + const handleClose = () => { + // Dispatch an action to Redux to hide the QuickActions component. + dispatch(setDisplayQuickActions(false)); + }; + + /** + * Renders the QuickActions component if displayQuickActions is true. + * If displayQuickActions is false, returns null. + * + * @return {JSX.Element|null} The QuickActions component or null. + */ + return displayQuickActions ? ( + // Wrap the QuickActions component with ClickAwayListener to handle clicks outside the component + // ClickAwayListener triggers the handleClose function when a click occurs outside the component + + {/* Render the Grid container for the QuickActions component */} + + {/* Render each quick action as a Grid item */} + {Object.values(quickActions).map((action, key) => { + // Generate a quick action component for each action + // The quick action component is a Grid item that triggers the handleActionClick function when clicked + return ( + handleActionClick(action)} + {...styles.quickAction} + > + {/* Render the name of the quick action */} + {action.actionType} + + ); + })} + + + ) : null; +}; + +export default QuickActions; diff --git a/frontend/templates/Chat/QuickActions/index.js b/frontend/templates/Chat/QuickActions/index.js new file mode 100644 index 000000000..128c56833 --- /dev/null +++ b/frontend/templates/Chat/QuickActions/index.js @@ -0,0 +1 @@ +export { default } from './QuickActions'; diff --git a/frontend/templates/Chat/QuickActions/styles.js b/frontend/templates/Chat/QuickActions/styles.js new file mode 100644 index 000000000..5976b0dbe --- /dev/null +++ b/frontend/templates/Chat/QuickActions/styles.js @@ -0,0 +1,40 @@ +/** + * Styles for the QuickActions component + */ +const styles = { + /** + * Styles for the container of the quick actions + */ + quickActionsGridContainer: { + item: true, + sx: { + padding: '5px', + display: 'flex', + flexDirection: 'row', + gap: '10px', + marginBottom: '10px', + border: '3px solid #9f86fe', + borderRadius: '10px', + }, + }, + /** + * Styles for a single quick action + */ + quickAction: { + item: true, + sx: { + padding: '10px', + backgroundColor: '#25262f', + color: '#9e94a5', + border: 'none', + borderRadius: '10px', + cursor: 'pointer', + transition: 'background-color 0.3s ease, color 0.3s ease', + '&:hover': { + color: '#ffffff', + }, + }, + }, +}; + +export default styles; diff --git a/frontend/templates/Chat/styles.js b/frontend/templates/Chat/styles.js index bfeaaba83..869ef9c91 100644 --- a/frontend/templates/Chat/styles.js +++ b/frontend/templates/Chat/styles.js @@ -1,4 +1,14 @@ const styles = { + chatInterface: { + container: true, + height: '100%', + width: '100%', + direction: 'row', + sx: { + flexWrap: 'nowrap', + position: 'relative', + }, + }, mainGridProps: { container: true, item: true, @@ -9,6 +19,7 @@ const styles = { height: '100%', overflow: 'hidden', sx: { + position: 'relative', form: { width: '100%', height: '100%', @@ -214,7 +225,7 @@ const styles = { borderRadius: '50px', }, }), - chatInputProps: (renderSendIcon, error, input) => ({ + chatInputProps: (renderQuicKAction, renderSendIcon, error, input) => ({ type: 'text', placeholder: !error && 'Send a message', autoComplete: 'off', @@ -234,11 +245,12 @@ const styles = { lineHeight: '35px', }), endAdornment: renderSendIcon(), + startAdornment: renderQuicKAction(), }, FormHelperTextProps: { sx: { position: 'absolute', - transform: 'translate(55px, 30%)', + transform: 'translate(120px, 30%)', fontFamily: 'Satoshi Medium', fontSize: { mobileSmall: '16px', desktopMedium: '20px' }, lineHeight: '35px', @@ -362,6 +374,31 @@ const styles = { }, }), }, + + quickActionButton: { + sx: { + padding: '10px', + marginLeft: '-5px', + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + backgroundColor: 'rgb(88,20,244)', + color: 'white', + borderRadius: '30px', + flexWrap: 'nowrap', + gap: '10px', + transition: 'background-color 0.1s ease, color 0.1s ease', + '&:hover': { + backgroundColor: 'rgb(123, 55, 255)', + }, + }, + }, + quickActionButtonAddIcon: { + sx: { + border: '2px solid white', + borderRadius: '50%', + }, + }, }; export default styles; diff --git a/functions/controllers/kaiAIController.js b/functions/controllers/kaiAIController.js index 4c6be56f9..ab66e65ec 100644 --- a/functions/controllers/kaiAIController.js +++ b/functions/controllers/kaiAIController.js @@ -150,7 +150,11 @@ const chat = onCall(async (props) => { })) ); - await chatSession.ref.update({ messages: updatedResponseMessages }); + // Update the chat session with the updated response messages and the current timestamp. + await chatSession.ref.update({ + messages: updatedResponseMessages, // Update the messages array with the new messages and timestamps + updatedAt: Timestamp.fromMillis(Date.now()), // Set the updatedAt timestamp to the current time + }); if (DEBUG) { logger.log( @@ -344,9 +348,17 @@ const createChatSession = onCall(async (props) => { const updatedChatSession = await chatSessionRef.get(); DEBUG && logger.log('Updated chat session: ', updatedChatSession.data()); + /** + * Creates a new chat session object by extracting relevant data from the Firestore document. Converts Firestore timestamps to ISO strings and includes the document ID. + * @param {Object} updatedChatSession The Firestore document containing the chat session data. + * @return {Object} The new chat session object. + */ const createdChatSession = { - ...updatedChatSession.data(), - id: updatedChatSession.id, + ...updatedChatSession.data(), // Extract relevant data from Firestore document + // Convert Firestore timestamps to ISO strings + createdAt: updatedChatSession.data().createdAt.toDate().toISOString(), + updatedAt: updatedChatSession.data().updatedAt.toDate().toISOString(), + id: updatedChatSession.id, // Include the document ID }; DEBUG && logger.log('Created chat session: ', createdChatSession); From db9c8f363ff4ab4ee3f67d6a16626eff0e680db7 Mon Sep 17 00:00:00 2001 From: Kishore Murugan Date: Tue, 30 Jul 2024 10:41:25 -0500 Subject: [PATCH 2/7] changed the border color and star --- frontend/assets/svg/PurpleStar.svg | 14 ++++ .../Chat/DefaultPrompt/DefaultPrompt.jsx | 68 +++++++------------ .../templates/Chat/DefaultPrompt/styles.js | 54 ++++++++------- 3 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 frontend/assets/svg/PurpleStar.svg diff --git a/frontend/assets/svg/PurpleStar.svg b/frontend/assets/svg/PurpleStar.svg new file mode 100644 index 000000000..4d40140fd --- /dev/null +++ b/frontend/assets/svg/PurpleStar.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx index b0bc0ed61..08562b274 100644 --- a/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx +++ b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx @@ -1,15 +1,12 @@ import { useEffect, useState } from 'react'; - import { Grid, Typography } from '@mui/material'; - import { useDispatch, useSelector } from 'react-redux'; - import { DEFAULT_PROMPTS } from '@/constants/bots'; +import { setInput } from '@/redux/slices/chatSlice'; +import MenuLogo from '@/assets/svg/PurpleStar.svg'; // Import MenuLogo import styles from './styles'; -import { setInput } from '@/redux/slices/chatSlice'; - /** * Renders the default prompt component. * @@ -19,16 +16,14 @@ import { setInput } from '@/redux/slices/chatSlice'; * @return {JSX.Element|null} The default prompt component or null if there is user input. */ const DefaultPrompt = ({ handleSendMessage }) => { - // Get the user input and typing from the Redux store const { - input: userInput, // The user's input - typing, // Whether the user is typing - sessionLoaded, // Whether the session is loaded + input: userInput, + typing, + sessionLoaded, } = useSelector((state) => state.chat); - const [readyToSend, setReadyToSend] = useState(false); // Whether the prompt is ready to be sent + const [readyToSend, setReadyToSend] = useState(false); - // Get the dispatch function from Redux const dispatch = useDispatch(); /** @@ -37,54 +32,41 @@ const DefaultPrompt = ({ handleSendMessage }) => { * @param {string} promptSelected - The selected default prompt. */ const handleClick = (promptSelected) => { - // Set the user input to the selected default prompt dispatch(setInput(promptSelected)); - - // Set the readyToSend state to true setReadyToSend(true); }; useEffect(() => { if (readyToSend) { - // Handle sending the message when the prompt is ready to be sent handleSendMessage(); setReadyToSend(false); } }, [readyToSend]); - // Define the default prompts const defaultPrompts = DEFAULT_PROMPTS; - /* const defaultPrompts = [ - 'Help me sound like an expert for an upcoming meeting', - 'Suggest a way to organize my code in Github', - 'Brainstorm presentation ideas about a topic', - 'List power words for my resume that show teamwork', - ]; */ - /** - * Renders the default prompt component. - * - * @return {JSX.Element|null} The default prompt component or null if there is user input. - */ return !sessionLoaded && !typing && userInput.length === 0 ? ( - // Render the grid container for the default prompts if there is no user input - - {/* Map over the default prompts and render a grid for each prompt */} - {defaultPrompts.map((prompt, key) => { - return ( - // Render a grid item for each default prompt - handleClick(prompt)} - {...styles.defaultPrompt} - > + + {defaultPrompts.map((prompt, key) => ( + handleClick(prompt)} + {...styles.defaultPrompt} + > +
+ {/* Render the MenuLogo SVG */} + {/* Render the prompt text */} - {prompt} - - ); - })} + {prompt} +
+
+ ))}
) : null; }; -export default DefaultPrompt; +export default DefaultPrompt; \ No newline at end of file diff --git a/frontend/templates/Chat/DefaultPrompt/styles.js b/frontend/templates/Chat/DefaultPrompt/styles.js index 603afdb62..cf687dc5f 100644 --- a/frontend/templates/Chat/DefaultPrompt/styles.js +++ b/frontend/templates/Chat/DefaultPrompt/styles.js @@ -1,10 +1,9 @@ +import { MarginOutlined } from "@mui/icons-material"; + /** * Styles for the DefaultPrompt component */ const styles = { - /** - * Styles for the container of the default prompt - */ defaultPromptsGridContainer: { container: true, sx: { @@ -13,50 +12,57 @@ const styles = { flexDirection: 'row', gap: '10px', marginBottom: '10px', + marginLeft:'10px' }, }, - /** - * Styles for a single default prompt - */ defaultPrompt: { container: true, item: true, sx: { position: 'relative', padding: '5px 10px', - paddingLeft: '40px', display: 'flex', - flexDirection: 'row', + alignItems: 'center', flex: '1', backgroundColor: 'transparent', color: '#5e20f4', - border: '2px solid #5e20f4', + border: 'none', borderRadius: '10px', cursor: 'pointer', - alignItems: 'center', transition: 'background-color 0.3s ease, color 0.3s ease', + '&::before': { + content: '""', + position: 'absolute', + inset: 0, + borderRadius: '10px', + padding: '2px', + background: 'linear-gradient(45deg, #a597cc, #5e20f3)', + WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', + WebkitMaskComposite: 'xor', + maskComposite: 'exclude', + }, '&:hover': { backgroundColor: '#5e20f4', color: 'white', - '&::before': { + '&::after': { background: 'white', transition: 'background-color 0.3s ease, color 0.3s ease', }, }, - '&::before': { - content: '""', - position: 'absolute', - width: '23px', - height: '23px', - left: '8px', - top: '50%', - transform: 'translateY(-50%)', - background: '#5e20f4', - clipPath: - 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)', - }, }, }, + menuLogo: { + marginRight: '10px', + }, + promptContent: { + display: 'flex', + alignItems: 'center', + width: '100%', + }, + promptText: { + flex: 1, + overflow: 'hidden', + }, }; -export default styles; +export default styles; \ No newline at end of file From 62228cb5d345ef07e0333e5bb4df3ad6dd95d992 Mon Sep 17 00:00:00 2001 From: Viraj Patel Date: Tue, 30 Jul 2024 15:34:35 -0400 Subject: [PATCH 3/7] Add the Discovery Library Section. --- frontend/templates/Chat/Chat.jsx | 2 + .../ChatHistoryWindow/ChatHistoryWindow.jsx | 62 ++++++++-------- .../Chat/ChatHistoryWindow/styles.js | 15 +++- .../Chat/DefaultPrompt/DefaultPrompt.jsx | 10 ++- .../templates/Chat/DefaultPrompt/styles.js | 10 +-- .../DiscoveryLibraryWindow.jsx | 73 +++++++++++++++++++ .../Chat/DiscoveryLibraryWindow/index.js | 1 + .../Chat/DiscoveryLibraryWindow/styles.js | 73 +++++++++++++++++++ 8 files changed, 200 insertions(+), 46 deletions(-) create mode 100644 frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx create mode 100644 frontend/templates/Chat/DiscoveryLibraryWindow/index.js create mode 100644 frontend/templates/Chat/DiscoveryLibraryWindow/styles.js diff --git a/frontend/templates/Chat/Chat.jsx b/frontend/templates/Chat/Chat.jsx index 3edc40c77..aefc3c593 100644 --- a/frontend/templates/Chat/Chat.jsx +++ b/frontend/templates/Chat/Chat.jsx @@ -28,6 +28,7 @@ import CenterChatContentNoMessages from './CenterChatContentNoMessages'; 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'; @@ -449,6 +450,7 @@ const ChatInterface = () => { return ( + {renderMoreChat()} {renderCenterChatContent()} diff --git a/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx index c5633fd39..2fef41a77 100644 --- a/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx +++ b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx @@ -1,9 +1,9 @@ import { useState } from 'react'; -import AddIcon from '@mui/icons-material/Add'; import ChatIcon from '@mui/icons-material/Chat'; +import HistoryIcon from '@mui/icons-material/History'; import RemoveIcon from '@mui/icons-material/Remove'; -import { Button, Fab, Grid, Tooltip, Typography } from '@mui/material'; +import { Fab, Grid, Tooltip, Typography } from '@mui/material'; import { useDispatch } from 'react-redux'; @@ -47,26 +47,30 @@ const ChatHistoryWindow = () => { return ( // Conditionally render the chat history sidebar based on the showHistorySidebar state !showHistorySidebar ? ( - // Render the open chat history button - - - + + {/* // Render the open chat history button */} + + + + + + ) : ( // Render the chat history sidebar {/* Header of the sidebar */} - newChat()} - > - + + newChat()} + {...styles.newChatIcon} + > @@ -80,25 +84,17 @@ const ChatHistoryWindow = () => { {/* Close chat history button */} - - - + + + + + - {/* Add new chat button */} - {/* Add new chat button with onClick event handler to reset the state of the chat reducer. */} - {/* - - */} - {/* Chat history section of the sidebar */} {/* Render the ChatHistory component */} diff --git a/frontend/templates/Chat/ChatHistoryWindow/styles.js b/frontend/templates/Chat/ChatHistoryWindow/styles.js index 8d878b9da..2546d45d0 100644 --- a/frontend/templates/Chat/ChatHistoryWindow/styles.js +++ b/frontend/templates/Chat/ChatHistoryWindow/styles.js @@ -2,6 +2,13 @@ * Contains styles for various components related to the chat history window. */ const styles = { + openChatHistoryContainer: { + item: true, + sx: { + width: 'fit-content', + height: 'fit-content', + }, + }, /** * Styles for the chat history sidebar. */ @@ -18,14 +25,14 @@ const styles = { borderRadius: '15px', // Set the background to black. backgroundColor: '#000000', - // Set the min-width to 25%. - minWidth: '25%', + // Set the min-width to 20%. + minWidth: '20%', + // Set the max width to 20%. + maxWidth: '20%', // Set the height to 100%. height: '100%', // Set the color to white. color: '#ffffff', - // Set the max width of the sidebar to 25%. - maxWidth: '25%', }, }, /** diff --git a/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx index 08562b274..565fb243c 100644 --- a/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx +++ b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx @@ -1,12 +1,16 @@ import { useEffect, useState } from 'react'; + import { Grid, Typography } from '@mui/material'; import { useDispatch, useSelector } from 'react-redux'; -import { DEFAULT_PROMPTS } from '@/constants/bots'; -import { setInput } from '@/redux/slices/chatSlice'; + import MenuLogo from '@/assets/svg/PurpleStar.svg'; // Import MenuLogo +import { DEFAULT_PROMPTS } from '@/constants/bots'; + import styles from './styles'; +import { setInput } from '@/redux/slices/chatSlice'; + /** * Renders the default prompt component. * @@ -69,4 +73,4 @@ const DefaultPrompt = ({ handleSendMessage }) => { ) : null; }; -export default DefaultPrompt; \ No newline at end of file +export default DefaultPrompt; diff --git a/frontend/templates/Chat/DefaultPrompt/styles.js b/frontend/templates/Chat/DefaultPrompt/styles.js index cf687dc5f..317fce0cc 100644 --- a/frontend/templates/Chat/DefaultPrompt/styles.js +++ b/frontend/templates/Chat/DefaultPrompt/styles.js @@ -1,5 +1,3 @@ -import { MarginOutlined } from "@mui/icons-material"; - /** * Styles for the DefaultPrompt component */ @@ -7,12 +5,11 @@ const styles = { defaultPromptsGridContainer: { container: true, sx: { - width: '90%', display: 'flex', flexDirection: 'row', gap: '10px', marginBottom: '10px', - marginLeft:'10px' + marginLeft: '10px', }, }, defaultPrompt: { @@ -37,7 +34,8 @@ const styles = { borderRadius: '10px', padding: '2px', background: 'linear-gradient(45deg, #a597cc, #5e20f3)', - WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', + WebkitMask: + 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', WebkitMaskComposite: 'xor', maskComposite: 'exclude', }, @@ -65,4 +63,4 @@ const styles = { }, }; -export default styles; \ No newline at end of file +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..e119e80eb --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; + +import RemoveIcon from '@mui/icons-material/Remove'; +import { Fab, Grid, Typography } from '@mui/material'; + +import styles from './styles'; + +const DiscoveryLibraryWindow = () => { + const [showDiscoveryLibrary, setShowDiscoveryLibraryWindow] = useState(false); + + const greeting = () => { + const hours = new Date().getHours(); + + if (hours < 12) { + return 'Good Morning'; + } + if (hours < 18) { + return 'Good Afternoon'; + } + return 'Good Evening'; + }; + + const toggleDiscoveryLibrarySidebar = () => + setShowDiscoveryLibraryWindow((prev) => !prev); + + return !showDiscoveryLibrary ? ( + + + + + + + + Discover + + + ) : ( + + + + + {greeting()} + + + + + + + {/* */} + + ); +}; + +export default DiscoveryLibraryWindow; diff --git a/frontend/templates/Chat/DiscoveryLibraryWindow/index.js b/frontend/templates/Chat/DiscoveryLibraryWindow/index.js new file mode 100644 index 000000000..42fdcc8a8 --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/index.js @@ -0,0 +1 @@ +export { default } from './DiscoveryLibraryWindow'; diff --git a/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js b/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js new file mode 100644 index 000000000..4f8f2d906 --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js @@ -0,0 +1,73 @@ +const styles = { + openDiscoveryLibraryContainer: { + item: true, + sx: { + width: 'fit-content', + height: 'fit-content', + '&:hover': { + cursor: 'pointer', + }, + }, + }, + discoveryLibraryWindow: { + item: true, + sx: { + display: 'flex', + flexDirection: 'column', + border: '5px solid rgba(115,80,255,255)', + borderRadius: '15px', + backgroundColor: '#000000', + maxWidth: '20%', + minWidth: '20%', + height: '100%', + color: '#ffffff', + }, + }, + discoveryLibraryWindowHeader: { + sx: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, + discoveryLibraryWindowTitle: { + sx: { + display: 'flex', + flexGrow: 1, + justifyContent: 'center', + }, + }, + discoveryLibraryWindowTitleText: { + textAlign: 'center', + }, + toggleDiscoveryLibraryWindowButton: (showDiscoveryLibraryWindow) => ({ + sx: { + padding: showDiscoveryLibraryWindow ? 'none' : '5px', + backgroundColor: '#000000', + color: 'rgba(115,80,255,255)', + border: showDiscoveryLibraryWindow + ? 'none' + : '5px solid rgba(115,80,255,255)', + borderRadius: showDiscoveryLibraryWindow ? 'none' : '15px', + '&:hover': { + backgroundColor: '#000000', + color: 'rgba(115,80,255,255)', + }, + }, + }), + discoveryLibraries: { + item: true, + sx: { + display: 'block', + backgroundColor: 'rgba(24,26,32,255)', + height: '100%', + width: '100%', + overflowY: 'auto', + borderRadius: '0px 0px 15px 15px', + transition: 'all 0.3s ease', + }, + }, +}; + +export default styles; From e33b1c020261bcec8484b61c0f3453d25d796da4 Mon Sep 17 00:00:00 2001 From: Kishore Murugan Date: Tue, 30 Jul 2024 22:05:09 -0500 Subject: [PATCH 4/7] adjusted padding and changed the border color darker --- frontend/templates/Chat/DefaultPrompt/styles.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/templates/Chat/DefaultPrompt/styles.js b/frontend/templates/Chat/DefaultPrompt/styles.js index 317fce0cc..352d3ebd0 100644 --- a/frontend/templates/Chat/DefaultPrompt/styles.js +++ b/frontend/templates/Chat/DefaultPrompt/styles.js @@ -17,7 +17,7 @@ const styles = { item: true, sx: { position: 'relative', - padding: '5px 10px', + padding: '10px', // Changed padding to be equal display: 'flex', alignItems: 'center', flex: '1', @@ -33,7 +33,7 @@ const styles = { inset: 0, borderRadius: '10px', padding: '2px', - background: 'linear-gradient(45deg, #a597cc, #5e20f3)', + background: 'linear-gradient(45deg, #8c6d9a, #5e20f3)', // Darker gradient color WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', WebkitMaskComposite: 'xor', @@ -63,4 +63,4 @@ const styles = { }, }; -export default styles; +export default styles; \ No newline at end of file From 04c84bcddc50a7ffd45fb7bdf9c6d2b4d0a90402 Mon Sep 17 00:00:00 2001 From: Viraj Patel Date: Thu, 8 Aug 2024 12:08:31 -0400 Subject: [PATCH 5/7] Fetching and displaying Discovery Library. --- .../layouts/MainAppLayout/SideMenu/styles.js | 3 +- frontend/redux/slices/chatSlice.js | 42 ++++++++++- .../redux/thunks/fetchDiscoveryLibraries.js | 26 +++++++ frontend/redux/thunks/fetchHistory.js | 11 ++- frontend/templates/Chat/Chat.jsx | 32 ++++++++ .../ChatHistoryWindow/ChatHistoryWindow.jsx | 20 +++-- .../Chat/ChatHistoryWindow/styles.js | 8 +- .../Chat/DefaultPrompt/DefaultPrompt.jsx | 10 +-- .../templates/Chat/DefaultPrompt/styles.js | 5 +- .../DiscoveryLibrary/DiscoveryLibrary.jsx | 73 ++++++++++++++++++ .../templates/Chat/DiscoveryLibrary/index.js | 1 + .../templates/Chat/DiscoveryLibrary/styles.js | 34 +++++++++ .../DiscoveryLibraryWindow.jsx | 74 ++++++++++++++++++- .../Chat/DiscoveryLibraryWindow/styles.js | 20 ++++- functions/controllers/kaiAIController.js | 28 ++++++- 15 files changed, 349 insertions(+), 38 deletions(-) create mode 100644 frontend/redux/thunks/fetchDiscoveryLibraries.js create mode 100644 frontend/templates/Chat/DiscoveryLibrary/DiscoveryLibrary.jsx create mode 100644 frontend/templates/Chat/DiscoveryLibrary/index.js create mode 100644 frontend/templates/Chat/DiscoveryLibrary/styles.js diff --git a/frontend/layouts/MainAppLayout/SideMenu/styles.js b/frontend/layouts/MainAppLayout/SideMenu/styles.js index 8fe3cd99b..96654fc20 100644 --- a/frontend/layouts/MainAppLayout/SideMenu/styles.js +++ b/frontend/layouts/MainAppLayout/SideMenu/styles.js @@ -2,7 +2,8 @@ const styles = { mainGridProps: { container: true, item: true, - width: '360px', + // width: '360px', + width: '300px', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', diff --git a/frontend/redux/slices/chatSlice.js b/frontend/redux/slices/chatSlice.js index 17d48d4e3..d3afe5a1b 100644 --- a/frontend/redux/slices/chatSlice.js +++ b/frontend/redux/slices/chatSlice.js @@ -1,8 +1,9 @@ import { createSlice } from '@reduxjs/toolkit'; -import { MESSAGE_ROLE, MESSAGE_TYPES } from '@/constants/bots'; +import { DEFAULT_PROMPTS, MESSAGE_ROLE, MESSAGE_TYPES } from '@/constants/bots'; import fetchChat from '../thunks/fetchChat'; +import { fetchDiscoveryLibraries } from '../thunks/fetchDiscoveryLibraries'; const initialState = { input: '', @@ -24,6 +25,9 @@ const initialState = { streaming: false, displayQuickActions: false, actionType: null, + defaultPrompts: DEFAULT_PROMPTS, + discoveryLibraries: [], + selectedDiscoveryLibraryId: null, }; const chatSlice = createSlice({ @@ -34,6 +38,7 @@ const chatSlice = createSlice({ resetChat: (state, _) => ({ ...initialState, sessions: state.sessions, + discoveryLibraries: state.discoveryLibraries, }), setInput: (state, action) => { state.input = action.payload; @@ -135,6 +140,23 @@ const chatSlice = createSlice({ setActionType: (state, action) => { state.actionType = action.payload; }, + setDefaultPrompt: (state, action) => { + if (action.payload) { + state.defaultPrompts = action.payload; + } else { + state.defaultPrompts = DEFAULT_PROMPTS; + } + }, + setSelectedDiscoveryLibraryId: (state, action) => { + state.selectedDiscoveryLibraryId = action.payload; + const selectedLibrary = state.discoveryLibraries.find( + (library) => library.id === action.payload + ); + + if (selectedLibrary) { + state.defaultPrompts = selectedLibrary.defaultPrompts; + } + }, }, extraReducers: (builder) => { builder @@ -145,6 +167,15 @@ const chatSlice = createSlice({ .addCase(fetchChat.fulfilled, (state, action) => { state.chat = action.payload; state.sessionLoaded = true; + state.selectedDiscoveryLibraryId = null; + + if (state.discoveryLibraries) { + const discoveryLibraryId = action.payload?.discoveryLibraryId; + if (discoveryLibraryId !== undefined && discoveryLibraryId !== null) { + state.selectedDiscoveryLibraryId = discoveryLibraryId; + } + } + state.error = null; }) .addCase(fetchChat.rejected, (state, action) => { @@ -152,6 +183,14 @@ const chatSlice = createSlice({ state.sessionLoaded = true; console.error(action.error); state.error = 'Could not fetch chat. Please try again.'; + }) + .addCase(fetchDiscoveryLibraries.fulfilled, (state, action) => { + state.discoveryLibraries = action.payload; + }) + .addCase(fetchDiscoveryLibraries.rejected, (state, action) => { + state.discoveryLibraries = null; + console.error(action.error); + state.error = 'Could not fetch discovery libraries. Please try again.'; }); }, }); @@ -181,6 +220,7 @@ export const { setHistoryLoaded, setDisplayQuickActions, setActionType, + setSelectedDiscoveryLibraryId, } = chatSlice.actions; export default chatSlice.reducer; diff --git a/frontend/redux/thunks/fetchDiscoveryLibraries.js b/frontend/redux/thunks/fetchDiscoveryLibraries.js new file mode 100644 index 000000000..8a2495f18 --- /dev/null +++ b/frontend/redux/thunks/fetchDiscoveryLibraries.js @@ -0,0 +1,26 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { collection, getDocs, getFirestore } from 'firebase/firestore'; + +/** + * Async thunk to fetch Discovery Libraries from Firestore. + * + * @returns {Promise} An array of Discovery Library objects. + */ +export const fetchDiscoveryLibraries = createAsyncThunk( + 'chat/fetchDiscoveryLibraries', // Unique string used to identify this thunk + async () => { + const firestore = getFirestore(); // Get Firestore instance + + const discoveryLibrariesCollection = collection( + firestore, + 'discoveryLibraries' + ); // Get reference to Discovery Libraries collection + + const discoveryLibrariesSnapshot = await getDocs( + discoveryLibrariesCollection + ); // Get snapshot of Discovery Libraries documents + + // Map snapshot documents to Discovery Library objects and return + return discoveryLibrariesSnapshot.docs.map((doc) => doc.data()); + } +); diff --git a/frontend/redux/thunks/fetchHistory.js b/frontend/redux/thunks/fetchHistory.js index e6ded24e2..5672cbdd5 100644 --- a/frontend/redux/thunks/fetchHistory.js +++ b/frontend/redux/thunks/fetchHistory.js @@ -44,8 +44,15 @@ const fetchHistory = createAsyncThunk( // Convert Firestore timestamps to JavaScript Date objects and format as ISO strings createdAt: createdAt.toDate().toISOString(), updatedAt: updatedAt.toDate().toISOString(), - // Extract the title from the first message in the chat session - title: messages[0]?.payload?.text, + + // Extract the title of the chat session. If the first message role is 'system', the title is the text of the second message. Otherwise, the title is the text of the first message. + title: + // Check if the first message role is 'system' + messages[0]?.role === 'system' + ? // If so, extract the text of the second message + messages[1]?.payload?.text + : // Otherwise, extract the text of the first message + messages[0]?.payload?.text, }; // Add the chat history object to the history array diff --git a/frontend/templates/Chat/Chat.jsx b/frontend/templates/Chat/Chat.jsx index aefc3c593..3531868a4 100644 --- a/frontend/templates/Chat/Chat.jsx +++ b/frontend/templates/Chat/Chat.jsx @@ -54,6 +54,7 @@ import { updateHistoryEntry, } from '@/redux/slices/historySlice'; import { firestore } from '@/redux/store'; +import { fetchDiscoveryLibraries } from '@/redux/thunks/fetchDiscoveryLibraries'; import createChatSession from '@/services/chatbot/createChatSession'; import sendMessage from '@/services/chatbot/sendMessage'; @@ -75,6 +76,8 @@ const ChatInterface = () => { error, displayQuickActions, actionType, + selectedDiscoveryLibraryId, + discoveryLibraries, } = useSelector((state) => state.chat); const { data: userData } = useSelector((state) => state.user); @@ -95,6 +98,31 @@ const ChatInterface = () => { dispatch(setTyping(true)); + /** + * If a selected discovery library ID is specified, create a system message + * containing the system message of the selected library. + * + * @returns {Object|null} The system message object or null if no library is selected. + */ + let systemMessage = null; + if (selectedDiscoveryLibraryId != null) { + // Find the selected library in the list of discovery libraries + const selectedLibrary = discoveryLibraries.find( + (library) => library.id === selectedDiscoveryLibraryId + ); + + // If a library is selected, create a system message object with its system message + if (selectedLibrary) { + systemMessage = { + role: MESSAGE_ROLE.SYSTEM, // The role of the message (system or human) + type: MESSAGE_TYPES.TEXT, // The type of the message (text-based) + payload: { + text: selectedLibrary.systemMessage, // The text of the system message + }, + }; + } + } + // Define the chat payload const chatPayload = { user: { @@ -104,6 +132,8 @@ const ChatInterface = () => { }, type: 'chat', message, + systemMessage, + discoveryLibraryId: selectedDiscoveryLibraryId ?? null, }; // Send a chat session @@ -140,6 +170,8 @@ const ChatInterface = () => { }; useEffect(() => { + // Fetching all the discovery libraries. + dispatch(fetchDiscoveryLibraries()); return () => { localStorage.removeItem('sessionId'); dispatch(resetChat()); diff --git a/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx index 2fef41a77..1135ec282 100644 --- a/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx +++ b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx @@ -64,17 +64,15 @@ const ChatHistoryWindow = () => { {/* Header of the sidebar */} - - - newChat()} - {...styles.newChatIcon} - > - - - - + + newChat()} + {...styles.newChatIcon} + > + + + {/* Title of the chat history sidebar */} {/* Display the title of the chat history sidebar */} diff --git a/frontend/templates/Chat/ChatHistoryWindow/styles.js b/frontend/templates/Chat/ChatHistoryWindow/styles.js index 2546d45d0..69b313d01 100644 --- a/frontend/templates/Chat/ChatHistoryWindow/styles.js +++ b/frontend/templates/Chat/ChatHistoryWindow/styles.js @@ -25,10 +25,10 @@ const styles = { borderRadius: '15px', // Set the background to black. backgroundColor: '#000000', - // Set the min-width to 20%. - minWidth: '20%', - // Set the max width to 20%. - maxWidth: '20%', + // Set the min-width to 250px. + minWidth: '250px', + // Set the width to 25%. + width: '25%', // Set the height to 100%. height: '100%', // Set the color to white. diff --git a/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx index 565fb243c..c640f55b7 100644 --- a/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx +++ b/frontend/templates/Chat/DefaultPrompt/DefaultPrompt.jsx @@ -5,8 +5,6 @@ import { useDispatch, useSelector } from 'react-redux'; import MenuLogo from '@/assets/svg/PurpleStar.svg'; // Import MenuLogo -import { DEFAULT_PROMPTS } from '@/constants/bots'; - import styles from './styles'; import { setInput } from '@/redux/slices/chatSlice'; @@ -24,6 +22,7 @@ const DefaultPrompt = ({ handleSendMessage }) => { input: userInput, typing, sessionLoaded, + defaultPrompts, } = useSelector((state) => state.chat); const [readyToSend, setReadyToSend] = useState(false); @@ -47,9 +46,10 @@ const DefaultPrompt = ({ handleSendMessage }) => { } }, [readyToSend]); - const defaultPrompts = DEFAULT_PROMPTS; - - return !sessionLoaded && !typing && userInput.length === 0 ? ( + return !sessionLoaded && + localStorage.getItem('sessionId') == null && + !typing && + userInput.length === 0 ? ( {defaultPrompts.map((prompt, key) => ( { + // Get the selected discovery library ID from the Redux store + const selectedDiscoveryLibraryId = useSelector( + (state) => state.chat.selectedDiscoveryLibraryId + ); + + // Get the dispatch function from the Redux store + const dispatch = useDispatch(); + + // Destructure the library object + const { id, title, description, imageUrl } = library; + + // Determine if the current library is the selected library + const isSelectedLibrary = id === selectedDiscoveryLibraryId; + + /** + * ClickLibrary function is called when the library is clicked. + * It handles the logic for selecting a library. + */ + const clickLibrary = () => { + if (localStorage.getItem('sessionId') == null) { + // If there is no session ID, dispatch the setSelectedDiscoveryLibraryId action + + // Dispatch the setChatSession action to set the chat session + dispatch(setSelectedDiscoveryLibraryId(id)); + } else { + // If there is a session ID, reset the chat state and remove the session ID from local storage + + // Dispatch the resetChat action to reset the chat state + dispatch(resetChat()); + + // Remove the session ID from local storage + localStorage.removeItem('sessionId'); + + // dispatch(setSelectedDiscoveryLibrary(library)); + dispatch(setSelectedDiscoveryLibraryId(id)); + } + }; + + return ( + // Render the DiscoveryLibrary component + + + {title} + + {description} + + + + ); +}; + +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..06d09c033 --- /dev/null +++ b/frontend/templates/Chat/DiscoveryLibrary/styles.js @@ -0,0 +1,34 @@ +const styles = { + discoveryLibrary: (isSelected, imageUrl) => ({ + container: true, + sx: { + margin: '20px 0px 0px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', + backgroundColor: 'rgba(24,26,32,255)', + border: isSelected + ? '3px solid rgba(115, 80, 255, 255)' + : '3px solid white', + height: '20%', + width: '90%', + overflowY: 'auto', + borderRadius: '15px', + transition: 'all 0.3s ease', + backgroundImage: `url(${imageUrl})`, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + }, + }), + discoveryLibraryTitle: { + fontSize: '20px', + fontWeight: '800', + }, + discoveryLibraryDescription: { + fontSize: '14px', + fontWeight: '400', + }, +}; + +export default styles; diff --git a/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx b/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx index e119e80eb..8cd7631a8 100644 --- a/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx @@ -1,27 +1,93 @@ import { useState } from 'react'; import RemoveIcon from '@mui/icons-material/Remove'; -import { Fab, Grid, Typography } from '@mui/material'; +import { Fab, Grid, Skeleton, Typography } from '@mui/material'; + +import { useSelector } from 'react-redux'; + +import DiscoveryLibrary from '../DiscoveryLibrary'; import styles from './styles'; const DiscoveryLibraryWindow = () => { + const discoveryLibraries = useSelector( + (state) => state.chat.discoveryLibraries + ); + const [showDiscoveryLibrary, setShowDiscoveryLibraryWindow] = useState(false); + /** + * Returns a greeting message based on the current time of day. + * + * @return {string} The greeting message. + */ const greeting = () => { + // Get the current hour of the day. const hours = new Date().getHours(); + // Determine the greeting based on the current hour. if (hours < 12) { + // If the hour is before 12, return 'Good Morning'. return 'Good Morning'; } if (hours < 18) { + // If the hour is between 12 and 18 (exclusive), return 'Good Afternoon'. return 'Good Afternoon'; } + // If the hour is 18 or later, return 'Good Evening'. return 'Good Evening'; }; - const toggleDiscoveryLibrarySidebar = () => + /** + * Toggles the visibility of the discovery library sidebar. + * + * @return {void} No return value. + */ + const toggleDiscoveryLibrarySidebar = () => { + // Toggle the visibility of the discovery library sidebar by updating the state using the previous state. setShowDiscoveryLibraryWindow((prev) => !prev); + }; + + /** + * Returns a JSX element representing a skeleton component for the discovery library window. The skeleton component consists of a Grid container with five Skeleton components, each representing a discovery library item. The grid container has a column direction and a height of 100%. + * + * @return {JSX.Element} The skeleton component for the discovery library window. + */ + const librarySkeleton = () => ( + // Grid container with 100% height and column direction + + {/* Map over an array of length 5 */} + {Array.from({ length: 5 }).map((_, index) => ( + // Skeleton component with specified props + + ))} + + ); + + /** + * Returns a JSX element representing an error message component. + * + * @return {JSX.Element} The error message component. + */ + const renderErrorMessage = () => ( + + No Discovery Libraries Found + + ); + + const getLibraryContent = () => { + if (discoveryLibraries === null) { + return renderErrorMessage(); + } + + if (discoveryLibraries.length === 0) { + return librarySkeleton(); + } + + return discoveryLibraries.map((library) => ( + + )); + }; return !showDiscoveryLibrary ? ( @@ -30,7 +96,7 @@ const DiscoveryLibraryWindow = () => { onClick={toggleDiscoveryLibrarySidebar} {...styles.toggleDiscoveryLibraryWindowButton(showDiscoveryLibrary)} > - + { - {/* */} + {getLibraryContent()} ); }; diff --git a/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js b/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js index 4f8f2d906..ab425e78e 100644 --- a/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js @@ -17,8 +17,8 @@ const styles = { border: '5px solid rgba(115,80,255,255)', borderRadius: '15px', backgroundColor: '#000000', - maxWidth: '20%', - minWidth: '20%', + width: '25%', + minWidth: '250px', height: '100%', color: '#ffffff', }, @@ -56,10 +56,24 @@ const styles = { }, }, }), + centerChatMessage: { + container: true, + sx: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + textAlign: 'center', + }, + }, discoveryLibraries: { item: true, sx: { - display: 'block', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', backgroundColor: 'rgba(24,26,32,255)', height: '100%', width: '100%', diff --git a/functions/controllers/kaiAIController.js b/functions/controllers/kaiAIController.js index ab66e65ec..b5d10a29b 100644 --- a/functions/controllers/kaiAIController.js +++ b/functions/controllers/kaiAIController.js @@ -287,13 +287,25 @@ const createChatSession = onCall(async (props) => { try { DEBUG && logger.log('Communicator started, data:', props.data); - const { user, message, type } = props.data; + const { user, message, type, discoveryLibraryId, systemMessage } = + props.data; - if (!user || !message || !type) { + if (!user || !message || !type || !discoveryLibraryId) { logger.log('Missing required fields', props.data); throw new HttpsError('invalid-argument', 'Missing required fields'); } + /** + * If a system message is provided, sets the timestamp of the system message to the current time. + * This is done to ensure that the timestamp of the system message is in the same format as the timestamp of user messages. + * + * @param {Object} systemMessage - The system message object, or null if no system message is provided. + */ + if (systemMessage != null) { + // Set the timestamp of the system message to the current time + systemMessage.timestamp = Timestamp.fromMillis(Date.now()); + } + const initialMessage = { ...message, timestamp: Timestamp.fromMillis(Date.now()), @@ -304,9 +316,13 @@ const createChatSession = onCall(async (props) => { .firestore() .collection('chatSessions') .add({ - messages: [initialMessage], + messages: + systemMessage == null + ? [initialMessage] + : [systemMessage, initialMessage], user, type, + discoveryLibraryId, createdAt: Timestamp.fromMillis(Date.now()), updatedAt: Timestamp.fromMillis(Date.now()), }); @@ -314,9 +330,13 @@ const createChatSession = onCall(async (props) => { // Send trigger message to ReX AI const response = await kaiCommunicator({ data: { - messages: [initialMessage], + messages: + systemMessage == null + ? [initialMessage] + : [systemMessage, initialMessage], user, type, + discoveryLibraryId, }, }); From c3265219c3ba076c6b081e3228b06251b99515c0 Mon Sep 17 00:00:00 2001 From: Viraj Patel Date: Tue, 13 Aug 2024 12:16:11 -0400 Subject: [PATCH 6/7] Updated Discovery Libraries and add seed file. --- frontend/redux/slices/chatSlice.js | 3 + frontend/templates/Chat/Chat.jsx | 11 ++- .../ChatHistoryWindow/ChatHistoryWindow.jsx | 41 ++++++++- .../DiscoveryLibrary/DiscoveryLibrary.jsx | 18 ++-- .../templates/Chat/DiscoveryLibrary/styles.js | 36 ++++++-- .../DiscoveryLibraryWindow.jsx | 20 ++-- .../Chat/DiscoveryLibraryWindow/styles.js | 28 ++++-- functions/cloud_db_seed.js | 26 ++++++ functions/controllers/kaiAIController.js | 2 +- functions/discoveryLibraries_seed.json | 92 +++++++++++++++++++ 10 files changed, 241 insertions(+), 36 deletions(-) create mode 100644 functions/discoveryLibraries_seed.json diff --git a/frontend/redux/slices/chatSlice.js b/frontend/redux/slices/chatSlice.js index d3afe5a1b..52e69d8fd 100644 --- a/frontend/redux/slices/chatSlice.js +++ b/frontend/redux/slices/chatSlice.js @@ -187,6 +187,9 @@ const chatSlice = createSlice({ .addCase(fetchDiscoveryLibraries.fulfilled, (state, action) => { state.discoveryLibraries = action.payload; }) + .addCase(fetchDiscoveryLibraries.pending, (state) => { + state.discoveryLibraries = null; + }) .addCase(fetchDiscoveryLibraries.rejected, (state, action) => { state.discoveryLibraries = null; console.error(action.error); diff --git a/frontend/templates/Chat/Chat.jsx b/frontend/templates/Chat/Chat.jsx index 3531868a4..9add494ea 100644 --- a/frontend/templates/Chat/Chat.jsx +++ b/frontend/templates/Chat/Chat.jsx @@ -158,7 +158,16 @@ const ChatInterface = () => { // The ID of the session. id: data?.id, // The first message of the session. - title: data?.messages[0]?.payload?.text, + // title: data?.messages[0]?.payload?.text, + + // Extract the title of the chat session. If the first message role is 'system', the title is the text of the second message. Otherwise, the title is the text of the first message. + title: + // Check if the first message role is 'system' + data?.messages[0]?.role === 'system' + ? // If so, extract the text of the second message + data?.messages[1]?.payload?.text + : // Otherwise, extract the text of the first message + data?.messages[0]?.payload?.text, // The timestamp of session creation. createdAt: data?.createdAt, // The timestamp of session last update. diff --git a/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx index 1135ec282..526bc0890 100644 --- a/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx +++ b/frontend/templates/Chat/ChatHistoryWindow/ChatHistoryWindow.jsx @@ -1,6 +1,5 @@ import { useState } from 'react'; -import ChatIcon from '@mui/icons-material/Chat'; import HistoryIcon from '@mui/icons-material/History'; import RemoveIcon from '@mui/icons-material/Remove'; import { Fab, Grid, Tooltip, Typography } from '@mui/material'; @@ -51,6 +50,7 @@ const ChatHistoryWindow = () => { {/* // Render the open chat history button */} { newChat()} {...styles.newChatIcon} > - + {/* */} + + + + + + {/* Title of the chat history sidebar */} @@ -84,6 +120,7 @@ const ChatHistoryWindow = () => { {/* Close chat history button */} { const dispatch = useDispatch(); // Destructure the library object - const { id, title, description, imageUrl } = library; + const { id, title, imageUrl } = library; // Determine if the current library is the selected library const isSelectedLibrary = id === selectedDiscoveryLibraryId; @@ -56,16 +56,18 @@ const DiscoveryLibrary = ({ library }) => { return ( // Render the DiscoveryLibrary component + // {...styles.discoveryLibrary(isSelectedLibrary, imageUrl)} - + + + + + {/* {title} - - {description} - - + */} ); }; diff --git a/frontend/templates/Chat/DiscoveryLibrary/styles.js b/frontend/templates/Chat/DiscoveryLibrary/styles.js index 06d09c033..348cf6776 100644 --- a/frontend/templates/Chat/DiscoveryLibrary/styles.js +++ b/frontend/templates/Chat/DiscoveryLibrary/styles.js @@ -1,5 +1,5 @@ const styles = { - discoveryLibrary: (isSelected, imageUrl) => ({ + /* discoveryLibrary: (isSelected, imageUrl) => ({ container: true, sx: { margin: '20px 0px 0px', @@ -7,10 +7,11 @@ const styles = { flexDirection: 'column', justifyContent: 'flex-end', backgroundColor: 'rgba(24,26,32,255)', + color: isSelected ? 'rgba(115, 80, 255, 255)' : 'white', border: isSelected ? '3px solid rgba(115, 80, 255, 255)' : '3px solid white', - height: '20%', + // height: '20%', width: '90%', overflowY: 'auto', borderRadius: '15px', @@ -20,14 +21,33 @@ const styles = { backgroundPosition: 'center', backgroundRepeat: 'no-repeat', }, + }), */ + discoveryLibrary: (isSelected) => ({ + container: true, + sx: { + padding: '10px', + margin: '10px 10px 10px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', + backgroundColor: 'rgba(24,26,32,255)', + color: isSelected ? 'rgba(115, 80, 255, 255)' : 'white', + border: isSelected + ? '3px solid rgba(115, 80, 255, 255)' + : '3px solid white', + // height: '20%', + width: '90%', + // overflowY: 'auto', + borderRadius: '15px', + transition: 'all 0.3s ease', + cursor: 'pointer', + }, }), discoveryLibraryTitle: { - fontSize: '20px', - fontWeight: '800', - }, - discoveryLibraryDescription: { - fontSize: '14px', - fontWeight: '400', + sx: { + padding: '10px', + fontSize: '20px', + }, }, }; diff --git a/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx b/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx index 8cd7631a8..b48acf3f9 100644 --- a/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx @@ -76,7 +76,7 @@ const DiscoveryLibraryWindow = () => { ); const getLibraryContent = () => { - if (discoveryLibraries === null) { + if (discoveryLibraries == null) { return renderErrorMessage(); } @@ -94,20 +94,22 @@ const DiscoveryLibraryWindow = () => { @@ -126,7 +128,7 @@ const DiscoveryLibraryWindow = () => { aria-label="close discovery library" size="medium" onClick={toggleDiscoveryLibrarySidebar} - {...styles.toggleDiscoveryLibraryWindowButton(showDiscoveryLibrary)} + {...styles.closeDiscoveryLibraryButton} > diff --git a/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js b/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js index ab425e78e..ff9de81e5 100644 --- a/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js +++ b/frontend/templates/Chat/DiscoveryLibraryWindow/styles.js @@ -41,21 +41,35 @@ const styles = { discoveryLibraryWindowTitleText: { textAlign: 'center', }, - toggleDiscoveryLibraryWindowButton: (showDiscoveryLibraryWindow) => ({ + closeDiscoveryLibraryButton: { sx: { - padding: showDiscoveryLibraryWindow ? 'none' : '5px', + padding: 'none', backgroundColor: '#000000', color: 'rgba(115,80,255,255)', - border: showDiscoveryLibraryWindow - ? 'none' - : '5px solid rgba(115,80,255,255)', - borderRadius: showDiscoveryLibraryWindow ? 'none' : '15px', + border: 'none', + borderRadius: 'none', '&:hover': { backgroundColor: '#000000', color: 'rgba(115,80,255,255)', }, }, - }), + }, + openDiscoveryLibraryButton: { + sx: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px', + backgroundColor: '#000000', + color: 'rgba(115,80,255,255)', + border: '5px solid rgba(115,80,255,255)', + borderRadius: '15px', + '&:hover': { + backgroundColor: '#000000', + color: 'rgba(115,80,255,255)', + }, + }, + }, centerChatMessage: { container: true, sx: { diff --git a/functions/cloud_db_seed.js b/functions/cloud_db_seed.js index 13fb84e07..8eb881677 100644 --- a/functions/cloud_db_seed.js +++ b/functions/cloud_db_seed.js @@ -3,6 +3,30 @@ const { Timestamp } = require('firebase-admin/firestore'); const db = admin.firestore(); +const seedDiscoveryLibraries = async () => { + const discoveryLibrariesData = require('./discoveryLibraries_seed.json'); + + try { + const discoveryLibraries = await db.collection('discoveryLibraries').get(); + + if (!discoveryLibraries.empty) { + console.log('Discovery Libraries is ready to go!'); + return; + } + + Object.values(discoveryLibrariesData).forEach(async (doc) => { + await db.collection('discoveryLibraries').doc(doc.id.toString()).set(doc); + console.log( + `Document with ID ${doc.id} added to the Discovery Libraries collection` + ); + }); + + console.log('Discovery Libraries data seeded successfully.'); + } catch (error) { + console.error('Error seeding Discovery Libraries collection:', error); + } +}; + const seedDatabase = async () => { const data = require('./seed_data.json'); @@ -25,6 +49,8 @@ const seedDatabase = async () => { console.log( 'Kai AI installed successfully to firebase and is ready to go!' ); + + await seedDiscoveryLibraries(); } catch (error) { console.error('Error seeding database:', error); } diff --git a/functions/controllers/kaiAIController.js b/functions/controllers/kaiAIController.js index b5d10a29b..b4adf421f 100644 --- a/functions/controllers/kaiAIController.js +++ b/functions/controllers/kaiAIController.js @@ -290,7 +290,7 @@ const createChatSession = onCall(async (props) => { const { user, message, type, discoveryLibraryId, systemMessage } = props.data; - if (!user || !message || !type || !discoveryLibraryId) { + if (!user || !message || !type) { logger.log('Missing required fields', props.data); throw new HttpsError('invalid-argument', 'Missing required fields'); } diff --git a/functions/discoveryLibraries_seed.json b/functions/discoveryLibraries_seed.json new file mode 100644 index 000000000..a5df7b328 --- /dev/null +++ b/functions/discoveryLibraries_seed.json @@ -0,0 +1,92 @@ +{ + "0": { + "id": "0", + "title": "Math Tutor", + "defaultPrompts": [ + "Add 2 + 2.", + "Differentiation of (sinx)^2." + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "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." + }, + "1": { + "id": "1", + "title": "English Grammar Tutor", + "defaultPrompts": [ + "Explain the use of commas in a sentence.", + "Correct the following sentence: 'She dont know the answer.'" + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "Please act as an English grammar tutor. I will be asking you to help me with grammar rules, sentence structure, punctuation, and writing tips. Provide clear explanations, examples, and exercises to help me improve my grammar and writing skills." + }, + "2": { + "id": "2", + "title": "Physics Tutor", + "defaultPrompts": [ + "Explain Newton's laws of motion.", + "Calculate the force exerted by a 5 kg object accelerating at 2 m/s^2." + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "Assume the role of a physics tutor. I will ask you questions about different physics topics, including mechanics, electromagnetism, thermodynamics, and quantum physics. Provide thorough explanations, solve problems, and offer real-world examples to illustrate the concepts." + }, + "3": { + "id": "3", + "title": "Chemistry Tutor", + "defaultPrompts": [ + "Balance the chemical equation: H2 + O2 → H2O.", + "Explain the concept of molarity." + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "I want you to be my chemistry tutor. I will inquire about various chemistry topics such as atomic structure, chemical reactions, stoichiometry, and organic chemistry. Please give detailed explanations, solve chemical equations, and provide examples and experiments related to these topics." + }, + "4": { + "id": "4", + "title": "Biology Tutor", + "defaultPrompts": [ + "Describe the structure of a cell.", + "Explain the process of natural selection." + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "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." + }, + "5": { + "id": "5", + "title": "History Tutor", + "defaultPrompts": [ + "Explain the causes of World War I.", + "Describe the significance of the American Revolution." + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "Become a history tutor for our conversation. I will be asking you about different historical periods, events, and figures. Provide detailed explanations, timelines, and analysis of historical significance for each topic we discuss." + }, + "6": { + "id": "6", + "title": "Programming Tutor", + "defaultPrompts": [ + "Write a Python function to calculate the factorial of a number.", + "Explain the difference between a stack and a queue." + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "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." + }, + "7": { + "id": "7", + "title": "Art Tutor", + "defaultPrompts": [ + "Describe the techniques used in Impressionism.", + "How do you create a color palette for a painting?" + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "Assume the role of an art tutor. I will ask you about different art techniques, art history, famous artists, and art critique. Provide detailed explanations, examples of artworks, and exercises to help me improve my artistic skills and knowledge." + }, + "8": { + "id": "8", + "title": "Music Tutor", + "defaultPrompts": [ + "Explain the basics of music theory.", + "How do you read sheet music?" + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "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." + }, + "9": { + "id": "9", + "title": "Spanish Language Tutor", + "defaultPrompts": [ + "Conjugate the verb 'hablar' in the present tense.", + "Translate the sentence 'I am learning Spanish' into Spanish." + ], + "imageUrl": "https://pi.ai/_next/image?url=https%3A%2F%2Fpi.ai%2Fpublic%2Fmedia%2Fdiscover%2Fimages%2Fventing.jpg&w=256&q=100","systemMessage": "Please act as my Spanish language tutor. I will be asking you to help me with Spanish vocabulary, grammar, pronunciation, and conversation practice. Provide detailed explanations, examples, and exercises to help me become fluent in Spanish." + } +} From d8e35d969774087f13f4c07ed125c8dc5d9a3850 Mon Sep 17 00:00:00 2001 From: Viraj Patel Date: Fri, 30 Aug 2024 16:31:18 -0400 Subject: [PATCH 7/7] Complete Epic 5 and Revamp Chat UI. --- frontend/assets/svg/ChatHistoryButtonIcon.svg | 3 + frontend/assets/svg/ChatIcon.svg | 6 + frontend/assets/svg/ChatIconFill.svg | 6 + frontend/assets/svg/DefaultPromptStar.svg | 9 + frontend/assets/svg/DiscoveryIcon.svg | 3 + frontend/assets/svg/HomeMenuIcon.svg | 3 + frontend/assets/svg/LogoutIcon.svg | 4 + frontend/assets/urls.js | 4 +- frontend/components/ToolCard/ToolCard.jsx | 6 +- frontend/constants/bots.js | 2 +- frontend/constants/routes.js | 1 + frontend/layouts/AuthLayout/AuthLayout.jsx | 10 +- frontend/layouts/AuthLayout/styles.js | 4 +- .../layouts/MainAppLayout/ MainAppLayout.jsx | 6 +- .../layouts/MainAppLayout/NavBar/Navbar.jsx | 171 +++++++++++++++ .../layouts/MainAppLayout/NavBar/index.js | 1 + .../layouts/MainAppLayout/NavBar/styles.js | 133 ++++++++++++ .../layouts/MainAppLayout/NavMenu/NavMenu.jsx | 4 +- frontend/layouts/MainAppLayout/styles.js | 14 +- frontend/pages/chat/index.jsx | 8 +- frontend/redux/slices/chatSlice.js | 45 +--- frontend/redux/slices/historySlice.js | 12 +- frontend/redux/thunks/fetchChat.js | 34 ++- .../redux/thunks/fetchDiscoveryLibraries.js | 26 --- frontend/redux/thunks/fetchHistory.js | 78 +++---- frontend/regex/routes.js | 3 +- frontend/styles/pageNotFoundStyles.js | 4 +- .../CenterChatContentNoMessages.jsx | 53 ----- .../Chat/CenterChatContentNoMessages/index.js | 1 - .../CenterChatContentNoMessages/styles.js | 84 -------- frontend/templates/Chat/Chat.jsx | 168 +++++---------- .../Chat/ChatHistory/ChatHistory.jsx | 64 +++--- frontend/templates/Chat/ChatHistory/styles.js | 138 +++++++++--- .../ChatHistoryWindow/ChatHistoryWindow.jsx | 124 +++-------- .../Chat/ChatHistoryWindow/styles.js | 200 +++++------------- .../Chat/DefaultPrompt/DefaultPrompt.jsx | 17 +- .../templates/Chat/DefaultPrompt/styles.js | 85 ++++---- .../DiscoveryLibrary/DiscoveryLibrary.jsx | 75 ------- .../templates/Chat/DiscoveryLibrary/index.js | 1 - .../templates/Chat/DiscoveryLibrary/styles.js | 54 ----- .../DiscoveryLibraryWindow.jsx | 141 ------------ .../Chat/DiscoveryLibraryWindow/index.js | 1 - .../Chat/DiscoveryLibraryWindow/styles.js | 101 --------- .../Chat/QuickActions/QuickActions.jsx | 38 ++-- .../templates/Chat/QuickActions/styles.js | 67 ++++-- .../Chat/TextMessage/TextMessage.jsx | 18 +- frontend/templates/Chat/TextMessage/styles.js | 20 +- frontend/templates/Chat/styles.js | 42 ++-- frontend/templates/HomePage/HomePage.jsx | 3 +- frontend/templates/HomePage/styles.js | 3 + frontend/theme/theme.jsx | 2 + functions/cloud_db_seed.js | 30 +-- ...iAIController.js => marvelAIController.js} | 53 +++-- functions/discoveryLibraries_seed.json | 92 -------- functions/index.js | 10 +- 55 files changed, 901 insertions(+), 1384 deletions(-) create mode 100644 frontend/assets/svg/ChatHistoryButtonIcon.svg create mode 100644 frontend/assets/svg/ChatIcon.svg create mode 100644 frontend/assets/svg/ChatIconFill.svg create mode 100644 frontend/assets/svg/DefaultPromptStar.svg create mode 100644 frontend/assets/svg/DiscoveryIcon.svg create mode 100644 frontend/assets/svg/HomeMenuIcon.svg create mode 100644 frontend/assets/svg/LogoutIcon.svg create mode 100644 frontend/layouts/MainAppLayout/NavBar/Navbar.jsx create mode 100644 frontend/layouts/MainAppLayout/NavBar/index.js create mode 100644 frontend/layouts/MainAppLayout/NavBar/styles.js delete mode 100644 frontend/redux/thunks/fetchDiscoveryLibraries.js delete mode 100644 frontend/templates/Chat/CenterChatContentNoMessages/CenterChatContentNoMessages.jsx delete mode 100644 frontend/templates/Chat/CenterChatContentNoMessages/index.js delete mode 100644 frontend/templates/Chat/CenterChatContentNoMessages/styles.js delete mode 100644 frontend/templates/Chat/DiscoveryLibrary/DiscoveryLibrary.jsx delete mode 100644 frontend/templates/Chat/DiscoveryLibrary/index.js delete mode 100644 frontend/templates/Chat/DiscoveryLibrary/styles.js delete mode 100644 frontend/templates/Chat/DiscoveryLibraryWindow/DiscoveryLibraryWindow.jsx delete mode 100644 frontend/templates/Chat/DiscoveryLibraryWindow/index.js delete mode 100644 frontend/templates/Chat/DiscoveryLibraryWindow/styles.js rename functions/controllers/{kaiAIController.js => marvelAIController.js} (90%) delete mode 100644 functions/discoveryLibraries_seed.json diff --git a/frontend/assets/svg/ChatHistoryButtonIcon.svg b/frontend/assets/svg/ChatHistoryButtonIcon.svg new file mode 100644 index 000000000..2e128f233 --- /dev/null +++ b/frontend/assets/svg/ChatHistoryButtonIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/svg/ChatIcon.svg b/frontend/assets/svg/ChatIcon.svg new file mode 100644 index 000000000..48821fb3f --- /dev/null +++ b/frontend/assets/svg/ChatIcon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/assets/svg/ChatIconFill.svg b/frontend/assets/svg/ChatIconFill.svg new file mode 100644 index 000000000..9904f410b --- /dev/null +++ b/frontend/assets/svg/ChatIconFill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/assets/svg/DefaultPromptStar.svg b/frontend/assets/svg/DefaultPromptStar.svg new file mode 100644 index 000000000..dcf5b0b10 --- /dev/null +++ b/frontend/assets/svg/DefaultPromptStar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/assets/svg/DiscoveryIcon.svg b/frontend/assets/svg/DiscoveryIcon.svg new file mode 100644 index 000000000..4969669fb --- /dev/null +++ b/frontend/assets/svg/DiscoveryIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/svg/HomeMenuIcon.svg b/frontend/assets/svg/HomeMenuIcon.svg new file mode 100644 index 000000000..094fe0c4f --- /dev/null +++ b/frontend/assets/svg/HomeMenuIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/svg/LogoutIcon.svg b/frontend/assets/svg/LogoutIcon.svg new file mode 100644 index 000000000..488bd3c49 --- /dev/null +++ b/frontend/assets/svg/LogoutIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/urls.js b/frontend/assets/urls.js index 216a55ac7..ea7ad20c2 100644 --- a/frontend/assets/urls.js +++ b/frontend/assets/urls.js @@ -1,11 +1,11 @@ const ImageURLs = { AuthBgImage: 'https://firebasestorage.googleapis.com/v0/b/radicalx-68127.appspot.com/o/HackathonBanners%2Fthe%20twilight%20zone.png?alt=media&token=cb4a7071-76a4-49ff-82b7-8e6ed96c5532', - KaiAuthImg: + MarvelAuthImg: 'https://firebasestorage.googleapis.com/v0/b/kai-ai-f63c8.appspot.com/o/Kai_blob-1.png?alt=media&token=997d9722-8188-4aee-a600-4607ba87c060', PurpleBlobSvg: 'https://firebasestorage.googleapis.com/v0/b/kai-ai-f63c8.appspot.com/o/Purple_Blob_1.png?alt=media&token=e5bce404-78d0-4d9c-9e45-b63bddd936da', - RexCircleAvatar: + MarvelCircleAvatar: 'https://firebasestorage.googleapis.com/v0/b/kai-ai-f63c8.appspot.com/o/KaiProfilePic.png?alt=media&token=4cfcd644-8d86-4944-82a7-cf83b4519311', }; diff --git a/frontend/components/ToolCard/ToolCard.jsx b/frontend/components/ToolCard/ToolCard.jsx index 1c1b31729..ec62b2f3c 100644 --- a/frontend/components/ToolCard/ToolCard.jsx +++ b/frontend/components/ToolCard/ToolCard.jsx @@ -25,7 +25,11 @@ const ToolCard = (props) => { const renderImage = () => { return ( - kai logo + Marvel logo ); }; diff --git a/frontend/constants/bots.js b/frontend/constants/bots.js index 3279b07f8..15dcf2d70 100644 --- a/frontend/constants/bots.js +++ b/frontend/constants/bots.js @@ -27,9 +27,9 @@ const BOT_TYPE = { }; const DEFAULT_PROMPTS = [ + 'Strategies to encourage student participation.', 'Design an engaging class activity.', 'Recommend resources for effective teaching.', - 'Strategies to encourage student participation.', ]; const MESSAGE_ROLE = { diff --git a/frontend/constants/routes.js b/frontend/constants/routes.js index 5771d07fc..1838cf6d5 100644 --- a/frontend/constants/routes.js +++ b/frontend/constants/routes.js @@ -9,6 +9,7 @@ const ROUTES = { REDIRECT: '/redirect', CHAT: '/chat', HISTORY: '/history', + DISCOVERY: '/discovery', }; export default ROUTES; diff --git a/frontend/layouts/AuthLayout/AuthLayout.jsx b/frontend/layouts/AuthLayout/AuthLayout.jsx index cc2cb163a..8dc181035 100644 --- a/frontend/layouts/AuthLayout/AuthLayout.jsx +++ b/frontend/layouts/AuthLayout/AuthLayout.jsx @@ -55,11 +55,11 @@ const AuthLayout = (props) => { const renderArtifacts = () => { return ( <> - + rexImage @@ -76,7 +76,7 @@ const AuthLayout = (props) => { const renderHead = () => { return ( - Kai AI + Marvel AI ); }; diff --git a/frontend/layouts/AuthLayout/styles.js b/frontend/layouts/AuthLayout/styles.js index e104bb601..241f8636b 100644 --- a/frontend/layouts/AuthLayout/styles.js +++ b/frontend/layouts/AuthLayout/styles.js @@ -17,7 +17,7 @@ const styles = { priority: true, style: { zIndex: 0 }, }, - reXImageProps: { + marvelImageProps: { layout: 'fill', objectFit: 'cover', priority: true, @@ -97,7 +97,7 @@ const styles = { }, }), }, - reXProps: { + marvelProps: { sx: (theme) => ({ position: 'absolute', zIndex: 2, diff --git a/frontend/layouts/MainAppLayout/ MainAppLayout.jsx b/frontend/layouts/MainAppLayout/ MainAppLayout.jsx index a2690b2ba..5b5d995dc 100644 --- a/frontend/layouts/MainAppLayout/ MainAppLayout.jsx +++ b/frontend/layouts/MainAppLayout/ MainAppLayout.jsx @@ -8,7 +8,7 @@ import { useDispatch, useSelector } from 'react-redux'; import AppDisabled from '@/components/AppDisabled'; import Loader from '@/components/Loader'; -import SideMenu from './SideMenu'; +import NavBar from './NavBar'; import styles from './styles'; import { setLoading } from '@/redux/slices/authSlice'; @@ -44,7 +44,7 @@ const MainAppLayout = (props) => { const renderHead = () => { return ( - Kai AI + Marvel AI ); }; @@ -52,7 +52,7 @@ const MainAppLayout = (props) => { const renderApp = () => { return ( <> - + {children} diff --git a/frontend/layouts/MainAppLayout/NavBar/Navbar.jsx b/frontend/layouts/MainAppLayout/NavBar/Navbar.jsx new file mode 100644 index 000000000..21d47507d --- /dev/null +++ b/frontend/layouts/MainAppLayout/NavBar/Navbar.jsx @@ -0,0 +1,171 @@ +import { Avatar, Button, Grid, Typography } from '@mui/material'; +import { signOut } from 'firebase/auth'; +import { useRouter } from 'next/router'; + +import { useSelector } from 'react-redux'; + +import ChatIcon from '@/assets/svg/ChatIcon.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 styles from './styles'; + +import { auth } from '@/redux/store'; +import { chatRegex, discoveryRegex, homeRegex } from '@/regex/routes'; + +// TODO: Once Discovery Feature is ready, uncomment Discovery Page from below array. +const PAGES = [ + { + name: 'Home', + link: ROUTES.HOME, + icon: , + id: 'home', + }, + /* { + name: 'Discovery', + link: ROUTES.DISCOVERY, + icon: , + id: 'discovery', + }, */ + { + name: 'Chat', + link: ROUTES.CHAT, + icon: , + id: 'chat', + }, +]; + +/** + * NavBar component renders the main navigation bar for the application. + * It includes the application logo, a navigation menu with dynamic active states, + * and a user profile section with the ability to sign out. + * + * @component + * @returns {JSX.Element} The navigation bar component. + */ +const NavBar = () => { + const router = useRouter(); + + const user = useSelector((state) => state.user.data); + + /** + * Signs out the currently signed-in user from Firebase Authentication + * and redirects them to the sign-in page. + * + * @returns {void} + */ + const handleSignOutUser = () => { + signOut(auth); + }; + + /** + * Renders the logo of the application in the navbar. + * Clicking on the logo redirects the user to the home page. + * + * @returns {JSX.Element} The JSX element representing the logo section. + */ + const renderLogo = () => { + return ( + + router.push(ROUTES.HOME)} {...styles.logoImage}> + {/* TODO: Put Marvel AI logo here */} + Marvel AI + + AI Teaching Assistant + + ); + }; + + /** + * Renders the profile section of the navbar, which includes the user's profile + * photo, name, and a logout button. + * + * @returns {JSX.Element} The profile section of the navbar. + */ + const renderProfile = () => { + return ( + + + + {user?.fullName[0]} + + {user?.fullName} + +