diff --git a/src/components/chat/Chats.jsx b/src/components/chat/Chats.jsx index be28c0202ae974c3b14dcc6941e72836166d56f3..3a117d07153bcd7683d4e60aab3e8ccec138542a 100644 --- a/src/components/chat/Chats.jsx +++ b/src/components/chat/Chats.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import Chat from './Chat'; import Heading from '../font/Heading'; -import { RiArrowLeftCircleLine, RiArrowRightCircleLine } from 'react-icons/ri'; +import { RiAddCircleLine, RiArrowLeftCircleLine, RiArrowRightCircleLine } from 'react-icons/ri'; import { useChat } from '../../contexts/Chat/ChatState'; import { useParams } from 'react-router-dom'; @@ -17,7 +17,7 @@ const Chats = () => { const { id } = useParams(); // ### CONNECT CONTEXT - const { fetchAllChats, chatHeadings } = useChat(); + const { fetchAllChats, chatHeadings, currentChatId, selectChat } = useChat(); // ### FETCH CHATS; useEffect(() => { @@ -26,8 +26,8 @@ const Chats = () => { const getChats = async () => { try { - // fetch all chats (and directly show id if provided) - await fetchAllChats(id); + // fetch all chats + await fetchAllChats(id || null); } catch (error) { console.error(error); mergeBackendValidation(error.response.status, error.response.data); @@ -42,6 +42,7 @@ const Chats = () => { }; }, []); + // ################################# // FUNCTIONS // ################################# @@ -52,6 +53,9 @@ const Chats = () => { return ( <div className="row-start-2 row-span-2 border-r-2 border-UhhGrey flex flex-col"> <Heading level="6" className="text-center">Recent</Heading> + + {<button onClick={() => { selectChat(null); }} disabled={currentChatId ? false : true} className='text-UhhBlue disabled:text-UhhLightBlue' title='start a new chat'><RiAddCircleLine /></button>} + <div className="p-1"> {showSidebar && chatHeadings.map((chat, index) => ( @@ -70,7 +74,7 @@ const Chats = () => { </button> </div> - </div> + </div > ); }; diff --git a/src/components/chat/Messages.jsx b/src/components/chat/Messages.jsx index 8e4fdcc1be29c22bdb6b3fbe502b34af539a651c..c29101da06556750a374c1f2428f4fc1b28e9189 100644 --- a/src/components/chat/Messages.jsx +++ b/src/components/chat/Messages.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import Message from './Message'; import { useChat } from '../../contexts/Chat/ChatState'; import { Link } from 'react-router-dom'; @@ -12,11 +12,14 @@ const Messages = () => { // ### CONNECT CONTEXT const { currentChatId, fetchChatHistory, chatHistory } = useChat(); + // ### FETCH HISTORY EVERY TIME THE CHAT ID CHANGES useEffect(() => { // ### on run exec this code const controller = new AbortController(); // ### fetch chat history based on current chat id fetchChatHistory(currentChatId); + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + // ### return will be executed on unmounting this component return () => { // on unmount abort request @@ -24,6 +27,11 @@ const Messages = () => { }; }, [currentChatId]); + // ### SCROLL TO BOTTOM + const bottomRef = useRef(); + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [chatHistory]); // ################################# // FUNCTIONS @@ -34,7 +42,7 @@ const Messages = () => { // ################################# return ( <div className="row-start-2 overflow-auto"> - {<Link to={`/onboarding/${currentChatId}`} className='text-UhhBlue' target='_blank' rel='noopener noreferrer'><RxBookmark /></Link>} + {currentChatId && <Link to={`/onboarding/${currentChatId}`} className='text-UhhBlue' target='_blank' rel='noopener noreferrer'><RxBookmark /></Link>} {chatHistory.map((prompt, index) => ( <Message key={index} @@ -43,7 +51,10 @@ const Messages = () => { /> )) } + {!chatHistory.length && <div className="text-center text-gray-500">No messages yet</div>} + + <div ref={bottomRef}> </div> </div> ); }; diff --git a/src/components/chat/PromptInput.jsx b/src/components/chat/PromptInput.jsx index cb5a328e30df151b670e91cf82fe23ffe57a365c..accf0c0bd073cb5628ec90bf586fb336843efa5c 100644 --- a/src/components/chat/PromptInput.jsx +++ b/src/components/chat/PromptInput.jsx @@ -1,13 +1,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { RiSendPlane2Line } from 'react-icons/ri'; +import { RiLoopRightFill, RiSendPlane2Line } from 'react-icons/ri'; import { z } from 'zod'; import Input from '../form/Input'; import { mergeBackendValidation } from '../../utils/ErrorHandling'; import api from '../../utils/AxiosConfig'; import { useChat } from '../../contexts/Chat/ChatState'; +import Select from '../form/Select'; function PromptInput() { // ################################# @@ -22,7 +23,7 @@ function PromptInput() { // HOOKS // ################################# // ### CONNECT CONTEXT - const { currentChatId } = useChat(); + const { currentChatId, availableModels, updateChatHistory } = useChat(); // ### PREPARE FORM const methods = useForm({ @@ -43,12 +44,15 @@ function PromptInput() { if (currentChatId) { inputs.chatId = currentChatId; }; // send data to api try { + // send input to api const result = await api.post('/ai/chat', inputs); - // TODO: update chat context - console.log("🚀 ~ handleSendForm ~ result:", result); - + // update chat history + updateChatHistory(currentChatId, result.data.chat.chatHistory); + // clear input field + methods.resetField('input'); } catch (error) { + console.error(error); // merge front & backend validation errors mergeBackendValidation(error.response.status, error.response.data, methods.setError); } @@ -63,8 +67,10 @@ function PromptInput() { <div className="row-start-3 p-3 border-t-2 border-UhhGrey"> <FormProvider {...methods} > <form onSubmit={methods.handleSubmit(handleSendForm)}> - <div className="flex content-center h-14"> - <Input name="model" /> + <div className="flex content-center h-14 relative"> + {methods.formState.isSubmitting && <div className='absolute bg-white bg-opacity-60 z-10 h-full w-full flex items-center justify-center text-2xl'><RiLoopRightFill className='animate-spin' /></div>} + + <Select name="model" options={availableModels} /> <Input name="input" type="text" placeholder="Type a message" className="block w-full h-8" /> <button type="submit" className="h-8 justify-center items-center bg-UhhBlue text-UhhWhite p-1 text-xs"> diff --git a/src/components/form/Select.jsx b/src/components/form/Select.jsx index f2fc954bc01b6539c1c1f98a28082c3afa3982ed..cd16a7021fe0cc81cc46da0837a06e3fe798c806 100755 --- a/src/components/form/Select.jsx +++ b/src/components/form/Select.jsx @@ -44,7 +44,7 @@ function Select({ options, name, title, defaultValue, className, tooltip, type, return ( <label htmlFor={id}> {props.required && <RequiredBadge />} - {capitalizeFirstLetter(title)} + {title && capitalizeFirstLetter(title)} {tooltip && <Tooltip>{tooltip}</Tooltip>} <Controller diff --git a/src/contexts/Chat/ChatReducer.jsx b/src/contexts/Chat/ChatReducer.jsx index d81d48e3fa9c8774df90e17a0bf18efa443a72c2..773f2af7cca91a7252d8779dd80bf3ec869f1d4b 100644 --- a/src/contexts/Chat/ChatReducer.jsx +++ b/src/contexts/Chat/ChatReducer.jsx @@ -5,10 +5,29 @@ const chatReducer = (state, action) => { case CHAT_ACTIONS.SET_CHATS: case CHAT_ACTIONS.SET_HEADINGS: case CHAT_ACTIONS.UPDATE_CHATID: - case CHAT_ACTIONS.SET_HISTORY: - return action.payload; + { return action.payload; } + + + case CHAT_ACTIONS.SET_HISTORY: { + if (!action.payload.id) return []; + // define a function to find the chat + const isSelectedChatId = element => element.id === action.payload.id; + // get index of matching item + const index = action.payload.chats.findIndex(isSelectedChatId); + // return history of chat + return action.payload.chats[index].chatHistory; + } + + case CHAT_ACTIONS.SET_MODELS: { + const models = action.payload.data; + const modelNames = models.map(model => { + return { title: model.name, _id: model.name }; + }); + return modelNames; + } + default: - return state; + { return state; } } }; diff --git a/src/contexts/Chat/ChatState.jsx b/src/contexts/Chat/ChatState.jsx index bb3cf8ddbbfdc45c1f651132c15131ddc74561da..4e9fdaf86e83113c239ea2553b9e421fb06cafd4 100644 --- a/src/contexts/Chat/ChatState.jsx +++ b/src/contexts/Chat/ChatState.jsx @@ -19,6 +19,7 @@ function ChatState({ children }) { const [chatHeadings, dispatchChatHeadings] = useReducer(chatReducer, []); const [currentChatId, dispatchCurrentChatId] = useReducer(chatReducer, null); const [chatHistory, dispatchChatHistory] = useReducer(chatReducer, []); + const [availableModels, dispatchAvailableModels] = useReducer(chatReducer, []); @@ -35,6 +36,9 @@ function ChatState({ children }) { fetchChatHeadings(items.data.chats); // select chat if id is provided if (id) selectChat(id); + // fetch available models + const models = await api.post('/ai/models', { filter: '' }); + dispatchAvailableModels({ type: CHAT_ACTIONS.SET_MODELS, payload: models }); } catch (error) { // display errors mergeBackendValidation(error.response.status, error.response.data); @@ -61,16 +65,18 @@ function ChatState({ children }) { // ### FETCH CHAT HISTORY function fetchChatHistory(id) { - // return empty if no chat was chosen - if (!id) return []; + dispatchChatHistory({ type: CHAT_ACTIONS.SET_HISTORY, payload: { id, chats } }); + } + + // ### UPDATE CHAT HISTORY + function updateChatHistory(id, history) { // define a function to find the chat const isSelectedChatId = element => element.id === id; // get index of matching item const index = chats.findIndex(isSelectedChatId); - // return history of chat - const history = chats[index].chatHistory; - // save history in state - dispatchChatHistory({ type: CHAT_ACTIONS.SET_HISTORY, payload: history }); + chats[index].chatHistory = history; + + dispatchChatHistory({ type: CHAT_ACTIONS.SET_HISTORY, payload: { id, chats } }); } @@ -92,7 +98,9 @@ function ChatState({ children }) { currentChatId, selectChat, fetchChatHistory, - chatHistory + chatHistory, + availableModels, + updateChatHistory }}> {children} </ChatContext.Provider> diff --git a/src/contexts/Chat/ChatTypes.js b/src/contexts/Chat/ChatTypes.js index 7b4fd86be1be13e03d8e09623cd15ef9c6186541..8321096622ca67091916dc6047220334bad046ad 100644 --- a/src/contexts/Chat/ChatTypes.js +++ b/src/contexts/Chat/ChatTypes.js @@ -2,5 +2,6 @@ export const CHAT_ACTIONS = { SET_CHATS: 'set_chats', SET_HEADINGS: 'set_headings', SET_HISTORY: 'set_history', - UPDATE_CHATID: 'update_chatid' + UPDATE_CHATID: 'update_chatid', + SET_MODELS: 'set_models' }; \ No newline at end of file