Adds mock conversation api layer, adds reducers to handle various asyc

state and wires it with conversation UI
This commit is contained in:
ajaythapliyal 2023-02-20 09:04:24 +05:30
parent 63859a814b
commit a036a6b979
7 changed files with 122 additions and 34 deletions

View File

@ -0,0 +1,9 @@
<svg width="30" height="33" viewBox="0 0 30 33" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="30" height="33" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_1_917" transform="scale(0.0166667 0.0151515)"/>
</pattern>
<image id="image0_1_917" width="60" height="66" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -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<AppDispatch>();
const endMessageRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(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 (
<div className="flex justify-center p-6">
@ -27,7 +44,30 @@ export default function Conversation() {
})}
{messages.length === 0 && <Hero className="mt-24"></Hero>}
</div>
<ConversationInput className="fixed bottom-2 w-10/12 md:w-[50%]"></ConversationInput>
<div className="fixed bottom-2 flex w-10/12 md:w-[50%]">
<div
ref={inputRef}
contentEditable
className={`min-h-5 border-000000 overflow-x-hidden; max-h-24 w-full overflow-y-auto rounded-xl border bg-white p-2 pr-9 opacity-100 focus:border-2 focus:outline-none`}
></div>
{status === 'loading' ? (
<img
src={Spinner}
className="relative right-9 animate-spin cursor-pointer"
></img>
) : (
<img
onClick={() => {
if (inputRef.current?.textContent) {
handleQuestion(inputRef.current.textContent);
inputRef.current.textContent = '';
}
}}
src={Send}
className="relative right-9 cursor-pointer"
></img>
)}
</div>
</div>
);
}

View File

@ -1,21 +0,0 @@
import Send from './../assets/send.svg';
export default function ConversationInput({
className,
}: {
className?: string;
}) {
return (
<div className={`${className} flex`}>
<div
contentEditable
className={`min-h-5 border-000000 overflow-x-hidden; max-h-24 w-full overflow-y-auto rounded-xl border bg-white p-2 pr-9 opacity-100 focus:border-2 focus:outline-none`}
></div>
<img
onClick={() => console.log('here')}
src={Send}
className="relative right-9"
></img>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { Answer } from './conversationModels';
export function fetchAnswerApi(
question: string,
apiKey: string,
): Promise<Answer> {
// 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);
});
}

View File

@ -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;
}

View File

@ -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<typeof store.getState>;
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;

View File

@ -9,4 +9,5 @@ const store = configureStore({
},
});
export type AppDispatch = typeof store.dispatch;
export default store;