Skip to content

Commit

Permalink
Merge pull request #82 from BinaryStudioAcademy/task/OV-42-add-chat-c…
Browse files Browse the repository at this point in the history
…omponent

OV-42: Add Chat Component
  • Loading branch information
nikita-remeslov authored Sep 12, 2024
2 parents 43f99da + 9e9ba40 commit 8c1d397
Show file tree
Hide file tree
Showing 66 changed files with 1,110 additions and 59 deletions.
6 changes: 3 additions & 3 deletions backend/src/bundles/chat/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { HttpCode, HTTPMethod } from '~/common/http/http.js';
import { type Logger } from '~/common/logger/logger.js';
import { MAX_TOKEN } from '~/common/services/open-ai/libs/constants/constants.js';
import {
ChatPath,
ChatApiPath,
OpenAIRole,
} from '~/common/services/open-ai/libs/enums/enums.js';
import { type OpenAIService } from '~/common/services/open-ai/open-ai.service.js';
Expand All @@ -34,7 +34,7 @@ class ChatController extends BaseController {
this.chatService = chatService;

this.addRoute({
path: ChatPath.ROOT,
path: ChatApiPath.ROOT,
method: HTTPMethod.POST,
validation: {
body: textGenerationValidationSchema,
Expand All @@ -49,7 +49,7 @@ class ChatController extends BaseController {
});

this.addRoute({
path: ChatPath.ROOT,
path: ChatApiPath.ROOT,
method: HTTPMethod.DELETE,
handler: (options) =>
this.deleteSession(
Expand Down
4 changes: 4 additions & 0 deletions backend/src/common/http/base-http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class BaseHttpApi implements HttpApi {
contentType,
payload = null,
headers: customHeaders,
credentials = 'same-origin',
keepAlive = false,
} = options;

const baseHeaders = [
Expand All @@ -55,6 +57,8 @@ class BaseHttpApi implements HttpApi {
method,
headers,
payload,
credentials,
keepAlive,
});

return this.checkResponse(response) as HttpApiResponse;
Expand Down
7 changes: 6 additions & 1 deletion backend/src/common/http/types/http-api-options.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { type ContentType, type ValueOf } from 'shared';

import { type CustomHeader, type HttpOptions } from './types.js';

type HttpApiOptions = Omit<HttpOptions, 'headers' | 'payload'> & {
type HttpApiOptions = Omit<
HttpOptions,
'headers' | 'payload' | 'credentials' | 'keepAlive'
> & {
contentType: ValueOf<typeof ContentType>;
headers?: CustomHeader;
payload?: HttpOptions['payload'];
credentials?: HttpOptions['credentials'];
keepAlive?: HttpOptions['keepAlive'];
};

export { type HttpApiOptions };
2 changes: 1 addition & 1 deletion backend/src/common/services/open-ai/libs/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { OpenAIRole } from './open-ai-role.enum.js';
export { ChatPath } from 'shared';
export { ChatApiPath } from 'shared';
58 changes: 58 additions & 0 deletions frontend/src/bundles/chat/chat-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ApiPath, ContentType } from '~/bundles/common/enums/enums.js';
import { type Http, HTTPMethod } from '~/framework/http/http.js';
import { BaseHttpApi } from '~/framework/http-api/http-api.js';
import { type Storage } from '~/framework/storage/storage.js';

import { ChatApiPath } from './enums/enums.js';
import {
type DeleteChatResponseDto,
type GenerateTextRequestDto,
type GenerateTextResponseDto,
} from './types/types.js';

type Constructor = {
baseUrl: string;
http: Http;
storage: Storage;
};

class ChatApi extends BaseHttpApi {
public constructor({ baseUrl, http, storage }: Constructor) {
super({ path: ApiPath.CHAT, baseUrl, http, storage });
}

public async sendMessage(
payload: GenerateTextRequestDto,
): Promise<GenerateTextResponseDto> {
const response = await this.load(
this.getFullEndpoint(ChatApiPath.ROOT, {}),
{
method: HTTPMethod.POST,
contentType: ContentType.JSON,
payload: JSON.stringify(payload),
credentials: 'include',
hasAuth: true,
},
);

return await response.json<GenerateTextResponseDto>();
}

public async deleteChat(): Promise<DeleteChatResponseDto> {
const response = await this.load(
this.getFullEndpoint(ChatApiPath.ROOT, {}),
{
method: HTTPMethod.DELETE,
contentType: ContentType.JSON,
payload: JSON.stringify({}),
credentials: 'include',
keepAlive: true,
hasAuth: true,
},
);

return await response.json<DeleteChatResponseDto>();
}
}

export { ChatApi };
15 changes: 15 additions & 0 deletions frontend/src/bundles/chat/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { config } from '~/framework/config/config.js';
import { http } from '~/framework/http/http.js';
import { storage } from '~/framework/storage/storage.js';

import { ChatApi } from './chat-api.js';

const chatApi = new ChatApi({
baseUrl: config.ENV.API.ORIGIN_URL,
storage,
http,
});

export { chatApi };
export { type GenerateTextRequestDto } from './types/types.js';
export { textGenerationValidationSchema } from './validation-schemas/validation-schemas.js';
27 changes: 27 additions & 0 deletions frontend/src/bundles/chat/components/chat-body/chat-body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { MessageList } from '~/bundles/chat/components/message-list/message-list.js';
import { type Message } from '~/bundles/chat/types/types.js';
import { Box, Loader } from '~/bundles/common/components/components.js';
import { DataStatus } from '~/bundles/common/enums/enums.js';
import { useAppSelector } from '~/bundles/common/hooks/hooks.js';

type Properties = {
messages: Message[];
};

const ChatBody: React.FC<Properties> = ({ messages }) => {
const { dataStatus } = useAppSelector(({ chat }) => ({
dataStatus: chat.dataStatus,
}));

return (
<Box minHeight="400px" maxHeight="400px" overflowY="auto" mb="2">
{dataStatus === DataStatus.PENDING ? (
<Loader />
) : (
<MessageList messages={messages} />
)}
</Box>
);
};

export { ChatBody };
56 changes: 56 additions & 0 deletions frontend/src/bundles/chat/components/chat-footer/chat-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
type GenerateTextRequestDto,
textGenerationValidationSchema,
} from '~/bundles/chat/chat.js';
import {
Button,
Flex,
FormProvider,
Input,
} from '~/bundles/common/components/components.js';
import { useAppForm } from '~/bundles/common/hooks/hooks.js';

import { DEFAULT_CHAT_FORM_PAYLOAD } from './constants/constants.js';

type Properties = {
onSendMessage: (payload: GenerateTextRequestDto) => void;
};

const ChatFooter: React.FC<Properties> = ({ onSendMessage }) => {
const form = useAppForm<GenerateTextRequestDto>({
initialValues: DEFAULT_CHAT_FORM_PAYLOAD,
validationSchema: textGenerationValidationSchema,
onSubmit: (data: GenerateTextRequestDto, { resetForm }) => {
onSendMessage(data);
resetForm();
},
});

const { handleSubmit, values } = form;

return (
<FormProvider value={form}>
<form onSubmit={handleSubmit}>
<Flex alignItems={'flex-start'} w={'100%'} gap={5}>
<Input
type="text"
label=""
placeholder="Send a message"
name="message"
value={values.message}
width="100%"
/>
<Button
type="submit"
label="Send"
size="md"
isDisabled={values.message.trim().length === 0}
width="100px"
/>
</Flex>
</form>
</FormProvider>
);
};

export { ChatFooter };
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type GenerateTextRequestDto } from '~/bundles/chat/types/types.js';

const DEFAULT_CHAT_FORM_PAYLOAD: GenerateTextRequestDto = {
message: '',
};

export { DEFAULT_CHAT_FORM_PAYLOAD };
25 changes: 25 additions & 0 deletions frontend/src/bundles/chat/components/chat-header/chat-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Box, Heading, Text } from '~/bundles/common/components/components.js';

type Properties = {
title: string;
comment: string;
};

const ChatHeader: React.FC<Properties> = ({ title, comment }) => {
return (
<Box
bg="background.600"
p="20px"
color="white"
borderTopLeftRadius="xl"
borderTopRightRadius="xl"
>
<Heading variant="H2" mb={2}>
{title}
</Heading>
<Text variant="bodySmall">{comment}</Text>
</Box>
);
};

export { ChatHeader };
34 changes: 34 additions & 0 deletions frontend/src/bundles/chat/components/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ChatBody } from '~/bundles/chat/components/chat-body/chat-body.js';
import { ChatFooter } from '~/bundles/chat/components/chat-footer/chat-footer.js';
import { ChatHeader } from '~/bundles/chat/components/chat-header/chat-header.js';
import {
type GenerateTextRequestDto,
type Message,
} from '~/bundles/chat/types/types.js';
import { Box } from '~/bundles/common/components/components.js';

type Properties = {
messages: Message[];
onSendMessage: (payload: GenerateTextRequestDto) => void;
headerTitle: string;
headerComment: string;
};

const Chat: React.FC<Properties> = ({
messages,
onSendMessage,
headerTitle,
headerComment,
}) => {
return (
<Box>
<ChatHeader title={headerTitle} comment={headerComment} />
<Box p="10">
<ChatBody messages={messages} />
<ChatFooter onSendMessage={onSendMessage} />
</Box>
</Box>
);
};

export { Chat };
1 change: 1 addition & 0 deletions frontend/src/bundles/chat/components/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Chat } from './chat/chat.js';
29 changes: 29 additions & 0 deletions frontend/src/bundles/chat/components/message-box/message-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MessageSender } from '~/bundles/chat/enums/enums.js';
import { type Message } from '~/bundles/chat/types/types.js';
import { Box, Flex, Text } from '~/bundles/common/components/components.js';

import styles from './styles.module.css';

type Properties = {
message: Message;
};

const MessageBox: React.FC<Properties> = ({ message }) => {
const { sender, text } = message;

return (
<Box
className={`${styles['message-box__container']} ${styles[sender === MessageSender.USER ? 'message-box__container--user' : 'message-box__container--ai']}`}
>
<Flex
className={`${styles['message-box']} ${styles[sender === MessageSender.USER ? 'message-box--user' : 'message-box--ai']}`}
>
<Text color="black" bg="background.50" p={3} borderRadius="md">
{text}
</Text>
</Flex>
</Box>
);
};

export { MessageBox };
22 changes: 22 additions & 0 deletions frontend/src/bundles/chat/components/message-box/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.message-box__container {
width: 100%;
}
.message-box__container--user {
align-self: flex-start;
}
.message-box__container--ai {
align-self: flex-end;
}
.message-box {
flex-direction: column;
}
.message-box--user {
align-items: flex-end;
margin-left: 50px;
margin-right: 10px;
}
.message-box--ai {
align-items: flex-start;
margin-left: 10px;
margin-right: 50px;
}
Loading

0 comments on commit 8c1d397

Please sign in to comment.