Skip to content
Snippets Groups Projects
Commit 0013e743 authored by Embruch, Gerd's avatar Embruch, Gerd
Browse files

added chat input an history update

parent 20dfd2ba
Branches
No related tags found
No related merge requests found
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Chat from './Chat'; import Chat from './Chat';
import Heading from '../font/Heading'; 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 { useChat } from '../../contexts/Chat/ChatState';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
...@@ -17,7 +17,7 @@ const Chats = () => { ...@@ -17,7 +17,7 @@ const Chats = () => {
const { id } = useParams(); const { id } = useParams();
// ### CONNECT CONTEXT // ### CONNECT CONTEXT
const { fetchAllChats, chatHeadings } = useChat(); const { fetchAllChats, chatHeadings, currentChatId, selectChat } = useChat();
// ### FETCH CHATS; // ### FETCH CHATS;
useEffect(() => { useEffect(() => {
...@@ -26,8 +26,8 @@ const Chats = () => { ...@@ -26,8 +26,8 @@ const Chats = () => {
const getChats = async () => { const getChats = async () => {
try { try {
// fetch all chats (and directly show id if provided) // fetch all chats
await fetchAllChats(id); await fetchAllChats(id || null);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
mergeBackendValidation(error.response.status, error.response.data); mergeBackendValidation(error.response.status, error.response.data);
...@@ -42,6 +42,7 @@ const Chats = () => { ...@@ -42,6 +42,7 @@ const Chats = () => {
}; };
}, []); }, []);
// ################################# // #################################
// FUNCTIONS // FUNCTIONS
// ################################# // #################################
...@@ -52,6 +53,9 @@ const Chats = () => { ...@@ -52,6 +53,9 @@ const Chats = () => {
return ( return (
<div className="row-start-2 row-span-2 border-r-2 border-UhhGrey flex flex-col"> <div className="row-start-2 row-span-2 border-r-2 border-UhhGrey flex flex-col">
<Heading level="6" className="text-center">Recent</Heading> <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"> <div className="p-1">
{showSidebar && {showSidebar &&
chatHeadings.map((chat, index) => ( chatHeadings.map((chat, index) => (
......
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import Message from './Message'; import Message from './Message';
import { useChat } from '../../contexts/Chat/ChatState'; import { useChat } from '../../contexts/Chat/ChatState';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
...@@ -12,11 +12,14 @@ const Messages = () => { ...@@ -12,11 +12,14 @@ const Messages = () => {
// ### CONNECT CONTEXT // ### CONNECT CONTEXT
const { currentChatId, fetchChatHistory, chatHistory } = useChat(); const { currentChatId, fetchChatHistory, chatHistory } = useChat();
// ### FETCH HISTORY EVERY TIME THE CHAT ID CHANGES
useEffect(() => { useEffect(() => {
// ### on run exec this code // ### on run exec this code
const controller = new AbortController(); const controller = new AbortController();
// ### fetch chat history based on current chat id // ### fetch chat history based on current chat id
fetchChatHistory(currentChatId); fetchChatHistory(currentChatId);
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
// ### return will be executed on unmounting this component // ### return will be executed on unmounting this component
return () => { return () => {
// on unmount abort request // on unmount abort request
...@@ -24,6 +27,11 @@ const Messages = () => { ...@@ -24,6 +27,11 @@ const Messages = () => {
}; };
}, [currentChatId]); }, [currentChatId]);
// ### SCROLL TO BOTTOM
const bottomRef = useRef();
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [chatHistory]);
// ################################# // #################################
// FUNCTIONS // FUNCTIONS
...@@ -34,7 +42,7 @@ const Messages = () => { ...@@ -34,7 +42,7 @@ const Messages = () => {
// ################################# // #################################
return ( return (
<div className="row-start-2 overflow-auto"> <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) => ( {chatHistory.map((prompt, index) => (
<Message <Message
key={index} key={index}
...@@ -43,7 +51,10 @@ const Messages = () => { ...@@ -43,7 +51,10 @@ const Messages = () => {
/> />
)) ))
} }
{!chatHistory.length && <div className="text-center text-gray-500">No messages yet</div>} {!chatHistory.length && <div className="text-center text-gray-500">No messages yet</div>}
<div ref={bottomRef}> </div>
</div> </div>
); );
}; };
......
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react'; import React from 'react';
import { FormProvider, useForm } from 'react-hook-form'; 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 { z } from 'zod';
import Input from '../form/Input'; import Input from '../form/Input';
import { mergeBackendValidation } from '../../utils/ErrorHandling'; import { mergeBackendValidation } from '../../utils/ErrorHandling';
import api from '../../utils/AxiosConfig'; import api from '../../utils/AxiosConfig';
import { useChat } from '../../contexts/Chat/ChatState'; import { useChat } from '../../contexts/Chat/ChatState';
import Select from '../form/Select';
function PromptInput() { function PromptInput() {
// ################################# // #################################
...@@ -22,7 +23,7 @@ function PromptInput() { ...@@ -22,7 +23,7 @@ function PromptInput() {
// HOOKS // HOOKS
// ################################# // #################################
// ### CONNECT CONTEXT // ### CONNECT CONTEXT
const { currentChatId } = useChat(); const { currentChatId, availableModels, updateChatHistory } = useChat();
// ### PREPARE FORM // ### PREPARE FORM
const methods = useForm({ const methods = useForm({
...@@ -43,12 +44,15 @@ function PromptInput() { ...@@ -43,12 +44,15 @@ function PromptInput() {
if (currentChatId) { inputs.chatId = currentChatId; }; if (currentChatId) { inputs.chatId = currentChatId; };
// send data to api // send data to api
try { try {
// send input to api
const result = await api.post('/ai/chat', inputs); const result = await api.post('/ai/chat', inputs);
// TODO: update chat context // update chat history
console.log("🚀 ~ handleSendForm ~ result:", result); updateChatHistory(currentChatId, result.data.chat.chatHistory);
// clear input field
methods.resetField('input');
} catch (error) { } catch (error) {
console.error(error);
// merge front & backend validation errors // merge front & backend validation errors
mergeBackendValidation(error.response.status, error.response.data, methods.setError); mergeBackendValidation(error.response.status, error.response.data, methods.setError);
} }
...@@ -63,8 +67,10 @@ function PromptInput() { ...@@ -63,8 +67,10 @@ function PromptInput() {
<div className="row-start-3 p-3 border-t-2 border-UhhGrey"> <div className="row-start-3 p-3 border-t-2 border-UhhGrey">
<FormProvider {...methods} > <FormProvider {...methods} >
<form onSubmit={methods.handleSubmit(handleSendForm)}> <form onSubmit={methods.handleSubmit(handleSendForm)}>
<div className="flex content-center h-14"> <div className="flex content-center h-14 relative">
<Input name="model" /> {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" /> <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"> <button type="submit" className="h-8 justify-center items-center bg-UhhBlue text-UhhWhite p-1 text-xs">
......
...@@ -44,7 +44,7 @@ function Select({ options, name, title, defaultValue, className, tooltip, type, ...@@ -44,7 +44,7 @@ function Select({ options, name, title, defaultValue, className, tooltip, type,
return ( return (
<label htmlFor={id}> <label htmlFor={id}>
{props.required && <RequiredBadge />} {props.required && <RequiredBadge />}
{capitalizeFirstLetter(title)} {title && capitalizeFirstLetter(title)}
{tooltip && <Tooltip>{tooltip}</Tooltip>} {tooltip && <Tooltip>{tooltip}</Tooltip>}
<Controller <Controller
......
...@@ -5,10 +5,29 @@ const chatReducer = (state, action) => { ...@@ -5,10 +5,29 @@ const chatReducer = (state, action) => {
case CHAT_ACTIONS.SET_CHATS: case CHAT_ACTIONS.SET_CHATS:
case CHAT_ACTIONS.SET_HEADINGS: case CHAT_ACTIONS.SET_HEADINGS:
case CHAT_ACTIONS.UPDATE_CHATID: 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: default:
return state; { return state; }
} }
}; };
......
...@@ -19,6 +19,7 @@ function ChatState({ children }) { ...@@ -19,6 +19,7 @@ function ChatState({ children }) {
const [chatHeadings, dispatchChatHeadings] = useReducer(chatReducer, []); const [chatHeadings, dispatchChatHeadings] = useReducer(chatReducer, []);
const [currentChatId, dispatchCurrentChatId] = useReducer(chatReducer, null); const [currentChatId, dispatchCurrentChatId] = useReducer(chatReducer, null);
const [chatHistory, dispatchChatHistory] = useReducer(chatReducer, []); const [chatHistory, dispatchChatHistory] = useReducer(chatReducer, []);
const [availableModels, dispatchAvailableModels] = useReducer(chatReducer, []);
...@@ -35,6 +36,9 @@ function ChatState({ children }) { ...@@ -35,6 +36,9 @@ function ChatState({ children }) {
fetchChatHeadings(items.data.chats); fetchChatHeadings(items.data.chats);
// select chat if id is provided // select chat if id is provided
if (id) selectChat(id); 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) { } catch (error) {
// display errors // display errors
mergeBackendValidation(error.response.status, error.response.data); mergeBackendValidation(error.response.status, error.response.data);
...@@ -61,16 +65,18 @@ function ChatState({ children }) { ...@@ -61,16 +65,18 @@ function ChatState({ children }) {
// ### FETCH CHAT HISTORY // ### FETCH CHAT HISTORY
function fetchChatHistory(id) { function fetchChatHistory(id) {
// return empty if no chat was chosen dispatchChatHistory({ type: CHAT_ACTIONS.SET_HISTORY, payload: { id, chats } });
if (!id) return []; }
// ### UPDATE CHAT HISTORY
function updateChatHistory(id, history) {
// define a function to find the chat // define a function to find the chat
const isSelectedChatId = element => element.id === id; const isSelectedChatId = element => element.id === id;
// get index of matching item // get index of matching item
const index = chats.findIndex(isSelectedChatId); const index = chats.findIndex(isSelectedChatId);
// return history of chat chats[index].chatHistory = history;
const history = chats[index].chatHistory;
// save history in state dispatchChatHistory({ type: CHAT_ACTIONS.SET_HISTORY, payload: { id, chats } });
dispatchChatHistory({ type: CHAT_ACTIONS.SET_HISTORY, payload: history });
} }
...@@ -92,7 +98,9 @@ function ChatState({ children }) { ...@@ -92,7 +98,9 @@ function ChatState({ children }) {
currentChatId, currentChatId,
selectChat, selectChat,
fetchChatHistory, fetchChatHistory,
chatHistory chatHistory,
availableModels,
updateChatHistory
}}> }}>
{children} {children}
</ChatContext.Provider> </ChatContext.Provider>
......
...@@ -2,5 +2,6 @@ export const CHAT_ACTIONS = { ...@@ -2,5 +2,6 @@ export const CHAT_ACTIONS = {
SET_CHATS: 'set_chats', SET_CHATS: 'set_chats',
SET_HEADINGS: 'set_headings', SET_HEADINGS: 'set_headings',
SET_HISTORY: 'set_history', SET_HISTORY: 'set_history',
UPDATE_CHATID: 'update_chatid' UPDATE_CHATID: 'update_chatid',
SET_MODELS: 'set_models'
}; };
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment