Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
  • snuggle
  • userHandling
3 results

Target

Select target project
  • zbhai/ragchat-frontend
1 result
Select Git revision
  • main
  • snuggle
  • userHandling
3 results
Show changes
Commits on Source (4)
Showing
with 524 additions and 23 deletions
VITE_APP_NAME=ZBH Portal
VITE_PAGE_AFTER_LOGIN=/
VITE_LOCALE=en-US
......@@ -24,8 +24,10 @@ cp ./.env.template.local ./.env.production.local
# Roadmap
- [ ] complete pages
- [ ] sign up
- [ ] resend verification code
- [ ] onboarding / RAGChat
- [ ] admin-login
- [ ] admin-page with LLM options
- [ ] fix errors
- [ ] fix JWT renewal
- [ ] fix axios' JWT auto renewal
- [ ] check width of label & submit on cleanLayout
\ No newline at end of file
......@@ -391,14 +391,16 @@ cp ./.env.template.local ./.env.production.local
<ul>
<li><input type="checkbox" id="checkbox0"><label for="checkbox0">complete pages</label>
<ul>
<li><input type="checkbox" id="checkbox1"><label for="checkbox1">sign up</label></li>
<li><input type="checkbox" id="checkbox1"><label for="checkbox1">resend verification code</label></li>
<li><input type="checkbox" id="checkbox2"><label for="checkbox2">onboarding / RAGChat</label></li>
<li><input type="checkbox" id="checkbox3"><label for="checkbox3">admin-login</label></li>
<li><input type="checkbox" id="checkbox4"><label for="checkbox4">admin-page with LLM options</label></li>
</ul>
</li>
<li><input type="checkbox" id="checkbox3"><label for="checkbox3">fix errors</label>
<li><input type="checkbox" id="checkbox5"><label for="checkbox5">fix errors</label>
<ul>
<li><input type="checkbox" id="checkbox4"><label for="checkbox4">fix JWT renewal</label></li>
<li><input type="checkbox" id="checkbox5"><label for="checkbox5">check width of label &amp; submit on cleanLayout</label></li>
<li><input type="checkbox" id="checkbox6"><label for="checkbox6">fix axios' JWT auto renewal</label></li>
<li><input type="checkbox" id="checkbox7"><label for="checkbox7">check width of label &amp; submit on cleanLayout</label></li>
</ul>
</li>
</ul>
......
import React from 'react';
import { useChat } from '../../contexts/Chat/ChatState';
const Chat = ({ id, title, time }) => {
// #################################
// HOOKS
// #################################
// ### CONNECT CONTEXT
const { currentChatId, selectChat } = useChat();
// #################################
// FUNCTIONS
// #################################
// ### mark active chat
const active = (currentChatId === id ? 'bg-gray-200' : 'bg-white');
// #################################
// OUTPUT
// #################################
return (
<button onClick={() => { selectChat(id); }} className={`block p-1 hover:bg-gray-200 m-1 rounded-md ${active}`} title={title}>
<div className={'flex items-center p-2 cursor-pointer '}>
<div className="flex-grow p-2">
<div className="flex justify-between text-md">
<div className="text-xs text-gray-400 dark:text-gray-300">{time}</div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 w-40 truncate">
{title}
</div>
</div>
</div>
</button>
);
};
export default Chat;
\ No newline at end of file
import React, { useEffect, useState } from 'react';
import Chat from './Chat';
import Heading from '../font/Heading';
import { RiAddCircleLine, RiArrowLeftCircleLine, RiArrowRightCircleLine } from 'react-icons/ri';
import { useChat } from '../../contexts/Chat/ChatState';
import { useParams } from 'react-router-dom';
const Chats = () => {
// #################################
// HOOKS
// #################################
// ### showSidebar
const initialShowSidebar = true;
const [showSidebar, setShowSidebar] = useState(initialShowSidebar);
// FETCH CHAT ID FROM URL
const { id } = useParams();
// ### CONNECT CONTEXT
const { fetchAllChats, chatHeadings, currentChatId, selectChat } = useChat();
// ### FETCH CHATS;
useEffect(() => {
// ### on run exec this code
const controller = new AbortController();
const getChats = async () => {
try {
// fetch all chats
await fetchAllChats(id || null);
} catch (error) {
console.error(error);
mergeBackendValidation(error.response.status, error.response.data);
}
};
getChats();
// ### return will be executed on unmounting this component
return () => {
// on unmount abort request
controller.abort();
};
}, []);
// #################################
// FUNCTIONS
// #################################
// #################################
// OUTPUT
// #################################
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) => (
<Chat
key={chat.id}
id={chat.id}
title={chat.title}
time={new Intl.DateTimeFormat(import.meta.env.VITE_LOCALE).format(new Date(chat.created))}
/>
))
}
</div>
<div className="mt-auto mb-8 flex justify-end text text-4xl" title='show / hide Chat'>
<button onClick={() => setShowSidebar(!showSidebar)}>
{showSidebar ? <RiArrowLeftCircleLine /> : <RiArrowRightCircleLine />}
</button>
</div>
</div >
);
};
export default Chats;
\ No newline at end of file
import React from 'react';
function Message({ sender, message }) {
// #################################
// HOOKS
// #################################
// #################################
// FUNCTIONS
// #################################
// TODO: use tailwind merge
// AI css
let tilePosition = '';
let tileColor = 'bg-UhhGrey';
let tileBorder = 'rounded-bl-none';
let tileMargin = 'mr-8';
let senderClasses = 'text-UhhLightGrey';
let messageClasses = 'text-UhhWhite';
// user css
if (sender === 'human') {
tilePosition = 'justify-end';
tileColor = 'bg-UhhLightGrey';
tileBorder = 'rounded-br-none';
tileMargin = 'ml-8';
senderClasses = 'text-UhhGrey';
messageClasses = 'text-UhhGrey';
}
// #################################
// OUTPUT
// #################################
return (
<div className={`flex ${tilePosition}`} >
<div className={`p-3 mx-3 my-1 rounded-2xl ${tileBorder} ${tileColor} ${tileMargin}`}>
<div className={`text-xs flex justify-between ${senderClasses}`} >
<div>{sender}</div>
</div>
<div className={`${messageClasses} whitespace-pre-line`}>
{message}
</div>
</div>
</div>
);
}
export default React.memo(Message);
\ No newline at end of file
import React, { useEffect, useRef } from 'react';
import Message from './Message';
import { useChat } from '../../contexts/Chat/ChatState';
import { Link } from 'react-router-dom';
import { RxBookmark } from 'react-icons/rx';
const Messages = () => {
// #################################
// HOOKS
// #################################
// ### 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
controller.abort();
};
}, [currentChatId]);
// ### SCROLL TO BOTTOM
const bottomRef = useRef();
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [chatHistory]);
// #################################
// FUNCTIONS
// #################################
// #################################
// OUTPUT
// #################################
return (
<div className="row-start-2 overflow-auto">
{currentChatId && <Link to={`/onboarding/${currentChatId}`} className='text-UhhBlue' target='_blank' rel='noopener noreferrer'><RxBookmark /></Link>}
{chatHistory.map((prompt, index) => (
<Message
key={index}
sender={prompt.type}
message={prompt.data.content}
/>
))
}
{!chatHistory.length && <div className="text-center text-gray-500">No messages yet</div>}
<div ref={bottomRef}> </div>
</div>
);
};
export default Messages;
\ No newline at end of file
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
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() {
// #################################
// VALIDATION SCHEMA
// #################################
const schema = z.object({
input: z.string().min(1),
model: z.string().min(1)
});
// #################################
// HOOKS
// #################################
// ### CONNECT CONTEXT
const { currentChatId, availableModels, updateChatHistory } = useChat();
// ### PREPARE FORM
const methods = useForm({
resolver: zodResolver(schema),
mode: 'onSubmit',
defaultValues: {
input: '',
model: 'llama2'
}
});
// #################################
// FUNCTIONS
// #################################
// ### HANDLE SUBMITTING FORM
async function handleSendForm(inputs) {
// invoke chatID if available
if (currentChatId) { inputs.chatId = currentChatId; };
// send data to api
try {
// send input to api
const result = await api.post('/ai/chat', inputs);
// 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);
}
}
// TODO fetch available model names from backend
// TODO make model a dropdown
// #################################
// OUTPUT
// #################################
return (
<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 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">
<RiSendPlane2Line />
</button>
</div>
</form>
</FormProvider>
</div>
);
}
export default React.memo(PromptInput);
\ No newline at end of file
......@@ -60,7 +60,6 @@ function FlatListEdit({ methods, initialItems, fieldName, keyName, validator, ta
const isMail = validator(input);
return isMail.success ?? keyName;
});
console.log('validInputs', validInputs);
// handle invalid inputs
// strip off valid entries from input to get remaining invalid inputs
......
......@@ -49,7 +49,6 @@ function FlatListEdit({ initialItems = [], fieldName = '', tooltip = '', columns
});
// exit if no valid items found
if (validInputs.length === 0) return;
console.log('validInputs', validInputs);
// handle invalid inputs
// strip off valid entries from input to get remaining invalid inputs
......@@ -60,7 +59,6 @@ function FlatListEdit({ initialItems = [], fieldName = '', tooltip = '', columns
// set error message on input
methods.setError('addItem', { message: 'invalid entries remaining' });
}
console.log('invalidInputs', invalidInputs);
// handle valid inputs
......
......@@ -42,7 +42,7 @@ function Input({ title, name, type, className, tooltip, ...props }) {
<>
<label htmlFor={id}>
{props.required && <RequiredBadge />}
{capitalizeFirstLetter(title)}
{title && capitalizeFirstLetter(title)}
{tooltip && <Tooltip>{tooltip}</Tooltip>}
<input {...register(name)}
......
......@@ -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
......
......@@ -26,12 +26,22 @@ function AuthState({ children }) {
// ### LOGIN
async function login(credentials) {
const result = await api.post(
let result = {};
try {
result = await api.post(
'/users/login',
credentials,
{ withCredentials: true }
);
console.log;
} catch (error) {
result = await api.post(
'/users/adminlogin',
credentials,
{ withCredentials: true }
);
// try to match output with normal user
result.data.record = { ...result.data.admin, isAdmin: true };
}
// set current user to login and merge accessToken into currentUser
dispatchCurrentUser({ type: USER_ACTIONS.SET, payload: { ...result.data.record } });
setAccessToken(result.data.token);
......
import { createContext } from "react";
const ChatContext = createContext();
export default ChatContext;
\ No newline at end of file
import { CHAT_ACTIONS } from './ChatTypes';
const chatReducer = (state, action) => {
switch (action.type) {
case CHAT_ACTIONS.SET_CHATS:
case CHAT_ACTIONS.SET_HEADINGS:
case CHAT_ACTIONS.UPDATE_CHATID:
{ 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; }
}
};
export default chatReducer;
\ No newline at end of file
import React, { useContext, useReducer, useState } from 'react';
import ChatContext from './ChatContext';
import chatReducer from './ChatReducer';
import { CHAT_ACTIONS } from './ChatTypes';
import api from '../../utils/AxiosConfig';
import { mergeBackendValidation } from '../../utils/ErrorHandling';
// ### EXPORT useContext TO REDUCE NEEDED CODE IN CLIENT FILES
export function useChat() {
return useContext(ChatContext);
}
function ChatState({ children }) {
// #################################
// HOOKS
// #################################
const [chats, dispatchChats] = useReducer(chatReducer, []);
const [chatHeadings, dispatchChatHeadings] = useReducer(chatReducer, []);
const [currentChatId, dispatchCurrentChatId] = useReducer(chatReducer, null);
const [chatHistory, dispatchChatHistory] = useReducer(chatReducer, []);
const [availableModels, dispatchAvailableModels] = useReducer(chatReducer, []);
// #################################
// FUNCTIONS
// #################################
// ### FETCH CHATS
async function fetchAllChats(id = null) {
try {
// load all chats and save them
const items = await api.get('/ai/chats');
dispatchChats({ type: CHAT_ACTIONS.SET_CHATS, payload: items.data.chats });
// fetch headings from chats
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);
}
}
// ### FETCH HEADINGS FROM CHATS
function fetchChatHeadings(chats) {
// create new array
const headings = [];
// loop through chats
chats.forEach(chat => {
// split history from chat object
const removedKey = 'chatHistory';
const { [removedKey]: removed, ...heading } = chat;
// save in array
headings.push(heading);
});
// save array in state
dispatchChatHeadings({ type: CHAT_ACTIONS.SET_HEADINGS, payload: headings });
}
// ### FETCH CHAT HISTORY
function fetchChatHistory(id) {
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);
chats[index].chatHistory = history;
dispatchChatHistory({ type: CHAT_ACTIONS.SET_HISTORY, payload: { id, chats } });
}
// ### SELECT CHAT
function selectChat(id) {
// return null if no id is provided (just to prevent switching between null and undefined)
if (!id) id = null;
// save chat id in state
dispatchCurrentChatId({ type: CHAT_ACTIONS.UPDATE_CHATID, payload: id });
}
// #################################
// OUTPUT
// #################################
return (
<ChatContext.Provider value={{
fetchAllChats,
chatHeadings,
currentChatId,
selectChat,
fetchChatHistory,
chatHistory,
availableModels,
updateChatHistory
}}>
{children}
</ChatContext.Provider>
);
}
export default React.memo(ChatState);
export const CHAT_ACTIONS = {
SET_CHATS: 'set_chats',
SET_HEADINGS: 'set_headings',
SET_HISTORY: 'set_history',
UPDATE_CHATID: 'update_chatid',
SET_MODELS: 'set_models'
};
\ No newline at end of file
import React from 'react';
import Heading from '../components/font/Heading';
function Home() {
// #################################
......@@ -13,9 +14,7 @@ function Home() {
// OUTPUT
// #################################
return (
<h1 className="text-3xl font-bold underline">
Hello world!
</h1>
<Heading level="1">Home</Heading>
);
}
......
import React from 'react';
import { Helmet } from 'react-helmet-async';
import Heading from '../../components/font/Heading';
import Chats from '../../components/chat/Chats';
import Messages from '../../components/chat/Messages';
import PromptInput from '../../components/chat/PromptInput';
import ChatState from '../../contexts/Chat/ChatState';
function Onboarding() {
// #################################
// HOOKS
// #################################
// #################################
// FUNCTIONS
// #################################
......@@ -13,8 +21,18 @@ function Onboarding() {
// OUTPUT
// #################################
return (
<div>TEST Onboarding</div>
<div className="h-full grid grid-rows-[auto_1fr_auto] grid-cols-[auto_1fr]">
{/* render page title */}
<Helmet><title>[{import.meta.env.VITE_APP_NAME}] Onboarding</title></Helmet>
<Heading level="1" className="col-span-2">Onboarding</Heading>
<ChatState>
<Chats />
<Messages />
<PromptInput />
</ChatState>
</div>
);
}
export default React.memo(Onboarding);
\ No newline at end of file
export default React.memo(Onboarding);;
\ No newline at end of file
......@@ -56,7 +56,6 @@ function ForgotPassword() {
setFlashMsg(result.data?.message);
} catch (error) {
// catch the error
console.error(error);
mergeBackendValidation(error.response.status, error.response.data, methods.setError);
}
}
......