From a036a6b9798694d26f4157046bd6736b599f03c6 Mon Sep 17 00:00:00 2001 From: ajaythapliyal Date: Mon, 20 Feb 2023 09:04:24 +0530 Subject: [PATCH 1/2] Adds mock conversation api layer, adds reducers to handle various asyc state and wires it with conversation UI --- frontend/src/assets/spinner.svg | 9 ++++ frontend/src/conversation/Conversation.tsx | 50 +++++++++++++++++-- .../src/conversation/ConversationInput.tsx | 21 -------- frontend/src/conversation/conversationApi.ts | 24 +++++++++ .../src/conversation/conversationModels.ts | 8 +++ .../src/conversation/conversationSlice.ts | 43 +++++++++++++--- frontend/src/store.ts | 1 + 7 files changed, 122 insertions(+), 34 deletions(-) create mode 100644 frontend/src/assets/spinner.svg delete mode 100644 frontend/src/conversation/ConversationInput.tsx create mode 100644 frontend/src/conversation/conversationApi.ts diff --git a/frontend/src/assets/spinner.svg b/frontend/src/assets/spinner.svg new file mode 100644 index 0000000..e18eaac --- /dev/null +++ b/frontend/src/assets/spinner.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 2d54699..9fc770f 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -1,15 +1,32 @@ import { useEffect, useRef } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import Hero from '../Hero'; +import { AppDispatch } from '../store'; import ConversationBubble from './ConversationBubble'; -import ConversationInput from './ConversationInput'; -import { selectConversation } from './conversationSlice'; +import { + addMessage, + fetchAnswer, + selectConversation, + selectStatus, +} from './conversationSlice'; +import Send from './../assets/send.svg'; +import Spinner from './../assets/spinner.svg'; export default function Conversation() { const messages = useSelector(selectConversation); + const status = useSelector(selectStatus); + const dispatch = useDispatch(); const endMessageRef = useRef(null); + const inputRef = useRef(null); - useEffect(() => endMessageRef?.current?.scrollIntoView()); + useEffect(() => + endMessageRef?.current?.scrollIntoView({ behavior: 'smooth' }), + ); + + const handleQuestion = (question: string) => { + dispatch(addMessage({ text: question, type: 'QUESTION' })); + dispatch(fetchAnswer({ question })); + }; return (
@@ -27,7 +44,30 @@ export default function Conversation() { })} {messages.length === 0 && }
- +
+
+ {status === 'loading' ? ( + + ) : ( + { + if (inputRef.current?.textContent) { + handleQuestion(inputRef.current.textContent); + inputRef.current.textContent = ''; + } + }} + src={Send} + className="relative right-9 cursor-pointer" + > + )} +
); } diff --git a/frontend/src/conversation/ConversationInput.tsx b/frontend/src/conversation/ConversationInput.tsx deleted file mode 100644 index 9dca9f7..0000000 --- a/frontend/src/conversation/ConversationInput.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Send from './../assets/send.svg'; - -export default function ConversationInput({ - className, -}: { - className?: string; -}) { - return ( -
-
- console.log('here')} - src={Send} - className="relative right-9" - > -
- ); -} diff --git a/frontend/src/conversation/conversationApi.ts b/frontend/src/conversation/conversationApi.ts new file mode 100644 index 0000000..c58532b --- /dev/null +++ b/frontend/src/conversation/conversationApi.ts @@ -0,0 +1,24 @@ +import { Answer } from './conversationModels'; + +export function fetchAnswerApi( + question: string, + apiKey: string, +): Promise { + // a mock answer generator, this is going to be replaced with real http call + return new Promise((resolve) => { + setTimeout(() => { + let result = ''; + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + let counter = 0; + while (counter < 5) { + result += characters.charAt( + Math.floor(Math.random() * charactersLength), + ); + counter += 1; + } + resolve({ answer: result, query: question, result }); + }, 3000); + }); +} diff --git a/frontend/src/conversation/conversationModels.ts b/frontend/src/conversation/conversationModels.ts index 1172f76..e2b55e4 100644 --- a/frontend/src/conversation/conversationModels.ts +++ b/frontend/src/conversation/conversationModels.ts @@ -1,4 +1,5 @@ export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER'; +export type Status = 'idle' | 'loading' | 'failed'; export interface Message { text: string; @@ -7,4 +8,11 @@ export interface Message { export interface ConversationState { conversation: Message[]; + status: Status; +} + +export interface Answer { + answer: string; + query: string; + result: string; } diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index a20b378..7417391 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -1,6 +1,7 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import store from '../store'; -import { ConversationState, Message } from './conversationModels'; +import { fetchAnswerApi } from './conversationApi'; +import { Answer, ConversationState, Message } from './conversationModels'; // harcoding the initial state just for demo const initialState: ConversationState = { @@ -24,13 +25,21 @@ const initialState: ConversationState = { { text: 'what is ChatGPT', type: 'QUESTION' }, { text: 'ChatGPT is large learning model', type: 'ANSWER' }, { text: 'what is ChatGPT', type: 'QUESTION' }, - { - text: 'ChatGPT is large learning model', - type: 'ANSWER', - }, + { text: 'ChatGPT is large learning model', type: 'ANSWER' }, ], + status: 'idle', }; +export const fetchAnswer = createAsyncThunk< + Answer, + { question: string }, + { state: RootState } +>('fetchAnswer', async ({ question }, { getState }) => { + const state = getState(); + const answer = await fetchAnswerApi(question, state.preference.apiKey); + return answer; +}); + export const conversationSlice = createSlice({ name: 'conversation', initialState, @@ -39,12 +48,30 @@ export const conversationSlice = createSlice({ state.conversation.push(action.payload); }, }, + extraReducers(builder) { + builder + .addCase(fetchAnswer.pending, (state) => { + state.status = 'loading'; + }) + .addCase(fetchAnswer.fulfilled, (state, action) => { + state.status = 'idle'; + state.conversation.push({ + text: action.payload.answer, + type: 'ANSWER', + }); + }) + .addCase(fetchAnswer.rejected, (state) => { + state.status = 'failed'; + }); + }, }); -export const { addMessage } = conversationSlice.actions; - type RootState = ReturnType; + export const selectConversation = (state: RootState) => state.conversation.conversation; +export const selectStatus = (state: RootState) => state.conversation.status; + +export const { addMessage } = conversationSlice.actions; export default conversationSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 74f573d..49f5cc4 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -9,4 +9,5 @@ const store = configureStore({ }, }); +export type AppDispatch = typeof store.dispatch; export default store; From f7463699be6698ab0747cbf0662c2c0d017b5fc6 Mon Sep 17 00:00:00 2001 From: ajaythapliyal Date: Mon, 20 Feb 2023 09:19:57 +0530 Subject: [PATCH 2/2] removes hardcoded conversation --- .../src/conversation/conversationSlice.ts | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 7417391..82e62ce 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -3,30 +3,8 @@ import store from '../store'; import { fetchAnswerApi } from './conversationApi'; import { Answer, ConversationState, Message } from './conversationModels'; -// harcoding the initial state just for demo const initialState: ConversationState = { - conversation: [ - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - { text: 'what is ChatGPT', type: 'QUESTION' }, - { text: 'ChatGPT is large learning model', type: 'ANSWER' }, - ], + conversation: [], status: 'idle', };