@@ -0,0 +1,3 @@ +{ + "postman.settings.dotenv-detection-notification-visibility": false +} @@ -1,67 +0,0 @@ -import React from 'react' -import { FormControlLabel, Typography } from '@mui/material' -import Switch from '@mui/material/Switch' - -import { setParams } from '#/app/store/model-parametres-store' -import { useAppDispatch } from '#/app/store/store' -import TooltipModelTypes from '#/widgets/filters-gpt/ui/tooltip-model-types' - -interface IProps { - name: string - value: boolean - filters: any - item_key: string - description: string - setNewParam: (payload: { [p: string]: string | number | number[] | boolean }) => void -} - -export const CheckboxFilter = ({ name, value, filters, item_key, description, setNewParam }: IProps) => { - const [check, setCheck] = React.useState(value || false) - - React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== check) { - setCheck(filters[item_key]) - } - }, [filters]) - - return ( - - { - setCheck(checked) - setNewParam({ [item_key]: checked }) - }} - /> - } - labelPlacement={'end'} - label={ - - {name} - - } - /> - - ) -} @@ -1,64 +0,0 @@ -import React from 'react' -import { Typography } from '@mui/material' -import Box from '@mui/material/Box' -import { useThemeAndDevice } from '#/shared/lib/hooks' -import TooltipModelTypes from '#/widgets/filters-gpt/ui/tooltip-model-types' -import { CommonTextArea } from '#/shared/ui/common-textarea' - -interface IProps { - title: string - item_key: string - values: { - default?: string - } - filters: any - description: string - setNewParam: (payload: { [p: string]: string | number | number[] | boolean }) => void -} - -export const InputFilter = ({ filters, item_key, values, title, description, setNewParam }: IProps) => { - const { theme } = useThemeAndDevice() - const [value, setValue] = React.useState(values?.default || '') - - React.useEffect(() => { - if (filters[item_key] !== value) { - if (filters[item_key] == undefined) { - setValue('') - } else { - setValue(filters[item_key]) - } - } - }, [filters]) - - return ( - - - - - {title} - - { - e.target.style.height = 'auto' - e.target.style.height = Math.min(e.target.scrollHeight, 177) + 'px' - - setNewParam({ [item_key]: e.target.value }) - setValue(e.target.value) - }} - /> - - - - ) -} @@ -9,7 +9,12 @@ interface IProps { export const ResetFilters = ({ reset, desktop, closeDrawer }: IProps) => { return ( - + > - list: IModelVersions[] + setValue: (value: string) => void + list: ModelVersions[] [x: string]: any setDefaultParams: () => void } -export const ChatSelect: React.FC = ({ value, setValue, list, setDefaultParams, ...args }) => { +export const ChatSelect: React.FC = ({ + value, + setValue, + list, + setDefaultParams, + ...args +}) => { const theme = useAppSelector((state) => state.theme.theme) const onChange = (event: SelectChangeEvent) => { @@ -34,7 +40,8 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul MenuListProps: { sx: { color: theme === 'light' ? '#373737' : '#A6A5A5', - backgroundColor: theme === 'light' ? 'transparent' : '#151518', + backgroundColor: + theme === 'light' ? 'transparent' : '#151518', }, }, }, @@ -52,10 +59,16 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul color: '#A6A5A5', }, '&& fieldset': { - border: theme === 'light' ? '1px solid #E9E9E9' : '0px solid transparent', + border: + theme === 'light' + ? '1px solid #E9E9E9' + : '0px solid transparent', }, '&.Mui-focused': { - border: theme === 'light' ? `1px solid ${baseColor}` : '2px solid #40404E;', + border: + theme === 'light' + ? `1px solid ${baseColor}` + : '2px solid #40404E;', borderColor: baseColor, '& .MuiOutlinedInput-notchedOutline': { border: 'none', @@ -63,7 +76,10 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul }, '&:hover': { '&& fieldset': { - border: theme === 'light' ? '1px solid #E9E9E9' : '0px solid transparent', + border: + theme === 'light' + ? '1px solid #E9E9E9' + : '0px solid transparent', }, }, }} @@ -73,7 +89,10 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul {list.map((item, idx) => { return ( - + = { English: 'en', } -async function getModels(token?: string): Promise { +async function getModels(token?: string): Promise { try { const { data } = await axios.get(API_URL + '/ml_models/', { headers: { @@ -66,7 +66,9 @@ const InfoBar: React.FC = ({ device }) => { const { data } = useSession() - const { email, first_name, last_name, profile_picture_link, account_type } = useAppSelector((state) => state.user) + const { email, first_name, last_name, profile_picture_link, account_type } = useAppSelector( + (state) => state.user + ) const dispatch = useAppDispatch() @@ -74,7 +76,7 @@ const InfoBar: React.FC = ({ device }) => { const [searchOpen, setSearchOpen] = useState(false) - const [models, setModels] = useState([]) + const [models, setModels] = useState([]) useEffect(() => { getModels(data?.access).then((res) => setModels(res)) @@ -143,9 +145,14 @@ const InfoBar: React.FC = ({ device }) => { > - Мы уже работаем над этой проблемой. Попробуйте перезайти в аккаунт + Мы уже работаем над этой проблемой. Попробуйте перезайти в + аккаунт - @@ -171,7 +178,10 @@ const InfoBar: React.FC = ({ device }) => { {show_balance && ( - + {declineToken(balance.toString())} @@ -199,7 +209,8 @@ const InfoBar: React.FC = ({ device }) => { sx={{ marginTop: '6px' }} PaperProps={{ style: { - backgroundColor: theme === 'dark' ? '#151518' : 'white', + backgroundColor: + theme === 'dark' ? '#151518' : 'white', borderRadius: '13px', boxShadow: 'none', }, @@ -221,7 +232,9 @@ const InfoBar: React.FC = ({ device }) => { router.push('/account?scope=setting')} + onClick={() => + router.push('/account?scope=setting') + } > = ({ device }) => { router.push('/account?scope=business')} + onClick={() => + router.push( + '/account?scope=business' + ) + } > {''} = ({ device }) => { router.push('/account?scope=referral')} + onClick={() => + router.push( + '/account?scope=referral' + ) + } > {''} = ({ device }) => { router.push('/account?scope=subscribe')} + onClick={() => + router.push( + '/account?scope=subscribe' + ) + } > = ({ device }) => { }} sx={{ marginTop: '15px', cursor: 'pointer' }} > - {''} - + {''} + Выйти @@ -15,15 +15,21 @@ --border-color2: #e7e7e7; --bg-audio: #f2f2fe; + --new-ui-bg-color: #eff0f2; + --new-ui-bg-app-color: #eff0f2; --new-ui-main-color: white; --new-ui-gray-color: #a4aab5; + --new-ui-gray-text-color: #868686; --new-ui-text-color: #2b2b42; --new-ui-border: 2px solid #eff0f2; + --new-ui-border-color: #eff0f2; --new-ui-btn-danger-bg: #ff23721a; --new-ui-ctrl-f-button-bg: #f9f9fc; --new-ui-ctrl-f-button-border: 1px solid #c4cbd8; --new-ui-table-cell-text: #97989f; + + --new-ui-message-text-color: #5e5e5e; } :root[data-theme='dark'] { @@ -43,15 +49,21 @@ --border-color2: #2c2c2c; --bg-audio: #303030; + --new-ui-bg-color: #303035; + --new-ui-bg-app-color: #303035; --new-ui-border: 1px solid #40404e; + --new-ui-border-color: #303035; --new-ui-main-color: #151518; --new-ui-gray-color: #a4aab5; + --new-ui-gray-text-color: #a6a5a5; --new-ui-text-color: white; --new-ui-btn-danger-bg: #ff23721a; --new-ui-ctrl-f-button-bg: #242428; --new-ui-ctrl-f-button-border: 1px solid #303035; --new-ui-table-cell-text: #97989f; + + --new-ui-message-text-color: #a6a5a5; } * { @@ -186,8 +198,12 @@ input:-webkit-autofill:active { /* Изменение цвета активного элемента в выпадающем списке */ .MuiAutocomplete-option.Mui-selected { - background-color: var(--new-ui-main-color); /* Замените #your-selected-color на цвет активного элемента */ - color: var(--new-ui-main-color); /* Замените #your-selected-text-color на цвет текста активного элемента */ + background-color: var( + --new-ui-main-color + ); /* Замените #your-selected-color на цвет активного элемента */ + color: var( + --new-ui-main-color + ); /* Замените #your-selected-text-color на цвет текста активного элемента */ } .introjs-tooltiptext { @@ -422,17 +438,18 @@ textarea { } /*scroll styles*/ -.smallScroll::-webkit-scrollbar { + +*::-webkit-scrollbar { height: 5px; width: 2px; } -.smallScroll::-webkit-scrollbar-track { +*::-webkit-scrollbar-track { background: initial; margin: 21px 0; } -.smallScroll::-webkit-scrollbar-thumb { +*::-webkit-scrollbar-thumb { background-color: rgba(217, 217, 217, 0.49); border-radius: 5px; } @@ -474,11 +491,11 @@ button { } .my-node-enter { - opacity: 0; + opacity: 0 !important; } .my-node-enter-active { - opacity: 1; + opacity: 1 !important; transition: opacity 400ms; } @@ -486,25 +503,63 @@ button { opacity: 1; } +.my-node-exit-done { + opacity: 0; +} + .my-node-exit-active { opacity: 0; transition: opacity 400ms; } - -.fade-enter{ - opacity: 0; +.fade-enter { + opacity: 0; } -.fade-exit{ - opacity: 1; +.fade-exit { + opacity: 1; } -.fade-enter-active{ - opacity: 1; +.fade-enter-active { + opacity: 1; } -.fade-exit-active{ - opacity: 0; +.fade-exit-active { + opacity: 0; } .fade-enter-active, -.fade-exit-active{ - transition: opacity 500ms; -} \ No newline at end of file +.fade-exit-active { + transition: opacity 500ms; +} + +.transform-enter { + opacity: 0 !important; + transform: translateY(-40px); +} + +.transform-enter-active { + opacity: 1 !important; + transition: all 400ms; + transform: translateY(0px) !important; +} + +.transform-exit { + opacity: 1; +} + +.transform-exit-active { + opacity: 0; + transition: all 400ms; +} + +.tooltip-enter-active { + opacity: 1; + transform: translateX(-50%) scale(1); + transition: all 0.3s ease-in-out; +} + +.tooltip-exit { + opacity: 0; + transform: translateX(-50%) scale(0.9); + transition: all 0.3s ease-in-out; +} + +.tooltip-exit-active { +} @@ -0,0 +1,12 @@ + + + \ No newline at end of file @@ -0,0 +1,9 @@ + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + \ No newline at end of file @@ -0,0 +1,40 @@ + + + + + + \ No newline at end of file @@ -0,0 +1,14 @@ + + + + \ No newline at end of file @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file @@ -0,0 +1,6 @@ + + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -1,8 +1,8 @@ -import { IShortModel } from '#/entities/model-entity' +import { ShortModel } from '#/entities/model-entity' import { API_URL } from '#/shared/lib/constants' import axios from 'axios' -export async function getAudio(token?: string): Promise { +export async function getAudio(token?: string): Promise { try { const { data } = await axios.get(API_URL + '/ml_models/?category=audio', { headers: { @@ -0,0 +1,18 @@ +import { api } from '#/shared/api' +import { ChatDTO, CreateChatDTO } from '../types' + +export async function putChat(uid: string, data: Partial) { + return await api.put(`/chats/${uid}/`, data) +} + +export async function getChats(model: string) { + return await api.get('/chats', { params: { model } }) +} + +export async function removeChat(uid: string) { + return await api.delete(`/chats/${uid}/`) +} + +export async function postChat({ title, model }: CreateChatDTO) { + return await api.post('chats/', { title, model }, { params: { model } }) +} @@ -0,0 +1 @@ +export * from './chat.api' \ No newline at end of file @@ -0,0 +1,17 @@ +import { create } from 'zustand' +import { ChatDTO } from '../types' + +export interface ChatsStore { + chats: ChatDTO[] + setChats: (chats: ChatDTO[]) => void +} + +export const useChatsStore = create((set, get) => { + function setChats(chats: ChatDTO[]) { + set({ ...get(), chats }) + } + return { + chats: [], + setChats, + } +}) @@ -0,0 +1,2 @@ +export * from './chats.store' +export * from './use-chat-actions' \ No newline at end of file @@ -0,0 +1,49 @@ +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { postChat, putChat, removeChat } from '../api' +import { useChatsStore } from '.' +import { CreateChatDTO } from '../types' + +export function useChatActions() { + const { showMessage } = useShowDataStore() + + const { chats, setChats } = useChatsStore() + + const onRenameChat = makePrivateRequest(async (uid: string, title: string) => { + const chat = chats.find((x) => x.uid === uid) + + const { data, status } = await putChat(uid, { title }) + + if (status !== 200 || !chat) return showMessage('Ошибка при переименовании чата') + + chat.title = data.title + + setChats([...chats]) + }) + + const onRemoveChat = makePrivateRequest(async (uid: string) => { + const chat = chats.find((x) => x.uid === uid) + + const { status } = await removeChat(uid) + + if (status !== 204 || !chat) return showMessage('Ошибка при переименовании чата') + + setChats([...chats.filter((c) => c.uid !== uid)]) + }) + + const onCreateChat = makePrivateRequest(async (dto: CreateChatDTO) => { + const { status, data: chat } = await postChat(dto) + + if (status !== 200) return showMessage('Ошибка при переименовании чата') + + const chats = useChatsStore.getState().chats + + setChats([...chats, chat]) + }) + + return { + onRenameChat, + onRemoveChat, + onCreateChat, + } +} @@ -0,0 +1,11 @@ +export interface ChatDTO { + uid: string + title: string + created_at: string +} + + +export interface CreateChatDTO { + title: string + model: string +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './chat.dto' \ No newline at end of file @@ -0,0 +1,15 @@ +.popup { + min-width: 200px; + + &__title { + color: #a4aab5; + font-weight: 600; + font-size: 16px; + margin-bottom: 20px; + } +} + + +.wrap{ + padding: unset !important; +} \ No newline at end of file @@ -0,0 +1,19 @@ +import { getPopupById, PopupTemplate } from '#/shared/ui/popup' +import { POPUP_CHAT_BOT_PARAMS } from '#/shared/ui/popup' +import React from 'react' +import styles from './chat-bot-options-popup.module.scss' +import { ChatModelOptions } from '#/widgets/chat-model-options' + +export const ChatBotOptionsPopup = () => { + const popup = getPopupById(POPUP_CHAT_BOT_PARAMS) + + return ( + +
+
+ +
+
+
+ ) +} @@ -0,0 +1,50 @@ +.main { + display: flex; + width: max-content; + border: none; + justify-content: space-between; + background-color: transparent; + border-radius: 13px; + padding: 12px 15px; + text-transform: none; + align-items: center; + position: relative; + + &__settings{ + color: var(--air-color); + position: relative; + } + + &__text { + font-weight: 500; + font-size: 15px; + color: var(--new-ui-gray-color); + width: max-content; + + &_active { + font-weight: 500; + font-size: 15px; + color: var(--air-color); + } + } + + &_active { + display: flex; + align-items: flex-start; + justify-content: space-between; + width: 100%; + border: none; + border-radius: 13px; + padding: 12px 15px; + padding-bottom: 10px; + gap: 12px; + text-transform: none; + background-color: rgba(130, 128, 255, 0.15); + + } +} + + +.chat{ + +} \ No newline at end of file @@ -0,0 +1,72 @@ +import { ChatItemActionsPopup } from '#/features/chat-item-actions' +import { useCurrentChat } from '#/features/chats' +import { CommonInput } from '#/shared/ui/common-input' +import React, { useRef, useState } from 'react' +import { ChatDTO } from '../types' +import styles from './chat-button.module.scss' + +import RenameSvg from '#/assets/svg/rename.svg?react' +import ChatSettingsSvg from '#/assets/svg/chat-settings.svg?react' +import { getPopupById } from '#/shared/ui/popup' +import { c } from '#/shared' +import { useChatActions } from '../model' +import { ChatMobileRename } from '#/features/chat-mobile-rename/ui/chat-mobile-rename-popup' + +interface ChatButtonProps extends ChatDTO {} + +export const ChatButton = ({ uid, title }: ChatButtonProps) => { + const { setCurrentChat, currentChat: chat } = useCurrentChat() + + const { onRenameChat } = useChatActions() + + const input = useRef(null) + + const [rename, setRename] = useState(false) + + const popup = getPopupById(uid) + + return ( + + } + /> + ) : ( +
+

+ {title} +

+ + {uid === chat && ( + + )} + + +
+ )} + + ) +} @@ -0,0 +1 @@ +export * from './chat-button' \ No newline at end of file @@ -0,0 +1,4 @@ +export * from './types' +export * from './api' +export * from './ui' +export * from './model' \ No newline at end of file @@ -1 +1,2 @@ -export * from './message.routes' \ No newline at end of file +export * from './message-media.routes' +export * from './message-chat-bot.routes' \ No newline at end of file @@ -0,0 +1,22 @@ +import { Message, MessageSend } from '../types' +import { api } from '#/shared/api' + +export async function getMessagesBySlug(chat: string, offset?: number, limit = 10) { + return await api.get(`/chats/${chat}/messages`, { params: { limit, offset } }) +} + +export async function postMessage(uid: string, { file, content, info }: MessageSend) { + const data = new FormData() + + if (file) data.append('file', file) + + data.append('content', content) + data.append('info', JSON.stringify(info)) + + return await api.post(`/chats/${uid}/messages/`, data) +} + + +export async function removeMessage(chatUid: string, uid: string) { + return api.delete(`/chats/${chatUid}/messages/${uid}`) +} \ No newline at end of file @@ -3,8 +3,13 @@ import { IMessageRequest } from '#/shared/lib/types/types-gpt' import axios, { AxiosResponse } from 'axios' import { Message, MessageSend } from '../types' -export async function sendImage(model: string | null, dataForSend: MessageSend | FormData, token?: string) { - const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' +export async function sendImage( + model: string | null, + dataForSend: MessageSend | FormData, + token?: string +) { + const HeaderDataType = + dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' return await axios.post(API_URL + `/media/image/${model}`, dataForSend, { withCredentials: true, @@ -17,10 +22,13 @@ export async function sendImage(model: string | null, dataForSend: MessageSen } export async function getImagesBySlug(slug: string, token: string, offset?: number, limit = 10) { - return await axios.get(API_URL + `/media/image/${slug}?limit=${limit}&offset=${offset}`, { - validateStatus: (status) => status < 500, - headers: { - Authorization: `Bearer ${token}`, - }, - }) + return await axios.get( + API_URL + `/media/image/${slug}?limit=${limit}&offset=${offset}`, + { + validateStatus: (status) => status < 500, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) } @@ -0,0 +1,26 @@ + import { create } from 'zustand' + import { Message } from '../types' + + export interface ChatBotMessagesStore { + messages: Message[] + loading: boolean + setMessages: (messages: Message[]) => void + setLoading: (loading: boolean) => void + } + + export const useChatBotMessages = create((set, get) => { + function setMessages(messages: Message[]) { + set({ ...get(), messages }) + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + return { + messages: [], + loading: false, + setMessages, + setLoading, + } + }) @@ -0,0 +1,3 @@ +export * from './chat-bot-messages.store' +export * from './use-user-message-actions' +export * from './use-user-message-image' \ No newline at end of file @@ -0,0 +1,52 @@ +import { useState } from 'react' +import { Message, MessageSend } from '../types' +import { makePrivateRequest } from '#/shared/api' +import { postMessage } from '../api' +import { getBlobFromUrl } from '#/shared' +import { useAppSelector } from '#/app/store/store' +import { useChatBot } from '#/entities/model-entity' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useChatBotMessages } from './chat-bot-messages.store' +import { useCurrentChat } from '#/features/chats' + +export function useUserMessageActions(message: Message) { + const includeParams = useAppSelector((state) => state.params.params) + + const { showMessage } = useShowDataStore() + + const { currentChat } = useCurrentChat() + + const { setMessages, setLoading } = useChatBotMessages() + + const { inference } = useChatBot() + + const resend = makePrivateRequest(async () => { + if (!currentChat || !inference) return + + const { file, content } = message + + const request: MessageSend = { + content, + info: { ...includeParams, inference: inference.slug }, + } + + if (file) request.file = await getBlobFromUrl(file as string) + + setLoading(true) + + const { data, status } = await postMessage(currentChat, request) + + setLoading(false) + + if (status !== 201) + return showMessage(`Ошибка при повторной отправке сообщений, повторите позже`) + + const { messages } = useChatBotMessages.getState() + + setMessages([...messages.filter((m) => m.uid !== 'new-send'), data as Message]) + }) + + return { + resend, + } +} @@ -0,0 +1,27 @@ +import { useMemo, useState } from "react" +import { Message } from "../types" + +export function useUserMessageImage(message: Message) { + + const [loaded, setLoaded] = useState(false) + + + const fileIsImage = useMemo(() => { + if (!message.file) return false + return /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg|\.wav/.test(message.file as string) + }, []) + + const fileName = useMemo(() => { + if (!message.file) return 'Подбираем имя файла...' + const matches = (message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/) + if (!matches) return 'Подбираем имя файла...' + return `${matches[1]}.${matches[2]}` + }, []) + + return { + fileIsImage, + fileName, + loaded, + setLoaded + } +} @@ -1,7 +1,7 @@ -export interface MessageSend { +export interface MessageSend { content: string file?: File | null - info: T + info: Record } export interface Message { @@ -10,7 +10,7 @@ export interface Message { elapsed_time: string file: T from_model: boolean - info: null + info: Record | null is_favourite: boolean is_sent: boolean uid: string @@ -0,0 +1,84 @@ +.button { + background-color: transparent; + border: 0; + cursor: pointer; +} + +.message { + display: flex; + align-items: center; + margin: 8px 0px; + + &__avatar { + margin-right: 6px; + @media screen and (max-width: 1000px) { + display: none; + } + } +} + +.content { + width: fit-content; + max-width: 68%; + margin-right: auto; + + @media screen and (max-width: 1000px) { + max-width: 95%; + } + + &__header { + display: flex; + justify-content: space-between; + + p { + color: var(--new-ui-gray-text-color); + font-size: 13px; + font-weight: 600; + } + } + + &__title { + line-height: 17px; + margin-right: 8px; + } + &__date { + line-height: 20px; + margin-right: 32px; + } + + &__description { + display: flex; + align-items: flex-start; + } + + &__menu{ + margin-left: 8px; + position: relative; + } + + &__wrap { + overflow-y: scroll; + position: relative; + padding: 15px 23px; + border: 1px solid var(--new-ui-border-color); + color: var(--new-ui-message-text-color); + line-height: 23px; + font-size: 15px; + margin-top: 2px; + border-radius: 13px; + & > p { + color: inherit; + font-style: inherit; + } + + &_file{ + display: flex; + align-items: center; + gap: 12px; + color: var(--text-color-purple); + font-weight: 600; + line-height: 140%; + cursor: pointer; + } + } +} @@ -0,0 +1,78 @@ +import React, { useEffect, useMemo } from 'react' +import { Box, Menu, MenuItem, Slide, Typography } from '@mui/material' +import Stack from '@mui/material/Stack' +import Image from 'next/image' +import styles from './bot-message.module.scss' + +import { useAppSelector } from '#/app/store/store' +import { TooltipCustom } from '#/shared' +import { Markdown } from '#/widgets/markdown/markdown' +import { c, formatDate } from '#/shared/lib/helpers' +import { Model } from '#/entities/model-entity' +import { Message } from '../types' +import { useThemeAndDevice } from '#/shared/lib/hooks' +import AvatarSvg from '#/assets/svg/avatar.svg?react' +import DocSvg from '#/assets/svg/doc.svg?react' +import MenuSvg from '#/assets/svg/menu.svg?react' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { UserMessageActionsPopup } from '#/features/user-message-actions-popup' +import { getPopupById } from '#/shared/ui/popup' + +export interface BotMessageProps { + model: Model + message: Message +} + +export function BotMessage({ model, message: { file, ...message } }: BotMessageProps) { + const fileName = useMemo(() => (file ? (file as string).match(/(.+)\/(.+)\?/)![2] : ''), []) + + const content = useMemo(() => (file ? fileName : message.content), []) + + const popup = getPopupById(message.uid) + + return ( +
+
+ +
+
+
+

{model.title}

+

+ {formatDate(message.created_at, { + hour: 'numeric', + minute: 'numeric', + })} +

+
+
+
{ + if (!file) return + window.open(file as string, '_blank') + }} + > + {file && } + +
+
+ + + + +
+
+
+
+ ) +} @@ -0,0 +1 @@ +export * from './user-message' \ No newline at end of file @@ -0,0 +1,120 @@ +.button { + background-color: transparent; + border: 0; + cursor: pointer; +} + +.message { + width: fit-content; + max-width: 68%; + margin: 8px 0px; + margin-left: auto; + + @media screen and (max-width: 1000px) { + max-width: 100%; + } + + &__time { + align-items: center; + display: flex; + justify-content: end; + color: var(--new-ui-gray-color); + font-size: 13px; + font-weight: 600; + margin-left: 4px; + } +} + +.content { + display: flex; + flex-direction: column; + align-items: end; + gap: 8px; + + &__filebox { + display: flex; + align-items: center; + overflow-y: scroll; + position: relative; + padding: 15px 23px; + border: 1px solid var(--new-ui-bg-app-color); + border-radius: 9px; + font-size: 15px; + margin-top: 2px; + text-align: left; + justify-content: start; + gap: 12px; + font-weight: 600; + line-height: 140%; + letter-spacing: 0.2px; + color: var(--air-color); + + p { + color: var(--air-color); + } + } + + &__message{ + display: flex; + width: 100%; + align-items: flex-start; + justify-content: end; + gap: 4px; + } + + &__text { + overflow-y: scroll; + padding: 10px 15px; + background-color: #7F7DF3; + color: white; + border-radius: 13px; + line-height: 21px; + font-size: 15px; + font-weight: 400; + margin-top: 2px; + text-align: left; + } + + &__imagebox { + display: flex; + align-items: end; + flex-direction: column; + height: 60%; + width: 60%; + } + + &__image { + position: relative; + img { + object-fit: contain; + width: 100%; + height: 100%; + border-radius: 13px; + cursor: pointer; + min-width: 100px; + min-height: 100px; + } + } + + &__skeleton { + background-color: #eff0f2; + position: absolute; + top: 0; + left: 0; + z-index: 1; + border-radius: 13px; + width: 100%; + height: 100%; + } +} + +.menu { + display: flex; + margin-right: 4px; +} + +html[data-theme='dark'] { + .content__skeleton { + background-color: #2d2d2f; + } +} @@ -0,0 +1,104 @@ +import React, { memo, useMemo, useState } from 'react' +import Image from 'next/image' + +import { c, formatDate } from '#/shared/lib/helpers' +import styles from './user-message.module.scss' +import { CommonTooltip } from '#/shared/ui/tooltip' + +import MenuSvg from '#/assets/svg/menu.svg?react' +import DocSvg from '#/assets/svg/doc.svg?react' +import WarningSvg from '#/assets/svg/warning.svg?react' +import { UserMessageActionsPopup } from '#/features/user-message-actions-popup' +import { getPopupById } from '#/shared/ui/popup' +import { Model } from '#/entities/model-entity' +import { useUserMessageActions, useUserMessageImage } from '../model' +import { Message } from '../types' + +interface IMessagesList { + message: Message + model: Model + // setCurrentSrc: (value: string) => void + // setModal: (value: boolean) => void +} + +export const UserMessage = memo(({ message, model }: IMessagesList) => { + const { loaded, setLoaded, fileIsImage, fileName } = useUserMessageImage(message) + + const { resend } = useUserMessageActions(message) + + const popup = getPopupById(message.uid) + + return ( +
+

+ {formatDate(message.created_at, { + hour: 'numeric', + minute: 'numeric', + })} +

+ +
+ {message.file && + (fileIsImage ? ( +
+
+ setLoaded(true)} + width={500} + height={500} + src={(message.file as string).split('?type')[0]} + alt={'К сожалению, изображение не загрузилось'} + /> + {!loaded &&
} +
+
+ ) : ( + + ))} + +
+
+ + + + +
+ + {!message.is_sent && ( + + )} + +

30 ? 'pre-wrap' : 'pre', + }} + > + {message.content} +

+
+
+
+ ) +}) @@ -1,2 +1,4 @@ export * from './types' -export * from './api' \ No newline at end of file +export * from './api' +export * from './model' +export * from './ui' \ No newline at end of file @@ -1,11 +1,14 @@ -import { API_URL } from '#/shared/lib/constants' -import axios from 'axios' -import { IModel } from '../types' +import { Inference, Model, ShortModel } from '../types' +import { api } from '#/shared/api' -export async function getBotParams(slug: string, token?: string) { - return await axios.get(API_URL + `/ml_models/${slug}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) +export async function getBotParams(slug: string) { + return await api.get(`/api/ai/model/${slug}`) +} + +export async function getInference(id: string) { + return await api.get(`api/ai/inferences/${id}`) +} + +export async function getChatBots() { + return await api.get(`api/chats/models/`) } @@ -1,9 +1,9 @@ import { API_URL } from '#/shared/lib/constants' import axios from 'axios' -import { IShortModel } from '../types' +import { ShortModel } from '../types' export async function getModelsImages(token?: string) { - return await axios.get(API_URL + '/ml_models/?category=images', { + return await axios.get(API_URL + '/ml_models/?category=images', { headers: { Authorization: `Bearer ${token}`, }, @@ -1 +1,3 @@ -export * from './use-images-bots' \ No newline at end of file +export * from './use-images-bots' +export * from './use-chat-bot' +export * from './use-chat-bot-params.store' \ No newline at end of file @@ -0,0 +1,64 @@ +import { create } from 'zustand' +import { Inference, Model, InferenceParams, InferenceInput } from '../types' + +export interface ChatBotParamsStore { + botParams: Model | null + inferenceParams: InferenceParams[] + setInferenceParams: (params: InferenceParams[]) => void + infrerenceValues: Record + setInferenceInputs: (inputs: InferenceInput[]) => void + inferenceInputs: InferenceInput[] + setInferenceValues: (infrerenceValues: Record) => void, + setInferenceValueByKey: (key: string, value: any) => void + loading: boolean + setBotParams: (botParams: Model) => void + setLoading: (loading: boolean) => void + inference: Inference | null + setInference: (inference: Inference) => void +} + +export const useChatBotParams = create((set, get) => { + function setBotParams(botParams: Model) { + set({ ...get(), botParams }) + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + function setInference(inference: Inference) { + set({ ...get(), inference }) + } + + function setInferenceParams(inferenceParams: InferenceParams[]) { + set({ ...get(), inferenceParams }) + } + + function setInferenceInputs(inferenceInputs: InferenceInput[]) { + set({ ...get(), inferenceInputs }) + } + + function setInferenceValues(infrerenceValues: Record) { + set({ ...get(), infrerenceValues }) + } + + function setInferenceValueByKey(key: string, value: any) { + set({ ...get(), infrerenceValues: { ...get().infrerenceValues, [key]: value } }) + } + + return { + botParams: null, + loading: false, + inferenceInputs: [], + inferenceParams: [], + infrerenceValues: {}, + setInferenceParams, + setInferenceInputs, + setInferenceValues, + setInferenceValueByKey, + inference: null, + setInference, + setBotParams, + setLoading, + } +}) @@ -0,0 +1,76 @@ +import { useEffect } from 'react' +import { makePrivateRequest } from '#/shared/api' +import { getBotParams, getInference } from '../api' +import { useRouter } from 'next/router' +import { useChatBotParams } from '.' +import { Inference } from '../types' + +export function useChatBot() { + const { + botParams, + setBotParams, + inference, + setInference, + setInferenceParams, + setInferenceValues, + inferenceParams, + inferenceInputs, + setInferenceInputs, + } = useChatBotParams() + + const { query, push } = useRouter() + + useEffect(() => { + console.log(inferenceParams) + }, [inferenceParams]) + + const fetchBotParams = makePrivateRequest(async () => { + const { status, data } = await getBotParams(query.slug as string) + + if (status !== 200) return push('/404') + + setBotParams(data) + + onSetInferenceParams(data.inferences[0].slug) + }) + + const onSetInferenceParams = makePrivateRequest(async (inference: string | null) => { + const { botParams } = useChatBotParams.getState() + + if (!botParams) return + + if (!inference) inference = botParams.inferences[0].slug + + const result = botParams.inferences.find((i) => i.slug === inference) + + if (!result) return + + const { + data: { parameters, ...data }, + } = await getInference(result.id) + + // console.log(parameters) + + // console.log(data) + + setInferenceInputs(data.inputs) + + setInferenceParams(parameters) + + setInference({ ...data, parameters }) + + setInferenceValues( + parameters.reduce((p, { key: k, values: v }) => ({ ...p, [k]: v.default }), {}) + ) + }) + + return { + botParams, + fetchBotParams, + inference, + setInference, + onSetInferenceParams, + inferenceParams, + inferenceInputs, + } +} @@ -1,118 +0,0 @@ -import { useState } from 'react' -import { IModel } from '../types' -import { useSession } from 'next-auth/react' -import { getBotParams } from '../api' -import { useAppDispatch } from '#/app/store/store' -import { setParams } from '#/app/store/model-parametres-store' - -export function useImageBot(slug: string) { - const [botParams, setBotParams] = useState(null) - const [version, setVersion] = useState('') - const [modelType, setModelType] = useState('') - - const dispatch = useAppDispatch() - - const { data } = useSession() - - function setDefault(bot: IModel) { - setVersion('') - - dispatch(setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } - - // это пиз**ц - // нужен рефакторинг (я то в этом не разбираюсь) - // а стажеры и подавно)))) - function setStoreParams(bot: IModel) { - dispatch( - setParams( - bot.parameters.reduce( - (a, v) => - v.versions.includes(bot.versions[0].slug) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } - - function setAllParams(bot: IModel) { - setBotParams(bot) - setModelType(bot.slug) - - // store - if (bot.versions.length === 0) return setDefault(bot) - - setVersion(bot.versions[0].slug) - setStoreParams(bot) - } - - // это пиз**ц - const resetParams = () => { - if (botParams) { - dispatch(setParams({})) - if (botParams.versions.length !== 0) { - setVersion(botParams.versions[0].slug) - dispatch( - setParams( - botParams.parameters.reduce( - (a, v) => - v.versions.includes(botParams.versions[0].slug) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } else { - setVersion(botParams.slug) - dispatch( - setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) - ) - } - } - } - - // это пиз**ц - const setDefaultParams = () => { - if (!botParams) return - - dispatch(setParams({})) - - if (version === '') { - return dispatch( - setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) - ) - } - - dispatch( - setParams( - botParams.parameters.reduce( - (a, v) => (v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } - - // api functions - - async function fetchBotParams() { - if (!data) return - - const { data: bot, ...response } = await getBotParams(slug, data.access) - - if (response.status < 400) setAllParams(bot) - } - - return { - botParams, - version, - modelType, - fetchBotParams, - setVersion, - resetParams, - setDefaultParams, - } -} @@ -1,10 +1,10 @@ import { useState } from 'react' import { getModelsImages } from '../api' import { useSession } from 'next-auth/react' -import { IShortModel } from '../types' +import { ShortModel } from '../types' export function useImagesBots() { - const [bots, setBots] = useState([]) + const [bots, setBots] = useState([]) const { data } = useSession() @@ -16,9 +16,9 @@ export function useImagesBots() { if (response.status < 400) setBots(response.data) } - return { - bots, - fetchBots, - setBots - } + return { + bots, + fetchBots, + setBots, + } } @@ -1,60 +1,78 @@ type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' -export interface IShortModel { +export interface ShortModel { uid: string title: string description: string slug: string image: string - blocked: boolean - tags: IModelTag[] - actual_stat: { - generation_time: string - tokens_cost: string - } + enabled: boolean + tags: ModelTag[] + types: string[] } -export interface IModelTag { - title: string - icon: string - color: string +export interface ModelTag { + title: string + icon: string + color: string } -export interface IModel { +export interface Model { uid: string title: string - blocked: boolean + enabled: boolean description: string slug: string image: string - settings: { is_active: boolean } - parameters: IModelParams[] - versions: IModelVersions[] - inputs: IModelInputs[] - tags: IModelTag[] + inferences: ShortModelInference[] + tags: ModelTag[] + types: string[] } -export interface IModelParams { + +export type InferenceParamType = 'floatrange' | 'intrange' | 'bool' | 'choices' | 'int' | 'str' + +export interface InferenceParams { name: string description: string key: string - type: string + type: InferenceParamType required: boolean values: { - availables: string[] + availables: string[] | Array default: any - end: number + stop: number start: number step: number } versions: string[] } -export interface IModelVersions { + +export interface ShortModelInference { + id: string + name: string + slug: string + description: string +} + +export interface Inference { + id: string + tags: string[] + tracking_records: any[] + parameters: InferenceParams[] + inputs: InferenceInput[] name: string description: string - default: boolean slug: string + enabled: false } -export interface IModelInputs { + +// export interface ModelVersions { +// name: string +// description: string +// default: boolean +// slug: string +// } +export interface InferenceInput { // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' type: string required: boolean @@ -1,34 +1,48 @@ -import React, { useEffect, useMemo } from 'react' -import { Avatar, Box, Button, Typography } from '@mui/material' +import React, { useMemo } from 'react' import Image from 'next/image' import Link from 'next/link' import LockSvg from '#/assets/svg/lock.svg?react' import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card-chat.module.scss' -import { useThemeAndDevice } from '#/shared/lib/hooks' import { c } from '#/shared/lib/helpers' -import { IShortModel } from '#/entities/model-entity' +import { ShortModel } from '#/entities/model-entity' import { SvgIcon } from '#/shared/ui/svg' import { CommonButton } from '#/shared/ui/button' -import { uid } from 'chart.js/dist/helpers/helpers.core' -export interface ChatModelCardProps extends IShortModel { +export interface ChatModelCardProps extends ShortModel { accessed_models: string[] | null } -export function ChatModelCard({ description, image, title, slug, accessed_models, blocked, tags }: ChatModelCardProps) { +export function ChatModelCard({ + description, + image, + title, + slug, + accessed_models, + enabled, + tags, +}: ChatModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `chat-bot/${slug}` + return accessed_models && !accessed_models.includes(slug) + ? '/account?scope=subscribe' + : `chat-bot/${slug}` }, [accessed_models]) - const { theme } = useThemeAndDevice() - return ( -
+
- {title} + {title}
@@ -36,10 +50,12 @@ export function ChatModelCard({ description, image, title, slug, accessed_models

{description}

- {blocked && ( + {!enabled && (
- Модель недоступна + + Модель недоступна +
)} {accessed_models && !accessed_models.includes(slug) && ( @@ -59,7 +75,12 @@ export function ChatModelCard({ description, image, title, slug, accessed_models {tags && tags.map((tag, index) => (
- + {tag.title}
))} @@ -8,26 +8,44 @@ import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card.module.scss' import { useThemeAndDevice } from '#/shared/lib/hooks' import { c } from '#/shared/lib/helpers' -import { IShortModel } from '#/entities/model-entity' +import { ShortModel } from '#/entities/model-entity' import { SvgIcon } from '#/shared/ui/svg' import { CommonButton } from '#/shared/ui/button' -export interface ImageModelCardProps extends IShortModel { +export interface ImageModelCardProps extends ShortModel { accessed_models: string[] | null } -export function ImageModelCard({ description, image, title, slug, accessed_models, blocked, tags }: ImageModelCardProps) { +export function ImageModelCard({ + description, + image, + title, + slug, + accessed_models, + tags, +}: ImageModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `images/${slug}` + return accessed_models && !accessed_models.includes(slug) + ? '/account?scope=subscribe' + : `images/${slug}` }, [accessed_models]) const { theme } = useThemeAndDevice() return ( -
+ {/*
- {title} + {title}
@@ -38,7 +56,9 @@ export function ImageModelCard({ description, image, title, slug, accessed_model {blocked && (
- Модель недоступна + + Модель недоступна +
)} {accessed_models && !accessed_models.includes(slug) && ( @@ -58,12 +78,17 @@ export function ImageModelCard({ description, image, title, slug, accessed_model {tags && tags.map((tag, index) => (
- + {tag.title}
))}
-
+
*/} ) } @@ -0,0 +1,34 @@ +import { setParams } from '#/app/store/model-parametres-store' +import { useAppSelector, useAppDispatch } from '#/app/store/store' +import { InferenceParams } from '#/entities/model-entity' +import React, { useEffect } from 'react' + +interface BotParamsMap { + params: InferenceParams[] + currentVersion: string | null +} + +export const useBotParamsMap = ({ params, currentVersion }: BotParamsMap) => { + const includeParams = useAppSelector((state) => state.params.params) + const dispatch = useAppDispatch() + + const setNewParam = (payload: { [key: string]: string | number | number[] | boolean }) => { + dispatch(setParams(payload)) + } + + useEffect(() => { + if (currentVersion && params) { + const res = params.map((el) => + el.versions.length !== 0 && !el.versions.includes(currentVersion) ? null : el + ) + if (res.every((el) => el === null)) { + setNewParam({}) + } + } + }, [currentVersion, params]) + + return { + includeParams, + setNewParam, + } +} @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; +} @@ -0,0 +1,84 @@ +import React from 'react' +import styles from './bot-params-map.module.scss' + +import { SwitchFilter } from '#/features/switch-filter/' +import { InputFilter } from '#/features/input-filter/ui/input-filter' +import { useChatBotParams } from '#/entities/model-entity' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { RangeSlider } from '#/shared/ui/range-slider' +import { CommonSelect } from '#/shared/ui/common-select' + +interface BotParamsMap {} + +export default function BotParamsMap({}: BotParamsMap) { + const { infrerenceValues, setInferenceValueByKey, inferenceParams } = useChatBotParams() + + return ( +
+ {inferenceParams.map(({ key, type, name, description, values }) => { + if (type == 'floatrange' || type == 'intrange') { + return ( + + setInferenceValueByKey(key, value)} + max={values.stop} + min={values.start} + step={values.step} + /> + + ) + } + if (type == 'bool') { + return ( + setInferenceValueByKey(key, value)} + name={name} + description={description} + /> + ) + } + + if (type == 'choices') { + return ( + setInferenceValueByKey(key, value)} + withoutTooltip + items={values.availables.map((a) => { + return { + value: a[0], + label: a[1], + } + })} + /> + ) + } + + if (type == 'int' || type == 'str') { + return ( + setInferenceValueByKey(key, value)} + /> + ) + } + })} +
+ ) +} @@ -1,97 +0,0 @@ -import React from 'react' -import { Stack } from '@mui/material' - -import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' -import { InputFilter } from '#/app/components/filters/input_filter' -import { SelectFilter } from '#/app/components/filters/select_filter' -import { Slide } from '#/app/components/filters/slide_filter' -import { setParams } from '#/app/store/model-parametres-store' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { IModelParams } from '#/shared/api/models/models' - -interface IProps { - params: IModelParams[] - currentVersion: string -} -export default function BotParamsMap({ params, currentVersion }: IProps) { - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useAppDispatch() - - const setNewParam = (payload: { [key: string]: string | number | number[] | boolean }) => { - dispatch(setParams(payload)) - } - - React.useEffect(() => { - if (currentVersion && params) { - const res = params.map((el) => - el.versions.length !== 0 && !el.versions.includes(currentVersion) ? null : el - ) - if (res.every((el) => el === null)) { - setNewParam({}) - } - } - }, [currentVersion, params]) - - return ( - - {params.map((item, idx) => { - if (item.versions.length === 0 || item.versions.includes(currentVersion)) { - if (item.type == 'floatrange' || item.type == 'intrange') { - return ( - - ) - } - if (item.type == 'bool') { - return ( - - ) - } - - if (item.type == 'list') { - return ( - - ) - } - - if (item.type == 'int' || item.type == 'str') { - return ( - - ) - } - } - })} - - ) -} @@ -0,0 +1 @@ +export * from './use-chat-bot-input' \ No newline at end of file @@ -0,0 +1,38 @@ +import { useAppSelector } from '#/app/store/store' +import { MessageSend } from '#/entities/message' +import { Inference, useChatBotParams } from '#/entities/model-entity' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useState } from 'react' + +export function useChatBotInput( + inference: Inference | null, + sendMessage: (data: MessageSend) => Promise +) { + const { showMessage } = useShowDataStore() + const [file, setFile] = useState(null) + const { inferenceParams, infrerenceValues } = useChatBotParams() + + const onSendMessage = async (content: string, required: (string | null)[]) => { + if (required.includes('text') && content === '') return showMessage('Введите сообщение!') + + if (required.includes('image') && file === null) + return showMessage('Прикрепите изображение!') + + if (!inference) return showMessage('Не выбрана версия модели') + + await sendMessage({ + content, + file, + info: { + inference: inference.slug, + ...infrerenceValues, + }, + }) + } + + return { + onSendMessage, + file, + setFile, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-chat-bot-pagination' \ No newline at end of file @@ -0,0 +1,142 @@ +import { useAppSelector } from '#/app/store/store' +import { getImagesBySlug, Message, useChatBotMessages } from '#/entities/message' +import { Device } from '#/shared/lib/types/entities' +import { getImagesGalery } from '#/widgets/messages' +import { useMediaQuery } from '@mui/material' +import { useSession } from 'next-auth/react' +import { createRef, useEffect, useRef, useState } from 'react' +import { LimitSize, Limit } from '../types' +import { useRouter } from 'next/router' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useThemeAndDevice } from '#/shared/lib/hooks' +import { getMessagesBySlug } from '#/entities/message' +import { makePrivateRequest } from '#/shared/api' +import { useCurrentChat } from '#/features/chats' +import { CommonScrollbarRef } from '#/shared/ui/scrollbar' +// import { ScrollBarRef } from '#/shared/ui/scrollbar' + +export function useChatBotPagination() { + const { currentChat } = useCurrentChat() + + const refScroll = useRef(null) + + const scrollContainer = createRef() + + const [loaded, setLoaded] = useState(false) + + const offset = useRef(0) + + const observer = useRef(null) + + const [isHidden, setIsHidden] = useState(false) + + const { messages, setMessages, loading, setLoading } = useChatBotMessages() + + const { showMessage } = useShowDataStore() + + const { data } = useSession() + + // TODO: Подумать как написать лучше + useEffect(() => { + setMessages([]) + offset.current = 0 + setLoaded(false) + fetchMessages() + if (!scrollContainer.current) return + scrollContainer.current.scrollToBottom() + }, [currentChat]) + + const limits: Record = { + small: { + active: useMediaQuery('(max-height: 600px)'), + limit: 10, + firstLimit: 40, + }, + medium: { + active: useMediaQuery('(min-height: 600px) and (max-height: 900px)'), + limit: 30, + firstLimit: 50, + }, + large: { + active: useMediaQuery('(min-height: 900px)'), + limit: 45, + firstLimit: 70, + }, + } + + const fetchMessages = makePrivateRequest(async (count?: number) => { + if (!data) return + + setLoading(true) + + const { currentChat } = useCurrentChat.getState() + + if (!currentChat) return + + const { data: answer, ...response } = await getMessagesBySlug( + currentChat, + offset.current, + count || 10 + ) + + setLoading(false) + + setLoaded(true) + + if (response.status >= 400 || !Array.isArray(answer)) + return showMessage('Ошибка загрузки чата') + + const { messages } = useChatBotMessages.getState() + + setMessages([...answer.reverse(), ...messages]) + + offset.current += answer.length + }) + + const callback = async function (entries: IntersectionObserverEntry[]) { + if (!entries[0].isIntersecting) return + + if (isHidden) return setIsHidden(false) + + const active = Object.values(limits).find((item) => item.active) + + if (!active) return + + if (offset.current > 0) { + fetchMessages(active.limit) + } else { + fetchMessages(active?.firstLimit) + } + } + + function onObserverMounted() { + if (!refScroll.current || observer.current) return + + observer.current = new IntersectionObserver(callback) + + observer.current.observe(refScroll.current) + } + + function isHiddenHandler() { + if (document.visibilityState === 'hidden') return setIsHidden(true) + setIsHidden(false) + } + + useEffect(() => { + document.addEventListener('visibilitychange', isHiddenHandler) + return () => document.removeEventListener('visibilitychange', isHiddenHandler) + }, []) + + return { + onObserverMounted, + messages, + loading, + setLoading, + offset, + setMessages, + fetchMessages, + scrollContainer, + refScroll, + loaded, + } +} @@ -0,0 +1 @@ +export * from './limits' \ No newline at end of file @@ -0,0 +1,7 @@ +export type LimitSize = 'small' | 'medium' | 'large' + +export interface Limit { + active: boolean + limit: number + firstLimit: number +} \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './model' +export * from './types' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-chat-imagel-load' \ No newline at end of file @@ -0,0 +1,37 @@ +import { useState, useCallback } from 'react' + +export function useChatImageLoad( + setFile: React.Dispatch> +) { + + const [resendValue, setResendValue] = useState('') + + const onLoadImage = useCallback( + (event: React.ChangeEvent | null, file?: File) => { + if (event?.target.files) { + setFile(event.target.files[0]) + } + if (file) { + setFile(file) + } + }, + [] + ) + + const handleSetResetValue = useCallback( + (value: string) => { + if (resendValue === value) { + setResendValue((prev) => prev + ' ') + } else { + setResendValue(value) + } + }, + [resendValue] + ) + + return { + onLoadImage, + resendValue, + handleSetResetValue + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1,31 @@ +.container { + padding: 6px 0px !important; + color: var(--air-color); +} + +.popup { + &__button { + font-size: 15px; + display: flex; + font-weight: 500; + align-items: center; + gap: 5px; + width: max-content; + padding: 8px 14px; + transition: all 0.3s ease; + width: 100%; + + &:hover { + backdrop-filter: brightness(90%); + } + } +} + +.container { + padding: 6px 0px !important; + color: var(--air-color); + + @media screen and (max-width: 1000px) { + padding: 10px !important; + } +} \ No newline at end of file @@ -0,0 +1,51 @@ +import { PopupTemplate, getPopupById } from '#/shared/ui/popup' +import React, { Dispatch, SetStateAction, useMemo } from 'react' +import styles from './chat-item-actions-popup.module.scss' +import Rename2Svg from '#/assets/svg/rename-2.svg?react' +import TrashSvg from '#/assets/svg/trash.svg?react' +import { useChatActions } from '#/entities/chat' +import { useMediaQuery } from 'react-responsive' + +export interface ChatItemActionsPopupProps { + id: string + setRename: Dispatch> +} + +export const ChatItemActionsPopup = ({ id, setRename }: ChatItemActionsPopupProps) => { + const { onRemoveChat } = useChatActions() + + const isMobile = useMediaQuery({ query: '(max-width: 1000px)' }) + + const popup = getPopupById(id) + + const renamePopup = getPopupById(`rename-${id}`) + + return ( + +
+ + +
+
+ ) +} @@ -0,0 +1 @@ +export * from './chat-item-actions-popup' \ No newline at end of file @@ -0,0 +1,9 @@ +.popup{ + &__save{ + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + } +} \ No newline at end of file @@ -0,0 +1,53 @@ +import React, { useState } from 'react' + +import styles from './chat-mobile-rename-popup.module.scss' +import { PopupTemplate, getPopupById } from '#/shared/ui/popup' +import { CommonInput } from '#/shared/ui/common-input' +import RenameSvg from '#/assets/svg/rename.svg?react' +import { useChatActions } from '#/entities/chat' +import { title } from 'process' +import { CommonButton } from '#/shared/ui/button' + +interface ChatMobileRenameProps { + id: string +} + +export const ChatMobileRename = ({ id }: ChatMobileRenameProps) => { + const { onRenameChat } = useChatActions() + + const popup = getPopupById('rename-' + id) + + const [value, setValue] = useState('') + + return ( + +
+ setValue(e.target.value)} + /> + + { + onRenameChat(id, value) + popup.setState(false) + }} + > + + Сохранить + +
+
+ ) +} @@ -0,0 +1,2 @@ +export * from './use-chats' +export * from './use-current-chat' \ No newline at end of file @@ -0,0 +1,43 @@ +import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import axios from 'axios' +import { useSession } from 'next-auth/react' + +import { createChat, getAllChats } from '#/shared/api/endpoints' +import { API_URL } from '#/shared/lib/constants' +import { useCurrentChat } from '.' +import { useRouter } from 'next/router' +import { ChatDTO, getChats, postChat, useChatsStore } from '#/entities/chat' +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks' + +export function useChats() { + const { query } = useRouter() + + const model = useMemo(() => query.slug as string, [query.slug]) + + const { chats, setChats } = useChatsStore() + + const { showMessage } = useShowDataStore() + + const { currentChat: chat, setCurrentChat } = useCurrentChat() + + const fetchChats = makePrivateRequest(async () => { + const { data, status } = await getChats(model) + + if (status !== 200) return showMessage('Ошибка при получении чатов') + + setChats(data) + + if (!data.length) return + + setCurrentChat(data[0].uid) + }) + + return { + chat, + chats, + fetchChats, + createChat, + setCurrentChat, + } +} @@ -0,0 +1,17 @@ +import { create } from 'zustand' + +export interface CurrentChatStore { + currentChat: string | null + setCurrentChat: (currentChat: string | null) => void +} + +export const useCurrentChat = create((set, get) => { + function setCurrentChat(currentChat: string | null) { + set({ currentChat }) + } + + return { + currentChat: null, + setCurrentChat + } +}) @@ -1,2 +1 @@ -export type { Chat, ChatsReturn } from './use-chats' -export { useChats } from './use-chats' +export * from './model' \ No newline at end of file @@ -1,96 +0,0 @@ -import React, { Dispatch, SetStateAction, useEffect, useState } from 'react' -import axios from 'axios' -import { useSession } from 'next-auth/react' - -import { createChat, getAllChats } from '#/shared/api/endpoints' -import { API_URL } from '#/shared/lib/constants' - -export type Chat = { - uid: string - title: string - created_at: string -} - -export type ChatsReturn = { - currentChat: string | null - chats: Chat[] | null - chatSetting: HTMLButtonElement | null - isTryRename?: string | undefined - desktop?: boolean - setIsTryRename?: Dispatch> - setChatSetting: Dispatch> - removeChat: (uid: string) => void - createNewChat: () => void - handleClickChatSetting: (event: React.MouseEvent) => void - setChat: (value: string | null) => void -} - -export function useChats(model: string): ChatsReturn { - const [chats, setChats] = useState(null) - - const [currentChat, setCurrentChat] = useState(null) - - const [isTryRename, setIsTryRename] = useState(undefined) - - const [chatSetting, setChatSetting] = useState(null) - - const { data } = useSession() - - useEffect(() => { - if (model && data?.access) { - getAllChats(model, data?.access).then((res) => { - setChats(res) - if (res !== null && res.length > 0 && res[0] !== null) { - setCurrentChat(res[0].uid) - } - }) - } - }, [model, data?.access]) - - const createNewChat = async () => { - const chat = await createChat(model, data?.access) - - if (chat === null) { - return - } - - setChats((prev) => [...prev!, chat]) - setCurrentChat(chat.uid) - } - - const removeChat = async (uid: string) => { - try { - setChatSetting(null) - const { status } = await axios.delete(API_URL + `/chats/${uid}/`, { - headers: { Authorization: `Bearer ${data?.access}` }, - }) - - if (status === 204) { - let newChats = chats!.filter((el) => el.uid !== uid) - setChats(newChats) - if (newChats.length !== 0) setCurrentChat(newChats[0].uid) - } - } catch (e) {} - } - - const handleClickChatSetting = (event: React.MouseEvent) => { - setChatSetting(event.currentTarget) - } - - const setChat: any = (value: string | null) => { - setCurrentChat(value) - } - - return { - currentChat, - chats, - chatSetting, - setChatSetting, - removeChat, - createNewChat, - handleClickChatSetting, - isTryRename, - setIsTryRename, - setChat, - } -} @@ -21,7 +21,7 @@ export function useImageBotCreateImage( const dispatch = useAppDispatch() - const createImage = async (dataForSend: MessageSend) => { + const createImage = async (dataForSend: MessageSend) => { const { content, file } = dataForSend setIsComplete(false) @@ -51,15 +51,9 @@ export function useImageBotPagination(deviceType: Device) { setLoading(true) - const { data: answer, ...response } = await getImagesBySlug( - query.slug as string, - data.access, - offset.current, - count || 10 - ) + const { data: answer, ...response } = await getImagesBySlug(query.slug as string, data.access, offset.current, count || 10) - if (response.status >= 400 || !Array.isArray(answer)) - return showMessage('Ошибка загрузки чата') + if (response.status >= 400 || !Array.isArray(answer)) return showMessage('Ошибка загрузки чата') if (deviceType === 'desktop') { setMessages((prev) => [...prev, ...answer]) @@ -113,8 +107,7 @@ export function useImageBotPagination(deviceType: Device) { } function onObserverMounted() { - const currentObserver = - deviceType === 'desktop' ? refScrollDesktop.current : refScrollMobile.current + const currentObserver = deviceType === 'desktop' ? refScrollDesktop.current : refScrollMobile.current if (!currentObserver) return @@ -2,11 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' import { ImageWithState } from './types' import { Message } from '#/entities/message' -export const useImagesLibrary = ( - setModal: Dispatch>, - current: string | null, - images: Message[] -) => { +export const useImagesLibrary = (setModal: Dispatch>, current: string | null, images: Message[]) => { const [initialCount, setInitialCount] = useState(0) const esc = (e: KeyboardEvent) => { @@ -16,10 +12,7 @@ export const useImagesLibrary = ( } } - const currentIndex = useMemo( - () => images.findIndex((item) => item.file === current), - [current] - ) + const currentIndex = useMemo(() => images.findIndex((item) => item.file === current), [current]) useEffect(() => { window.addEventListener('keydown', esc) @@ -29,6 +22,6 @@ export const useImagesLibrary = ( return { initialCount, setInitialCount, - currentIndex, + currentIndex, } } @@ -160,9 +160,6 @@ export default function FullScreenModal({ slidesPerView={'auto'} spaceBetween={0} className={styles.swiper} - onActiveIndexChange={() => { - console.log(swiper?.activeIndex) - }} simulateTouch={false} initialSlide={currentIndex} onSwiper={(swiper) => setSwiper(swiper)} @@ -0,0 +1 @@ +export * from './input-filter' \ No newline at end of file @@ -0,0 +1,6 @@ +.title { + margin-bottom: 16px; + font-size: 15px; + color: var(--new-ui-gray-color); + font-weight: 400; +} \ No newline at end of file @@ -0,0 +1,44 @@ +import React from 'react' +import { CommonTextArea } from '#/shared/ui/common-textarea' +import { CommonTooltip } from '#/shared/ui/tooltip' +import styles from './input-filter.module.scss' + +interface InputFilterProps { + title: string + value: string + description: string + setValue: (value: string) => void + withoutTooltip?: boolean +} + +export const InputFilter = ({ + value, + setValue, + title, + description, + withoutTooltip = false, +}: InputFilterProps) => { + return ( +
+ +

{title}

+ { + e.target.style.height = 'auto' + e.target.style.height = Math.min(e.target.scrollHeight, 177) + 'px' + + setValue(e.target.value) + }} + /> +
+
+ ) +} @@ -0,0 +1 @@ +export * from './ui' @@ -1,3 +1,4 @@ export const PLATE_CHANGE_PASSWORD = 'plate-change-password' export const RESEND_INVATION_PASSWORD = 'resend-invation-password' export const ERROR_REPORT = 'error-report' + @@ -0,0 +1 @@ +export * from './use-model-input' \ No newline at end of file @@ -0,0 +1,34 @@ +import { FormEventHandler, KeyboardEventHandler, useState } from 'react' + +export function useModelInput( + sendMessage: (message: string) => void, + setFile: (file: File | null) => void +) { + const [value, setValue] = useState('') + + const onInputHeightCorrect: FormEventHandler = ({ target }) => { + ;(target as HTMLTextAreaElement).style.height = + (target as HTMLTextAreaElement).scrollHeight + 'px' + } + + function onSendMessage() { + sendMessage(value) + setValue('') + setFile(null) + } + + const keyDownSend: KeyboardEventHandler = (e) => { + if (e.shiftKey || e.key !== 'Enter') return + e.preventDefault() + + onSendMessage() + } + + return { + onInputHeightCorrect, + keyDownSend, + value, + setValue, + onSendMessage, + } +} @@ -5,6 +5,13 @@ width: 100%; border-radius: 15px; gap: 10px; + background-color: var(--new-ui-main-color); + &__settings { + display: none; + @media screen and (max-width: 1000px) { + display: block; + } + } } :root[data-theme='light'] { @@ -34,7 +41,7 @@ -ms-overflow-style: none; padding: 0px; font-size: 16px; - max-height: 82px; + max-height: 62px; &::-webkit-scrollbar { display: none; /* Chrome, Brave, Edge */ @@ -1,167 +1,74 @@ -import React, { FC, SyntheticEvent, useEffect } from 'react' -import { Divider, InputAdornment, TextField } from '@mui/material' -import Stack from '@mui/material/Stack' -import { TextFieldProps } from '@mui/material/TextField/TextField' -import Image from 'next/image' +import React, { useEffect } from 'react' import { LoadImage } from '#/app/components/input_components/load_image' import { SendBtn } from '#/app/components/input_components/send_button' -import { useAppSelector } from '#/app/store/store' -import { IModelInputs } from '#/shared/api/models/models' -import { styleInputWithoutBorderFocus } from '#/shared/ui/input' -import { useThemeAndDevice } from '#/shared/lib/hooks' +import { ModelInputs } from '#/shared/api/models/models' import classes from './model-input.module.scss' +import { useChatBotMessages } from '#/entities/message' +import InputSettingsSvg from '#/assets/svg/input-setting.svg?react' +import { CommonTextArea } from '#/shared/ui/common-textarea' +import { useModelInput } from '../model' +import { CommonSendButton } from '#/shared/ui/common-send-button' +import { CommonLoadFile } from '#/shared/common-load-file' -interface Input { - loading: boolean - // requestImage: () => void - wonderMe?: () => void - desktop: boolean - image?: File | null - count?: number - quality?: string - unpinImage?: () => void - imageLoad?: React.ChangeEventHandler - // openFilters: (event: React.MouseEvent) => void - sendMessage: (message: string, required: (string | null)[]) => boolean - // setInput: React.Dispatch> - // setImage: React.Dispatch> - styles: 'images' | 'chats' | 'audio' - input_types?: IModelInputs[] +export interface ModelInputProps { + file: File | null + setFile: (file: File | null) => void + sendMessage: (message: string) => void + inputTypes: string[] blocked?: boolean viewMobileSettings: () => void - currentVersion: string - resendValue?: string } -export const ModelInput: FC = ({ - image, - loading, - unpinImage, +export const ModelInput = ({ + file, sendMessage, - desktop, - imageLoad, - styles, - input_types, + setFile, + inputTypes, viewMobileSettings, - currentVersion, - resendValue, - blocked, -}: Input) => { - const theme = useAppSelector((state) => state.theme.theme) - const [disabled, setDisabled] = React.useState(true) - const [required, setRequired] = React.useState<(string | null)[]>([]) - const [types, setTypes] = React.useState([]) - const [typeVersions, setTypeVersions] = React.useState({}) - const [value, setValue] = React.useState('') + blocked = false, +}: ModelInputProps) => { + const { loading } = useChatBotMessages() - useEffect(() => { - if (input_types) { - setRequired(input_types.map((el) => (el.required ? el.type : null))) - setTypes(input_types.map((el) => el.type)) - setTypeVersions(input_types.reduce((a, v) => ({ ...a, [v.type]: v.versions }), {})) - } - }, [input_types]) - - useEffect(() => { - if (input_types && typeVersions && !typeVersions['text']) { - setDisabled(true) - } else if ( - typeVersions['text'] && - (typeVersions['text'].length === 0 || typeVersions['text'].includes(currentVersion)) - ) { - setDisabled(false) - } - }, [typeVersions]) - - useEffect(() => { - if (resendValue) { - setValue(resendValue) - } - }, [resendValue]) + const { onInputHeightCorrect, keyDownSend, value, setValue, onSendMessage } = useModelInput( + sendMessage, + setFile + ) return ( -
- {!desktop && ( - - - - )} +
+ + onInput={onInputHeightCorrect} + onKeyDown={keyDownSend} + rows={1} + placeholder={ + blocked ? 'Ввод текста недоступен для этой модели' : 'Ваше сообщение' + } + className={classes.area} + disabled={blocked} + />
- {typeVersions['image'] && - (typeVersions['image'].length === 0 || - typeVersions['image'].includes(currentVersion)) && ( - <> - {!blocked && ( - - )} -
- - )} - {!disabled && !blocked && ( - setFile(null)} + setFile={(file) => setFile(file)} + types={inputTypes} /> )} +
+ + {!blocked && ( + onSendMessage()} /> + )}
) @@ -0,0 +1 @@ +export * from './model-params-select' @@ -0,0 +1,70 @@ +.container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.select { + position: relative; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 16px 8px 16px 14px; + cursor: pointer; + + border-radius: 13px; + border: 2px solid #40404e; + &_open { + border: 2px solid #7f7df3; + } + + &__text { + font-size: 18px; + color: var(--new-ui-text-color); + line-height: 1.5; + } +} + +.icon { + color: #a6a5a5; +} + +html[data-theme='light'] { + .select { + border: 1px solid #e9e9e9; + &_open { + border: 1px solid #7f7df3; + } + } +} + +.dropdown { + position: absolute; + top: 60px; + left: 0; + right: 0; + z-index: 1000; + background-color: var(--new-ui-main-color); + width: 100%; + border-radius: 13px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, + rgba(0, 0, 0, 0.12) 0px 3px 14px 2px; + + &__option { + list-style: none; + padding: 10px 16px; + + &_active { + background-color: rgba(25, 118, 210, 0.12); + } + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + &:active { + background-color: rgba(0, 0, 0, 0.2); + } + } + &__name { + font-size: 18px; + } +} @@ -0,0 +1,28 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useChatBot } from '#/entities/model-entity' +import { CommonSelect, SelectItem } from '#/shared/ui/common-select' + +interface ModelParamsSelectProps {} + +export const ModelParamsSelect = ({ ...props }: ModelParamsSelectProps) => { + const { botParams, inference, onSetInferenceParams } = useChatBot() + + const selectItems = useMemo(() => { + if (!botParams) return [] + return botParams.inferences.map(({ description, slug, name }) => ({ + value: slug, + label: name, + description, + })) + }, [botParams]) + + const value = useMemo(() => (inference ? inference.slug : undefined), [inference]) + + return ( + onSetInferenceParams(slug)} + items={selectItems} + /> + ) +} @@ -0,0 +1 @@ +export * from './ui' @@ -0,0 +1 @@ +export * from './reset-bot-filters' @@ -0,0 +1,30 @@ +.container { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + &__text { + line-height: 19.6px; + font-size: 14px; + font-weight: 400; + display: none; + cursor: pointer; + + @media screen and (max-width: 1000px) { + display: block; + } + + &_reset { + color: #ff4170; + } + &_close { + color: #808283; + } + } + + @media screen and (max-width: 1000px) { + margin-top: 24px; + } +} @@ -0,0 +1,31 @@ +import React from 'react' +import styles from './reset-bot-filters.module.scss' +import { c } from '#/shared' +import { POPUP_CHAT_BOT_PARAMS, getPopupById } from '#/shared/ui/popup' +import { useChatBot } from '#/entities/model-entity' + +interface ResetBotFilters {} + +export const ResetBotFilters = ({}: ResetBotFilters) => { + const popup = getPopupById(POPUP_CHAT_BOT_PARAMS) + + const { onSetInferenceParams, inference } = useChatBot() + + return ( +
+

onSetInferenceParams(inference?.slug ?? null)} + > + Сбросить настройки +

+ +

popup.setState(false)} + > + Закрыть +

+
+ ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './switch-filter' \ No newline at end of file @@ -0,0 +1,52 @@ +.wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.switch { + display: none; + + &__label { + color: #a4aab5; + font-size: 15px; + line-height: 19.6px; + font-weight: 400; + } + & { + display: none; + + &:checked + .switch__slider { + background-color: #7f7df280; + + &::before { + transform: translateX(21px); + background-color: #7f7df3; + } + } + } + + &__slider { + position: relative; + width: 34px; + height: 13px; + background-color: #40404f61; + border-radius: 20px; + cursor: pointer; + transition: background-color 0.25s ease; + + &::before { + content: ''; + position: absolute; + top: -3.5px; + left: -3.5px; + width: 20px; + height: 20px; + background-color: #fff; + border-radius: 50%; + transition: transform 0.25s ease; + box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + } +} @@ -0,0 +1,41 @@ +import React from 'react' +import { CommonTooltip } from '#/shared/ui/tooltip' +import styles from './switch-filter.module.scss' + +interface SwitchFilterProps { + name: string + value: boolean + setValue: (value: boolean) => void + description: string + withoutTooltip?: boolean +} + +export const SwitchFilter = ({ + name, + value, + description, + setValue, + withoutTooltip = false, +}: SwitchFilterProps) => { + return ( + + + + ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,40 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import Link from 'next/link' - -import styles from './title.module.scss' - -export interface TitleProps { - type: string - title: string - linkBack?: string - rightSlot?: React.ReactNode -} - -export default function Title(props: TitleProps) { - - return ( - - - - {' '} - {props.type} •{' '} - - -  {props.title} - - -
- - {props.title} - -
- {props.rightSlot} -
-
-
- ) -} @@ -0,0 +1 @@ +export * from './use-chat-message-actions' \ No newline at end of file @@ -0,0 +1,112 @@ +import { useAppDispatch } from '#/app/store/store' +import { getUserBalance } from '#/entities/balance' +import { + Message, + MessageSend, + postMessage, + removeMessage, + useChatBotMessages, +} from '#/entities/message' +import { useChatBotParams } from '#/entities/model-entity' +import { useCurrentChat } from '#/features/chats' +import { makePrivateRequest } from '#/shared/api' +import { useEventSource } from '#/shared/lib/event-source' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { cloneDeep } from 'lodash' +import { useSession } from 'next-auth/react' + +export function useChatMessageActions() { + const dispatch = useAppDispatch() + + const { messages, setLoading, setMessages, loading } = useChatBotMessages() + + const { botParams } = useChatBotParams() + + const { currentChat } = useCurrentChat() + + const { showMessage } = useShowDataStore() + + const { data: session } = useSession() + + const { event } = useEventSource() + + function getFileLink(file: File) { + return URL.createObjectURL(file) + '?type=.' + file.name.split('.')[1] + } + + function getOptimisticMessage(data: MessageSend): Message { + const date = new Date() + + const file = data.file ? getFileLink(data.file) : null + + return { + ...data, + file, + is_sent: true, + model: '', + from_model: false, + uid: 'new-send', + elapsed_time: '', + is_favourite: false, + created_at: date.toISOString(), + } + } + + const sendMessage = makePrivateRequest(async (data: MessageSend) => { + if (loading || !botParams || !currentChat) return + + const userMessage = getOptimisticMessage(data) + + const date = new Date() + + const modelMessage: Message = { + ...userMessage, + content: `Ваш вопрос получен. Ожидание ответа от ${botParams.slug}...`, + from_model: true, + file: null, + uid: 'new-bot-send', + created_at: date.toISOString(), + } + + setLoading(true) + + setMessages([...messages, userMessage, modelMessage]) + + const r = useChatBotMessages.getState().messages + + const initialMessages = r.filter((m) => m.uid !== 'new-bot-send') + + const { data: result, status } = await postMessage(currentChat, data) + + setLoading(false) + + if (status >= 400) { + userMessage.is_sent = false + + setMessages([...initialMessages]) + + const error = result as { detail: string } + + if (error.detail) return showMessage(error.detail) + + return showMessage('Непредвиденная ошибка, попробуйте еще раз') + } + + setMessages([...initialMessages, result as Message]) + + dispatch(getUserBalance(session?.access)) + }) + + async function deleteMessage(uid: string) { + if (!currentChat) return + + const { status } = await removeMessage(currentChat, uid) + + if (status) setMessages(messages?.filter((el) => el.uid !== uid)) + } + + return { + sendMessage, + deleteMessage, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './user-message-popup' \ No newline at end of file @@ -0,0 +1,22 @@ +.container { + padding: 6px 0px !important; + color: var(--air-color); +} + +.popup { + &__button { + font-size: 15px; + display: flex; + font-weight: 500; + align-items: center; + gap: 5px; + width: max-content; + padding: 8px 14px; + transition: all 0.3s ease; + width: 100%; + + &:hover { + backdrop-filter: brightness(90%); + } + } +} @@ -0,0 +1,39 @@ +import { PopupTemplate, getPopupById } from '#/shared/ui/popup' +import React from 'react' +import CopySvg from '#/assets/svg/copy.svg?react' +import TrashSvg from '#/assets/svg/trash.svg?react' + +import styles from './user-message-popup.module.scss' +import { useChatBotMessages, useUserMessageActions } from '../../../entities/message/model' +import { useChatMessageActions } from '#/features/use-chat-message-actions' + +interface UserMessageActionsPopupProps { + id: string +} + +export const UserMessageActionsPopup = ({ id }: UserMessageActionsPopupProps) => { + const { deleteMessage } = useChatMessageActions() + + const popup = getPopupById(id) + + const content = popup.getStoreProperty('content')! + + return ( + +
+ + +
+ ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,6 +1,6 @@ -import { ChatBotsPage } from "#/views/chat-bot"; -import { getDefaultLayout } from "#/widgets/layouts"; +import { ChatBotsPage } from '#/views/chat-bot' +import { getDefaultLayout } from '#/widgets/layouts' -ChatBotsPage.getLayout = getDefaultLayout({ titlePage: 'Чат-боты' }); +ChatBotsPage.getLayout = getDefaultLayout({ titlePage: 'Чат-боты' }) export default ChatBotsPage @@ -18,9 +18,8 @@ import { Message } from '#/shared/lib/types/model' import { IDalleProps } from '#/shared/lib/types/types-dalle' import { SelectUI } from '#/shared/ui/select' -import Title from '../../features/title/title' - import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' +import { CommonTitle } from '#/shared/ui/title' const toolbarOptions = { options: ['inline', 'blockType', 'list', 'textAlign', 'history'], @@ -156,7 +155,7 @@ const Create: React.FC = ({ device, token, favorites }) => { return ( - ) => { -// let url = params[0] - -// const baseUrl = process.env.NEXT_PUBLIC_NEXTAUTH_URL! - -// if (typeof url === 'string' && !(url as string).startsWith(baseUrl) && !(url as string).includes('http')) { -// url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) + params[0] : baseUrl + params[0] -// console.log(url) -// } - -// return fetch(url, params[1]) -// } - axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, }) @@ -1,3 +1,4 @@ +import { PopupBackdrop } from '#/shared/ui/popup' import { Head, Html, Main, NextScript } from 'next/document' export default function Document() { @@ -35,6 +36,7 @@ export default function Document() { </div> </noscript> <Main /> + <div id='popup-container'></div> <div id='modal-container'></div> <NextScript /> </body> @@ -1,25 +1,28 @@ import axios from 'axios' import { API_URL } from '#/shared/lib/constants' -import { IShortModel, IModel } from '#/entities/model-entity' +import { ShortModel, Model } from '#/entities/model-entity' const model_api = { - async getBots(token?: string): Promise<IShortModel[]> { + async getBots(token?: string): Promise<ShortModel[]> { try { - const { data } = await axios.get<IShortModel[]>(API_URL + '/ml_models/?category=chat-bots', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.get<ShortModel[]>( + API_URL + '/ml_models/?category=chat-bots', + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { return err } }, - async getBotParams(slug: string, token?: string): Promise<IModel> { + async getBotParams(slug: string, token?: string): Promise<Model> { try { - const { data } = await axios.get<IModel>(API_URL + `/ml_models/${slug}`, { + const { data } = await axios.get<Model>(API_URL + `/ml_models/${slug}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -30,7 +33,7 @@ const model_api = { } }, - async getImages(token?: string): Promise<IShortModel[]> { + async getImages(token?: string): Promise<ShortModel[]> { try { const { data } = await axios.get(API_URL + '/ml_models/?category=images', { headers: { @@ -1,3 +1,4 @@ +// @ts-nocheck import React, { useCallback, useEffect, useRef, useState } from 'react' import axios, { AxiosError, AxiosResponse } from 'axios' import base64 from 'base64-encode-file' @@ -12,7 +13,7 @@ import { IMessageRequest } from '#/shared/lib/types/types-gpt' import { Message, MessageSend } from '#/entities/message' import { Variant } from '#/shared/lib/hooks/use-show-data' -const formDataHelper = (file: File, dataForSend: MessageSend<any>): FormData => { +const formDataHelper = (file: File, dataForSend: MessageSend): FormData => { const FD = new FormData() FD.append('file', file) FD.append('info', JSON.stringify(dataForSend.info)) @@ -24,11 +25,14 @@ const formDataHelper = (file: File, dataForSend: MessageSend<any>): FormData => export const ModelsWithChatsEndpoints = { getData: async (chatUid: string, offset: number, token?: string) => { try { - const { data } = await axios.get<Message[]>(API_URL + `/chats/${chatUid}/messages/?limit=10&offset=${offset}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.get<Message[]>( + API_URL + `/chats/${chatUid}/messages/?limit=10&offset=${offset}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { return { @@ -38,8 +42,13 @@ export const ModelsWithChatsEndpoints = { } } }, - sendData: async <T>(chatUid: string | null, dataForSend: MessageSend<T> | FormData, token?: string) => { - const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' + sendData: async <T>( + chatUid: string | null, + dataForSend: MessageSend | FormData, + token?: string + ) => { + const HeaderDataType = + dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' return await axios.post<IMessageRequest, AxiosResponse<Message[] | { detail: string }>>( API_URL + `/chats/${chatUid}/messages/`, @@ -58,8 +67,7 @@ export const ModelsWithChatsEndpoints = { export function useModel( currentChat: string | null, showMessage: (message: string, variant?: Variant) => void, - modelType: string, - clearInput?: () => void + modelType: string ) { const { data } = useSession() const [messages, setMessages] = useState<Message[] | null>(null) @@ -72,7 +80,11 @@ export function useModel( setMessages([]) ;(async () => { setLoading(true) - const answer = await ModelsWithChatsEndpoints.getData(currentChat, 0, data?.access) + const answer = await ModelsWithChatsEndpoints.getData( + currentChat, + 0, + data?.access + ) setLoading(false) if (Array.isArray(answer)) { @@ -89,7 +101,11 @@ export function useModel( const getMessagesPagination = async () => { if (currentChat) { setLoading(true) - const answer = await ModelsWithChatsEndpoints.getData(currentChat, offset, data?.access) + const answer = await ModelsWithChatsEndpoints.getData( + currentChat, + offset, + data?.access + ) setLoading(false) if (Array.isArray(answer) && messages != null) { @@ -103,7 +119,7 @@ export function useModel( } } - const sendMessage = async <T>(dataForSend: MessageSend<T>) => { + const sendMessage = async <T>(dataForSend: MessageSend) => { if (loading) { return } @@ -115,7 +131,11 @@ export function useModel( info: dataForSend.info as any, is_sent: true, model: '', - file: dataForSend.file ? ((URL.createObjectURL(dataForSend.file) + '?type=.' + dataForSend.file.name.split('.')[1]) as string) : null, + file: dataForSend.file + ? ((URL.createObjectURL(dataForSend.file) + + '?type=.' + + dataForSend.file.name.split('.')[1]) as string) + : null, from_model: false, uid: 'new-send', elapsed_time: '', @@ -151,7 +171,11 @@ export function useModel( formData = FD } - const { data: result, status } = await ModelsWithChatsEndpoints.sendData(currentChat, formData ?? dataForSend, data?.access) + const { data: result, status } = await ModelsWithChatsEndpoints.sendData( + currentChat, + formData ?? dataForSend, + data?.access + ) setMessages((prev) => prev!.slice(0, -2)) @@ -173,11 +197,14 @@ export function useModel( } const deleteMessage = (message_uid: string) => { - axios.delete(process.env.NEXT_PUBLIC_API_HOST + `/chats/${currentChat}/messages/${message_uid}`, { - headers: { - Authorization: `Bearer ${data?.access}`, - }, - }).then(() => { + axios.delete( + process.env.NEXT_PUBLIC_API_HOST + `/chats/${currentChat}/messages/${message_uid}`, + { + headers: { + Authorization: `Bearer ${data?.access}`, + }, + } + ).then(() => { if (messages) setMessages(messages?.filter((el) => el.uid !== message_uid)) }) } @@ -188,11 +215,14 @@ export function useModel( export const ModelsWithImagesEndpoints = { getData: async (type: string, offset: number, token?: string) => { try { - const { data } = await axios.get<Message[]>(API_URL + `/media/image/${type}?limit=10&offset=${offset}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.get<Message[]>( + API_URL + `/media/image/${type}?limit=10&offset=${offset}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { return { @@ -202,17 +232,26 @@ export const ModelsWithImagesEndpoints = { } } }, - sendData: async <T>(type: string | null, dataForSend: MessageSend<T> | FormData, token?: string) => { - const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' + sendData: async <T>( + type: string | null, + dataForSend: MessageSend | FormData, + token?: string + ) => { + const HeaderDataType = + dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' try { - const { data } = await axios.post<IMessageRequest, AxiosResponse<Message[]>>(API_URL + `/media/image/${type}`, dataForSend, { - withCredentials: true, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': HeaderDataType, - }, - }) + const { data } = await axios.post<IMessageRequest, AxiosResponse<Message[]>>( + API_URL + `/media/image/${type}`, + dataForSend, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': HeaderDataType, + }, + } + ) return data } catch (err: any) { @@ -225,7 +264,11 @@ export const ModelsWithImagesEndpoints = { }, } -export function useModelImages<T>(showMessage: (message: string) => void, type: string, device: Device) { +export function useModelImages<T>( + showMessage: (message: string) => void, + type: string, + device: Device +) { const { data } = useSession() const { getData, sendData } = ModelsWithImagesEndpoints @@ -289,20 +332,35 @@ export function useModelImages<T>(showMessage: (message: string) => void, type: // console.log(offsetRef.current); // console.log(messagesRef.current); - if (dataRef.current?.access && typeRef.current !== '' && typeRef.current !== undefined) { + if ( + dataRef.current?.access && + typeRef.current !== '' && + typeRef.current !== undefined + ) { setLoading(true) - const answer = await getData(typeRef.current, offsetRef.current, dataRef.current?.access) + const answer = await getData( + typeRef.current, + offsetRef.current, + dataRef.current?.access + ) setLoading(false) - if (Array.isArray(answer) && messagesRef.current != null && device === 'mobile') { + if ( + Array.isArray(answer) && + messagesRef.current != null && + device === 'mobile' + ) { const newMessages = answer.reverse() setMessages([...newMessages, ...messagesRef.current]) setOffset((prev) => prev + answer.length) - } else if (Array.isArray(answer) && messagesRef.current != null && device === 'desktop') { + } else if ( + Array.isArray(answer) && + messagesRef.current != null && + device === 'desktop' + ) { setMessages([...messagesRef.current, ...answer]) setOffset((prev) => prev + answer.length) } else { - console.log('err') showMessage('Ошибка загрузки сообщений') return @@ -312,7 +370,7 @@ export function useModelImages<T>(showMessage: (message: string) => void, type: [data?.access, type, offset, messages] ) - const createImage = async <T>(dataForSend: MessageSend<T>) => { + const createImage = async <T>(dataForSend: MessageSend) => { const { content, file } = dataForSend setIsComplete(false) @@ -325,8 +383,10 @@ export function useModelImages<T>(showMessage: (message: string) => void, type: setLoading(false) if (result.hasOwnProperty('error')) { - //@ts-ignore - const message = (result.details as AxiosError).response.data.trim() ?? 'Ошибка отправки сообщения' + + const message = + (result.details as AxiosError).response.data.trim() ?? + 'Ошибка отправки сообщения' showMessage(message) return } @@ -367,14 +427,22 @@ export const ModelsMediaApi = { } } }, - sendData: async <T>(type: string | null, dataForSend: MessageSend<T> | FormData, token?: string) => { + sendData: async <T>( + type: string | null, + dataForSend: MessageSend | FormData, + token?: string + ) => { try { - const { data } = await axios.post<IMessageRequest, AxiosResponse<Message[]>>(API_URL + `/media/audio/${type}`, dataForSend, { - withCredentials: true, - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.post<IMessageRequest, AxiosResponse<Message[]>>( + API_URL + `/media/audio/${type}`, + dataForSend, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { @@ -414,7 +482,7 @@ export function useMedia(showMessage: (message: string) => void, modelType: stri } }, [data?.access]) - const sendMessage = async <T>(dataForSend: MessageSend<T>) => { + const sendMessage = async <T>(dataForSend: MessageSend) => { if (input.trim() === '') { showMessage('Введите запрос!') return @@ -456,7 +524,9 @@ export function useMedia(showMessage: (message: string) => void, modelType: stri if (result.hasOwnProperty('error')) { try { //@ts-ignore - const message = (result.details as AxiosError).response.data.trim() ?? 'Ошибка отправки сообщения' + const message = + (result.details as AxiosError).response.data.trim() ?? + 'Ошибка отправки сообщения' showMessage(message) return } catch (e) { @@ -1,6 +1,6 @@ type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' -export interface IShortModel { +export interface ShortModel { uid: string title: string description: string @@ -8,18 +8,18 @@ export interface IShortModel { image: string } -export interface IModel { +export interface Model { uid: string title: string description: string slug: string image: string settings: { is_active: boolean } - parameters: IModelParams[] - versions: IModelVersions[] - inputs: IModelInputs[] + parameters: InferenceParams[] + versions: ModelVersions[] + inputs: ModelInputs[] } -export interface IModelParams { +export interface InferenceParams { name: string description: string key: string @@ -34,13 +34,13 @@ export interface IModelParams { } versions: string[] } -export interface IModelVersions { +export interface ModelVersions { name: string description: string default: boolean slug: string } -export interface IModelInputs { +export interface ModelInputs { // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' type: string required: boolean @@ -3,6 +3,7 @@ import axios from 'axios' export const api = axios.create({ validateStatus: () => true, baseURL: process.env.NEXT_PUBLIC_API_HOST, + withCredentials: true, }) api.interceptors.response.use( @@ -1,10 +1,12 @@ import { useSession } from 'next-auth/react' import { api } from './instance' -export function makePrivateRequest(callback: (...params: any) => Promise<any>) { +export function makePrivateRequest<Params extends any[], ReturnType>( + callback: (...params: Params) => Promise<ReturnType> +) { const { data } = useSession() - return async (...params: any) => { + return async (...params: Params) => { if (!data) return api.interceptors.request.use((config) => { @@ -0,0 +1 @@ +export * from './load-file-types' \ No newline at end of file @@ -0,0 +1,10 @@ +export const acceptTypes: Record<string, string | null> = { + image: 'image/png,image/jpeg', + zip: 'application/zip', + pdf: 'application/pdf', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + doc: 'application/msword', + audio: 'audio/*', + txt: '.txt', + text: null, +} @@ -0,0 +1,56 @@ +import React, { useMemo, useRef } from 'react' +import { acceptTypes } from '../config' +import PinnedFileSvg from '#/assets/svg/pinned-file.svg?react' +import AttachFileSvg from '#/assets/svg/attach-file.svg?react' + +interface CommonLoadFileProps { + loading: boolean + unpinFile?: () => void + file: File | null | undefined + setFile: (file: File) => void + types: string[] +} + +export const CommonLoadFile = ({ + loading, + unpinFile, + file, + setFile, + types, +}: CommonLoadFileProps) => { + const ref = useRef(null) + + const inputTypes = useMemo(() => types.map((e) => acceptTypes[e]).filter((e) => e), [types]) + + if (loading || inputTypes.length === 0) return <></> + + return ( + <> + {file ? ( + <button onClick={unpinFile}> + <PinnedFileSvg width={28} height={28} color='#827FFF' stroke='white' /> + </button> + ) : ( + <> + <input + type='file' + ref={ref} + style={{ display: 'none' }} + accept={inputTypes.toString()} + onChange={(e) => { + if (!e.target.files) return + setFile(e.target.files[0]) + }} + alt='Загрузка файла' + /> + <button + style={{ stroke: 'var(--new-ui-main-color)' }} + onClick={() => (ref.current! as any).click()} + > + <AttachFileSvg stroke="gray" width={18} height={20} /> + </button> + </> + )} + </> + ) +} @@ -0,0 +1 @@ +export * from './common-load-file' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,45 @@ +const API_URL = process.env.NEXT_PUBLIC_API_HOST + +import { EventSourcePolyfill } from 'event-source-polyfill' +import { useSession } from 'next-auth/react' + +export function useEventSource() { + const events: { name: string; obj: EventSource }[] = [] + + const { data } = useSession() + + function event(name: string, src: string, callback?: (value: any) => void) { + if (!API_URL) return + + const url = API_URL.slice(-1) !== '/' ? API_URL : API_URL.slice(-1) + + events.push({ + name, + obj: new EventSourcePolyfill(`${url}/${src}`, { + headers: { Authorization: `Bearer ${data?.access}` }, + }), + }) + + if (callback) { + addEventCallback(name, callback) + } + } + + function addEventCallback(name: string, callback: (value: any) => void) { + const event = events.find((e) => e.name === name) + + if (!event) { + throw new Error('Event not found') + } + + event.obj.addEventListener('message', (event) => { + console.log(event) + // callback(JSON.parse(event.data)) + }) + } + + return { + event, + addEventCallback, + } +} @@ -0,0 +1 @@ +export * from './event-source' \ No newline at end of file @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1,11 @@ +import axios from 'axios' + +export async function getBlobFromUrl(url: string) { + const extension = url.match(/\.(\w+)\?/) + + if(!extension) return + + const buffer = await fetch(url).then((res) => res.arrayBuffer()) + + return new File([buffer], `file.${extension[1]}`) +} @@ -0,0 +1,12 @@ +export function getImageResolution(url: string): Promise<{ width: number; height: number }> { + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => { + resolve({ width: img.width, height: img.height }) + } + img.onerror = () => { + reject(new Error('Не удалось загрузить изображение')) + } + img.src = url + }) +} @@ -5,3 +5,6 @@ export * from './get-type-device' export * from './get-random-image' export * from './string' export * from './date-helper' +export * from './image' +export * from './file' +export * from './pending' \ No newline at end of file @@ -0,0 +1,20 @@ +import { useState } from 'react' + +export function withPending<Params extends any[], ReturnType>( + callback: (...params: Params) => Promise<ReturnType> +): [(...params: Params) => Promise<ReturnType>, boolean] { + const [pending, setPending] = useState(false) + + return [ + async (...params: Params) => { + setPending(true) + + const result = await callback(...params) + + setPending(false) + + return result + }, + pending, + ] +} @@ -1,2 +1,3 @@ export { useAutoScroll } from './use-auto-scroll' export { useThemeAndDevice } from './use-theme-and-device' +export * from './use-show-data' \ No newline at end of file @@ -1,11 +1,12 @@ import { useAppSelector } from '#/app/store/store' +import { getDeviceType } from '../helpers' import { DeviceOs } from '../types/entities' -export const useThemeAndDevice = (device?: 'desktop' | 'mobile', deviceOs?: DeviceOs) => { +export const useThemeAndDevice = (device: 'desktop' | 'mobile' = getDeviceType(), deviceOs?: DeviceOs) => { const theme = useAppSelector((state) => state.theme.theme) const desktop = device === 'desktop' const ios = deviceOs === 'ios' - return { theme, desktop, ios } + return { theme, desktop, ios, device } } @@ -1,6 +1,6 @@ import React from 'react' -import { IModelInputs } from '#/shared/api/models/models' +import { ModelInputs } from '#/shared/api/models/models' import { Device, DeviceOs } from '#/shared/lib/types/entities' export type MessageSend<T> = { @@ -43,7 +43,7 @@ export type ChatProps<T> = { elements?: Elements[] model?: string getMessagesPagination?: () => Promise<void> - input_types?: IModelInputs[] + input_types?: ModelInputs[] deleteMessage: (message_uid: string) => void modelTitle: string | undefined currentVersion: string @@ -0,0 +1 @@ +export * from './select-item' \ No newline at end of file @@ -0,0 +1,6 @@ +export interface SelectItem<T = string> { + value: T + label: string + description?: string + [key: string]: any +} @@ -0,0 +1,93 @@ +.container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.select { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 16px 8px 16px 14px; + cursor: pointer; + + border-radius: 13px; + border: 2px solid #40404e; + &_open { + border: 2px solid #7f7df3; + } + + &__text { + font-size: 18px; + color: var(--new-ui-text-color); + line-height: 1.5; + } +} + +.icon { + color: #a6a5a5; + margin: 4px; +} + +html[data-theme='light'] { + .select { + border: 1px solid #e9e9e9; + &_open { + border: 1px solid #7f7df3; + } + } + .list { + &__option { + &_active { + background-color: #ececff; + } + } + } +} + +.list { + position: absolute; + top: 110%; + left: 0; + right: 0; + z-index: 1000; + + background-color: var(--new-ui-main-color); + width: 100%; + border-radius: 13px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, + rgba(0, 0, 0, 0.12) 0px 3px 14px 2px; + + &__wrap { + &:first-child { + li { + border-radius: 13px 13px 0px 0px; + } + } + &:last-child { + li { + border-radius: 0px 0px 13px 13px; + } + } + } + + &__option { + list-style: none; + padding: 10px 16px; + + &_active { + background-color: #1976d214; + } + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + &:active { + background-color: rgba(0, 0, 0, 0.2); + } + } + &__name { + font-size: 18px; + } +} @@ -0,0 +1 @@ +export * from './сommon-select' @@ -0,0 +1,91 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import ArrowDownSvg from '#/assets/svg/arrow-down.svg?react' + +import styles from './common-select.module.scss' +import { c } from '#/shared' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { SelectItem } from '../types/select-item' +import { CSSTransition } from 'react-transition-group' + +interface CommonSelectProps { + value: string | null | undefined + setValue: (value: string) => void + items: SelectItem[] + withoutTooltip?: boolean +} + +export const CommonSelect = ({ + value, + setValue, + items, + withoutTooltip = false, + ...props +}: CommonSelectProps) => { + const ref = useRef<HTMLDivElement>(null) + + const listRef = useRef<HTMLUListElement>(null) + + const [opened, setOpened] = useState(false) + + const item = useMemo(() => items.find((i) => i.value === value), [items, value]) + + useEffect(() => { + console.log(opened) + }, [opened]) + + return ( + <div + // onMouseDown={(event) => + // ref.current && !ref.current.contains(event.target as Node) && setOpened(false) + // } + className={styles.container} + ref={ref} + > + <div + className={c(opened && styles.select_opened, styles.select)} + onClick={() => setOpened((x) => !x)} + {...props} + > + <p className={styles.select__text}>{item?.label}</p> + <ArrowDownSvg + width='14' + height='20' + className={c(opened ? 'rotate-180' : 'rotate-0', styles.icon)} + /> + <CSSTransition + nodeRef={listRef} + timeout={400} + classNames='transform' + in={opened} + unmountOnExit + > + <ul ref={listRef} className={styles.list}> + {items.map(({ label, description, ...item }) => ( + <CommonTooltip + position='left' + title={label} + key={item.value} + className={styles.list__wrap} + description={description} + disabled={withoutTooltip} + > + <li + onClick={() => { + setOpened((x) => !x) + setValue(item.value) + }} + className={c( + styles.list__option, + item.value == value && styles.list__option_active + )} + > + <p className={styles.list__name}>{label}</p> + </li> + </CommonTooltip> + ))} + </ul> + </CSSTransition> + </div> + </div> + ) +} @@ -0,0 +1,2 @@ +export * from './ui' +export * from './types' \ No newline at end of file @@ -0,0 +1,20 @@ +import React, { MouseEventHandler } from 'react' +import SendMessageSvg from '#/assets/svg/send-message.svg?react' +import { Loader } from '#/shared/ui/loader' + +interface CommonSendButtonProps { + loading: boolean + onClick: MouseEventHandler<HTMLButtonElement> +} + +export const CommonSendButton = ({ loading, onClick }: CommonSendButtonProps) => { + + + return loading ? ( + <Loader size={28} thickness={3} /> + ) : ( + <button onClick={onClick}> + <SendMessageSvg color='#8280ff' height={28} width={28} /> + </button> + ) +} @@ -0,0 +1 @@ +export * from './common-send-button' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -14,12 +14,27 @@ export interface CommonTextAreaProps extends React.TextareaHTMLAttributes<HTMLTe buttomSlot?: React.ReactNode trim?: boolean showError?: boolean + wrapperClassName?: string } const inter = Raleway({ subsets: ['latin'] }) export const CommonTextArea = forwardRef<HTMLTextAreaElement, CommonTextAreaProps>( - ({ showError = true, buttomSlot, label, error, className, disabled, trim = false, variant = 'default', ...props }, ref) => { + ( + { + showError = true, + buttomSlot, + label, + error, + className, + disabled, + trim = false, + variant = 'default', + wrapperClassName, + ...props + }, + ref + ) => { const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { event.target.value = trim ? event.target.value.trim() : event.target.value } @@ -27,14 +42,25 @@ export const CommonTextArea = forwardRef<HTMLTextAreaElement, CommonTextAreaProp return ( <div className={c(styles.box, className)}> <div className={styles.header}> - {label && <label className={c(styles.label, styles[`label_${variant}`], error && styles.label_error)}>{label}</label>} + {label && ( + <label + className={c( + styles.label, + styles[`label_${variant}`], + error && styles.label_error + )} + > + {label} + </label> + )} </div> <div className={c( styles.wrapper, styles[`wrapper_${variant}`], disabled && styles[`wrapper_${variant}_disabled`], - error && styles.wrapper_error + error && styles.wrapper_error, + wrapperClassName )} > <textarea @@ -52,7 +78,16 @@ export const CommonTextArea = forwardRef<HTMLTextAreaElement, CommonTextAreaProp /> {buttomSlot} </div> - {showError && <p className={c(styles.header__error, error && styles.header__error_visible)}>{error ? error?.message : ''}</p>} + {showError && ( + <p + className={c( + styles.header__error, + error && styles.header__error_visible + )} + > + {error ? error?.message : ''} + </p> + )} </div> ) } @@ -0,0 +1 @@ +export * from './loader' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './popup.config' \ No newline at end of file @@ -0,0 +1,2 @@ +export const POPUP_USER_MESSAGE_ACTIONS = 'popup-user-message-actions' +export const POPUP_CHAT_BOT_PARAMS = 'popup-chat-bot-params' @@ -0,0 +1 @@ +export * from './store.popup' \ No newline at end of file @@ -0,0 +1,101 @@ +import { create } from 'zustand' + +type Set = { + (partial: (state: PopupStore) => Partial<PopupStore>): void +} + +type Get = () => PopupStore + +function makePopupInstance(set: Set, get: Get, key: string) { + const PopupInstance: Popup = { + id: key, + state: false, + store: {}, + setState(state, store) { + const popups = get().popups + + store = store ?? {} + + set(() => ({ + popups: { + ...popups, + [this.id]: { ...popups[key], state, store }, + }, + })) + }, + toogleState(store) { + const popups = get().popups + + this.setState(!popups[key].state, store) + }, + setStoreProperty(key, value) { + const popups = get().popups + + this.store[key] = value + + set(() => ({ + popups: { + ...popups, + [this.id]: { ...popups[key], store: this.store }, + }, + })) + }, + getStoreProperty(key) { + return this.store[key] + }, + } + + return PopupInstance +} + +export interface Popup { + id: string + state: boolean + store: Record<string, any> + setState: (state: boolean, store?: Record<string, any>) => void + toogleState: (store?: Record<string, any>) => void + setStoreProperty: (key: string, value: any) => void + getStoreProperty: <T = any>(key: string) => T | undefined +} + +export interface PopupStore { + popups: Record<string, Popup> + setPopup: (key: string) => void + getPopup: (key: string) => Popup + hideAllPopups: () => void +} + +export const usePopupStore = create<PopupStore>((set, get) => { + const popups: Record<string, Popup> = {} + + function setPopup(key: string) { + set((state) => ({ + popups: { + ...state.popups, + [key]: Object.assign({}, makePopupInstance(set, get, key)), + }, + })) + } + + function hideAllPopups() { + set((state) => ({ + popups: Object.fromEntries( + Object.entries(state.popups).map(([key, popup]) => [key, { ...popup, state: false }]) + ), + })) + } + + function getPopup(key: string) { + return ( + get().popups[key] ?? + Object.assign({}, makePopupInstance(set, get, key)) + ) + } + + return { + popups, + setPopup, + getPopup, + hideAllPopups, + } +}) @@ -0,0 +1,20 @@ +.backdrop { + visibility: hidden; + opacity: 0; + transition: visibility 0.3s ease-in-out, opacity 0.2s ease-in-out; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background-color: rgba(0, 0, 0, 0.1); + + @media screen and (max-width: 1000px) { + &_open { + visibility: visible; + opacity: 1; + z-index: 10000; + } + } +} @@ -0,0 +1,24 @@ +import React, { useMemo } from 'react' + +import styles from './backdrop.module.scss' +import { usePopupStore } from '../model' +import { c } from '#/shared/lib/helpers' + +export const PopupBackdrop = () => { + const { popups, hideAllPopups } = usePopupStore() + + const someIsOpened = useMemo( + () => Object.values(popups).some((popup) => popup.state), + [popups] + ) + + return ( + <div + onClick={() => hideAllPopups()} + className={c( + styles.backdrop, + someIsOpened && styles.backdrop_open + )} + ></div> + ) +} @@ -0,0 +1,2 @@ +export * from './template' +export * from './backdrop' @@ -0,0 +1,96 @@ +.popup { + display: flex; + flex-direction: column; + + position: absolute; + width: fit-content; + min-width: 100px; + min-height: 70px; + transition: opacity 0.3s ease-in-out; + z-index: 1000; + + &_exiting { + visibility: hidden; + opacity: 0; + } + + &_exited { + visibility: hidden; + opacity: 0; + } + + &_entered { + opacity: 1; + visibility: visible; + } + + &_mobtransform { + width: 100vw; + max-height: 80dvh; + min-height: 12dvh; + position: fixed; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + top: unset !important; + z-index: 10000000; + overflow-y: scroll; + overflow-x: hidden; + padding: 35px; + + border-radius: 13px 13px 0px 0px !important; + transition: transform 0.4s ease-in-out, visibility 0.2s ease-in-out, + opacity 0.2s ease-in-out; + + * { + font-size: 16px; + } + + &_exiting { + transform: translateY(100%) !important; + visibility: hidden; + opacity: 0; + } + + &_exited { + transform: translateY(100%) !important; + visibility: hidden; + opacity: 0; + } + + &_entered { + transform: translateY(0%) !important; + visibility: visible; + left: 0 !important; + opacity: 1; + } + } + + &_horizontal { + &-left { + left: 0; + } + &-right { + right: 0; + } + &-center { + left: 50%; + transform: translateX(-50%); + } + } + &_vertical { + &-top { + top: calc(100% + 10px); + } + &-bottom { + bottom: calc(100% + 10px); + } + } + + &_primary { + background-color: var(--new-ui-main-color); + border-radius: 15px; + padding: 30px; + border: 1px solid rgba($color: #a4aab5, $alpha: 0.1); + } +} @@ -0,0 +1,85 @@ +import React, { useEffect, useMemo, useRef } from 'react' +import styles from './template.module.scss' +import { c } from '#/shared/lib/helpers' +import { usePopupStore } from '../model' +import { ClientOnly } from '#/shared/client-only' +import { createPortal } from 'react-dom' +import { Transition, TransitionStatus } from 'react-transition-group' + +export type PopupTemplateVariant = 'primary' + +interface PopupTemplateProps extends React.HTMLAttributes<HTMLDivElement> { + id: string + variant?: PopupTemplateVariant + className?: string + vertical?: 'top' | 'bottom' + horizontal?: 'left' | 'right' | 'center' + hasMobileTransform?: boolean + onlyMobile?: boolean +} + +const Template = ({ + id, + children, + className, + variant = 'primary', + vertical = 'bottom', + horizontal = 'center', + hasMobileTransform = false, + ...props +}: PopupTemplateProps) => { + const { setPopup, getPopup } = usePopupStore() + + const isMobile = useMemo(() => window.innerWidth < 1000, []) + + const modal = getPopup(id) + + useEffect(() => setPopup(id), []) + + const ref = useRef(null) + + const opened = useMemo(() => { + return !hasMobileTransform ? modal.state : isMobile && modal.state + }, [isMobile, modal.state]) + + return ( + <Transition timeout={400} in={opened} unmountOnExit nodeRef={ref}> + {(state) => ( + <div + ref={ref} + style={{ display: !hasMobileTransform && isMobile ? 'none' : 'block' }} + className={c( + styles.popup, + styles[`popup_${variant}`], + !isMobile && styles[`popup_horizontal-${horizontal}`], + !isMobile && styles[`popup_vertical-${vertical}`], + styles[`popup_${state}`], + hasMobileTransform && styles.popup_mobtransform, + hasMobileTransform && styles[`popup_mobtransform_${state}`], + className + )} + {...props} + > + {children} + </div> + )} + </Transition> + ) +} + +export const PopupTemplate = (props: PopupTemplateProps) => { + const isMobile = useMemo(() => window.innerWidth < 1000, []) + + return ( + <ClientOnly> + {(isMobile && props.hasMobileTransform) || props.onlyMobile ? ( + createPortal( + <Template {...props} />, + document.getElementById('popup-container')! + ) + ) : ( + <Template {...props} /> + )} + </ClientOnly> + ) +} @@ -0,0 +1,10 @@ +import { usePopupStore } from './model' + +export * from './ui' +export * from './model' +export * from './config' + +export function getPopupById(id: string) { + const { getPopup } = usePopupStore() + return getPopup(id) +} @@ -0,0 +1,67 @@ +import { min, max } from 'lodash' +import { useEffect, useRef, useState } from 'react' + +const getPrecision = (num: number) => { + const s = num.toString() + const decimal = s.split('.')[1] + return decimal ? decimal.length : 0 +} + +interface RangeSlider { + value: number + onChange: (value: number) => void + min: number + max: number + step: number +} + +export const useRangeSlider = ({ value, onChange, min, max, step }: RangeSlider) => { + const trackRef = useRef<HTMLDivElement>(null) + const [isDragging, setIsDragging] = useState(false) + + const percent = ((value - min) / (max - min)) * 100 + + const updateValueFromPos = (clientX: number) => { + const rect = trackRef.current?.getBoundingClientRect() + if (!rect) return + + // debugger + + const relativeX = Math.min(Math.max(clientX - rect.left, 0), rect.width) + const ratio = relativeX / rect.width + const rawValue = min + ratio * (max - min) + const steppedValue = Math.round(rawValue / step) * step + const precision = getPrecision(step) + const newValue = parseFloat(steppedValue.toFixed(precision)) + + onChange(newValue) + } + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging) updateValueFromPos(e.clientX) + } + + const handleTouchMove = (e: TouchEvent) => { + if (isDragging) updateValueFromPos(e.touches[0].clientX) + } + + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove) + window.addEventListener('mouseup', () => setIsDragging(false)) + window.addEventListener('touchmove', handleTouchMove) + window.addEventListener('touchend', () => setIsDragging(false)) + return () => { + window.removeEventListener('mousemove', handleMouseMove) + window.removeEventListener('mouseup', () => setIsDragging(false)) + window.removeEventListener('touchmove', handleTouchMove) + window.removeEventListener('touchend', () => setIsDragging(false)) + } + }, [isDragging]) + + return { + trackRef, + setIsDragging, + updateValueFromPos, + percent, + } +} @@ -0,0 +1 @@ +export * from './range-slider' \ No newline at end of file @@ -0,0 +1,53 @@ +.header { + display: flex; + justify-content: space-between; + + &__value { + color: #8280ff; + font-size: 15px; + font-weight: 600; + margin-top: 0.5; + margin-left: 16px; + } +} + +.slider { + margin: 13px 0px; + position: relative; + width: 100%; + height: 4px; + background-color: #eff0f294; + border-radius: 3px; + cursor: pointer; + user-select: none; + + &__thumb { + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 18px; + height: 18px; + background-color: #fff; + border: 2px solid #7f7df3; + border-radius: 50%; + transition: left 0.15s ease-out; + z-index: 1; + + &:hover { + box-shadow: 0 0 0 5px rgba(127, 125, 243, 0.2); + } + &:active { + box-shadow: 0 0 0 8px rgba(127, 125, 243, 0.2); + } + } +} + +html[data-theme='dark'] { + .slider { + background-color: #40404e; + &__thumb { + background-color: #373737; + border: 2px solid #7f7df3; + } + } +} @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react' +import styles from './range-slider.module.scss' +import { useRangeSlider } from '../model/use-range-slider' + +interface RangeSlider { + value: number + title: string + onChange: (value: number) => void + min: number + max: number + step: number +} + +export const RangeSlider = ({ value, title, onChange, min, max, step }: RangeSlider) => { + const { trackRef, setIsDragging, updateValueFromPos, percent } = useRangeSlider({ + value, + onChange, + min, + max, + step, + }) + + useEffect(() => { + console.log(value) + }, [value]) + + return ( + <> + <div className={styles.header}> + <p className='title-main-gray'>{title}</p> + <p className={styles.header__value}>{value}</p> + </div> + + <div + className={styles.slider} + ref={trackRef} + onMouseDown={(e) => { + setIsDragging(true) + updateValueFromPos(e.clientX) + }} + onTouchStart={(e) => { + setIsDragging(true) + updateValueFromPos(e.touches[0].clientX) + }} + > + <div className={styles.slider__thumb} style={{ left: `${percent}%` }} /> + </div> + </> + ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,3 @@ +export * from './use-listeners-wheel' +export * from './use-listeners-thumbnail' +export * from './use-hovered' \ No newline at end of file @@ -0,0 +1,12 @@ +import { MutableRefObject, useEffect, useRef, useState } from 'react' + +export function useGenerallScroll(containerRef: MutableRefObject<HTMLDivElement | null>) { + function scrollToBottom() { + if (!containerRef.current) return + containerRef.current.style.transform = `translateY(0px)` + } + + return { + scrollToBottom, + } +} @@ -0,0 +1,21 @@ +import { useState } from 'react' + +export function useHovered() { + const [hovered, setHovered] = useState(false) + + function onMounseEnter() { + document.body.style.overflow = 'hidden' + setHovered(true) + } + + function onMouseLeave() { + document.body.style.overflow = 'visible' + setHovered(false) + } + + return { + onMounseEnter, + onMouseLeave, + hovered, + } +} @@ -0,0 +1,8 @@ +import { ForwardedRef, MutableRefObject, Ref, useEffect, useMemo, useRef } from 'react' + +export function useListenersThumbnail() { + const draggbleRef = useRef<HTMLDivElement | null>(null) + return { + draggbleRef, + } +} @@ -0,0 +1,79 @@ +import { + MutableRefObject, + useCallback, + useEffect, + useRef, + useState, + WheelEventHandler, +} from 'react' + +export function useListenersWheel( + containerRef: MutableRefObject<HTMLDivElement | null>, + draggbleRef: MutableRefObject<HTMLDivElement | null>, + scrollRef: MutableRefObject<HTMLDivElement | null>, + blockedScrollTop: boolean +) { + const preventScroll = useRef(false) + const translateYRef = useRef(0) + + const [draggbleHeight, setDraggbleHeight] = useState(100) + + function defineDraggbleHeight() { + const container = containerRef.current + const { current } = scrollRef + + if (current && container) + setDraggbleHeight((current.clientHeight / container?.clientHeight) * 100) + } + + useEffect(() => { + defineDraggbleHeight() + }, [containerRef.current?.clientHeight]) + + const onWheelCallback = useCallback<WheelEventHandler>((e) => { + if (preventScroll.current) return + + requestAnimationFrame(() => { + const deltaY = e.deltaY + translateYRef.current -= deltaY + + if (translateYRef.current < 0) translateYRef.current = 0 + + const container = containerRef.current + + const { current } = scrollRef + + if (container) container.style.transform = `translateY(${translateYRef.current}px)` + + defineDraggbleHeight() + + if (draggbleRef.current && scrollRef.current && container) { + const proportion = scrollRef.current.clientHeight / container.clientHeight + + draggbleRef.current.style.transform = `translateY(${ + -(translateYRef.current * proportion) + + scrollRef.current.clientHeight - + draggbleRef.current.clientHeight + }px)` + } + + if (blockedScrollTop && container && current) { + const diffrenece = + translateYRef.current - container.clientHeight + current.clientHeight + + if (diffrenece > 0) { + translateYRef.current = container.clientHeight - current.clientHeight + } + } + + preventScroll.current = false + }) + }, []) + + return { + onWheelCallback, + preventScroll, + translateYRef, + draggbleHeight, + } +} @@ -0,0 +1,41 @@ +.scroll { + height: 100%; + overflow: hidden; + position: relative; + will-change: transform; + + &__container { + bottom: 0; + position: absolute; + width: 100%; + left: 0; + padding-right: 20px; + min-height: 100%; + } + + &__thumbnail { + position: absolute; + right: 0px; + top: 10px; + height: 98%; + width: 6px; + z-index: 1000; + background-color: rgba($color: gray, $alpha: 0.2); + display: flex; + justify-content: center; + border-radius: 2px; + transition: all 0.3s ease; + opacity: 0; + + &_visible { + opacity: 1; + } + } + + &__draggble { + width: 6px; + border-radius: 2px; + background-color: rgba($color: gray, $alpha: 0.4); + bottom: 0; + } +} @@ -0,0 +1,64 @@ +import React, { forwardRef, useImperativeHandle, useRef } from 'react' +import styles from './common-scrollbar.module.scss' +import { c } from '#/shared/lib/helpers' +import { useHovered, useListenersThumbnail, useListenersWheel } from '../model' +import { useGenerallScroll } from '../model/use-generall-scroll' + +interface CommonScrollbarProps extends React.HTMLAttributes<HTMLDivElement> { + blockedScrollTop?: boolean +} + +export interface CommonScrollbarRef extends HTMLDivElement { + scrollToBottom: () => void +} + +export const CommonScrollbar = forwardRef<CommonScrollbarRef, CommonScrollbarProps>( + ({ children, className, blockedScrollTop = false, ...props }, ref) => { + const containerRef = useRef<HTMLDivElement | null>(null) + + const localRef = useRef<HTMLDivElement | null>(null) + + const { scrollToBottom } = useGenerallScroll(containerRef) + + useImperativeHandle(ref, () => ({ ...localRef.current!, scrollToBottom })) + + const { draggbleRef } = useListenersThumbnail() + + const { onMounseEnter, onMouseLeave, hovered } = useHovered() + + const { onWheelCallback, draggbleHeight } = useListenersWheel( + containerRef, + draggbleRef, + localRef, + blockedScrollTop + ) + + return ( + <div + ref={localRef} + onMouseEnter={onMounseEnter} + onMouseLeave={onMouseLeave} + onWheel={onWheelCallback} + className={c(styles.scroll, className)} + > + <div + className={c( + styles.scroll__thumbnail, + hovered && styles.scroll__thumbnail_visible + )} + > + <div + ref={draggbleRef} + style={{ + height: draggbleHeight + '%', + }} + className={styles.scroll__draggble} + ></div> + </div> + <div ref={containerRef} className={c(styles.scroll__container)}> + {children} + </div> + </div> + ) + } +) @@ -0,0 +1 @@ +export * from './common-scrollbar' \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './ui' +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './title' \ No newline at end of file @@ -1,14 +1,37 @@ -.wrapModelTitle { +.wrap { margin-bottom: 20px; margin-top: 15px; - // width: max-content; - width: 100% !important; + width: 100% !important; + @media (max-width: 768px) { margin: 0; margin-top: 15px; } - .bigTitle { - min-width: max-content !important; +} + +.navigation { + display: flex; + align-items: center; + gap: 1ch; + color: var(--new-ui-gray-color); + + &__back { + font-size: 20px; + font-weight: 600; + color: var(--new-ui-gray-color); + } + + &__current { + font-size: 23px; + font-weight: 500; + } +} + +.box { + &__title { + font-size: 35px; + font-weight: 600; + min-width: max-content !important; @media (max-width: 768px) { display: none; overflow: hidden; @@ -0,0 +1,32 @@ +import React from 'react' +import Link from 'next/link' + +import styles from './title.module.scss' +import { c } from '#/shared' + +export interface CommonTitleProps { + type: string + title: string + linkBack?: string + rightSlot?: React.ReactNode +} + +export function CommonTitle({ linkBack, type, title, rightSlot }: CommonTitleProps) { + return ( + <div className={styles.wrap}> + {linkBack && ( + <div className={styles.navigation}> + <Link className={c(styles.navigation__back)} href={linkBack}> + {type} + </Link> + <span>•</span> + <p className={styles.navigation__current}> {title}</p> + </div> + )} + <div className={c(styles.box)}> + <p className={styles.box__title}>{title}</p> + <div>{rightSlot}</div> + </div> + </div> + ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,115 @@ +.wrap { + position: relative; +} +.tooltip { + border-radius: 9px; + background: #ececff; + color: var(--air-color); + padding: 6px 12px; + max-width: 340px; + width: max-content; + position: absolute; + left: 50%; + font-weight: 500; + + transition: all 0.3s ease-in-out; + top: 140%; + box-shadow: 0px 0px 4px rgba($color: #000000, $alpha: 0.2); + + &_bottom { + left: 50%; + top: 140%; + transform: translateX(-50%) scale(0.9); + + &_exiting { + opacity: 0; + } + + &_entered { + transform: translateX(-50%) scale(1); + opacity: 1; + } + + &_entering { + transform: translateX(-50%) scale(1); + opacity: 1; + } + + &::after { + background: #ececff; + content: ''; + position: absolute; + top: -12px; + width: 15px; + height: 12px; + left: 50%; + clip-path: polygon(50% 0%, 0% 100%, 100% 100%); + transform: translateX(-50%); + } + } + + &_left { + background-color: white; + top: 50%; + right: 100%; + left: unset; + padding: 12px 16px; + transform: translateY(-50%) scale(0.9); + + &_exiting { + opacity: 0; + } + + &_entered { + transform: translateY(-50%) scale(1); + opacity: 1; + } + + &_entering { + transform: translateY(-50%) scale(1); + opacity: 1; + } + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 100%; + transform: translateY(-50%); + width: 12px; + height: 12px; + background-color: #ececff; + clip-path: polygon(0 0, 100% 50%, 0 100%); + } + } + + &__heading { + font-size: 17px; + line-height: 1.5; + font-weight: 600; + color: rgb(102, 102, 102); + } + &__description { + font-size: 14px; + line-height: 1.5; + font-weight: 500; + color: rgb(134, 134, 134); + letter-spacing: 0.1px; + margin-top: 8px; + width: 200px; + } +} + +html[data-theme='dark'] { + .tooltip { + background-color: #303035; + + &::after { + background-color: #303035; + } + + &__heading { + color: rgb(207, 207, 207); + } + } +} @@ -0,0 +1,61 @@ +import React, { HTMLAttributes, useRef, useState } from 'react' + +import styles from './common-tooltip.module.scss' +import { c } from '#/shared/lib/helpers' +import { CSSTransition, Transition, TransitionStatus } from 'react-transition-group' + +interface CommonTooltipProps extends HTMLAttributes<HTMLDivElement> { + content?: string | React.ReactNode + title?: string + description?: string + position?: 'bottom' | 'left' + disabled?: boolean +} + +export const CommonTooltip = ({ + children, + content, + title, + description, + className, + position = 'bottom', + disabled = false, + ...props +}: CommonTooltipProps) => { + const [hovered, setHovered] = useState(false) + + const ref = useRef(null) + + return ( + <div + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + className={c(styles.wrap, styles[`wrap_${position}`], className)} + {...props} + > + {children} + + <Transition timeout={300} nodeRef={ref} in={hovered && !disabled} unmountOnExit> + {(state) => ( + <div + ref={ref} + className={c( + styles.tooltip, + styles[`tooltip_${position}`], + styles[`tooltip_${position}_${state}`] + )} + > + {position == 'bottom' ? ( + content + ) : ( + <> + <p className={styles.tooltip__heading}>{title}</p> + <p className={styles.tooltip__description}>{description}</p> + </> + )} + </div> + )} + </Transition> + </div> + ) +} @@ -0,0 +1 @@ +export * from './common-tooltip' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -14,7 +14,7 @@ export * from './lib/helpers' export * from './ui/graphics-main-page' export { Input, InputStyleDark, InputStyleLight } from './ui/input' export { InputImagesModels } from './ui/input-images-models/input-images-models' -export { Loader } from './ui/loader/loader' +export { Loader } from './ui/loader/ui/loader' export type { ModalProps } from './ui/modal/modal' export { useConcat } from './lib/helpers' export { Modal } from './ui/modal/modal' @@ -4,11 +4,11 @@ import { Box, Typography } from '@mui/material' import { useSession } from 'next-auth/react' import { getAudio } from '#/entities/audio-model' import { NextPageWithLayout } from '#/pages/_app' -import { ImageModelCard, IShortModel } from '#/entities/model-entity' +import { ImageModelCard, ShortModel } from '#/entities/model-entity' import { useAppSelector } from '#/app/store/store' export const AudioModelsPage: NextPageWithLayout = () => { - const [bots, setBots] = useState<IShortModel[] | null>(null) + const [bots, setBots] = useState<ShortModel[] | null>(null) const { data, status } = useSession() @@ -27,7 +27,7 @@ export const AudioModelsPage: NextPageWithLayout = () => { </Typography> <Box display='flex' flexWrap='wrap'> {bots?.map((item, idx) => { - return ( + return ( <ImageModelCard accessed_models={user.payment_plan.plan.accessed_models} key={idx} @@ -0,0 +1 @@ +export * from './use-chat-bots' \ No newline at end of file @@ -0,0 +1,27 @@ +import { ShortModel, getChatBots } from '#/entities/model-entity' +import { withPending } from '#/shared' +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useState } from 'react' + +export function useChatBots() { + const [bots, setBots] = useState<ShortModel[]>([]) + + const { showMessage } = useShowDataStore() + + const [fetchChatBots, pending] = withPending( + makePrivateRequest(async () => { + const { data, status } = await getChatBots() + + if (status !== 200) return showMessage('Ошибка при получении чат ботов') + + setBots(data) + }) + ) + + return { + pending, + fetchChatBots, + bots, + } +} \ No newline at end of file @@ -0,0 +1,84 @@ +@keyframes fadein { + 0% { + opacity: 0; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.header { + width: 70%; + + @media screen and (max-width: 768px) { + width: 100%; + } +} + + +.main { + width: 100%; + padding-right: 60px; + display: grid; + grid-template-columns: 1fr 25%; + grid-template-rows: 350px max-content; + gap: 20px; + + @media screen and (max-width: 1000px) { + grid-template-columns: 100%; + grid-template-rows: 100% 100%; + padding-right: unset; + padding-top: 20px; + } + + &__chats { + width: 100%; + grid-row: 1 / 2; + margin: unset !important; + } + + &__options { + width: 100%; + + @media screen and (max-width: 1000px) { + display: none !important; + } + } + + &__chat { + grid-row: 1 / 3; + width: 100%; + + @media screen and (max-width: 1000px) { + grid-row: unset; + } + } +} + +.blocked { + display: flex; + align-items: center; + justify-content: center; + background-color: #c32528; + // padding-right: 20px; + padding: 8px 15px; + gap: 5px; + position: absolute; + left: 0; + bottom: -10px; + width: 100%; + border-radius: 0px 0px 15px 15px; + + // @media screen and (max-width: 768px) { + // display: none; + // } + + &__text { + color: white; + font-size: 14px; + font-weight: 600; + } +} @@ -1,386 +1,54 @@ -import React, { useCallback, useEffect } from 'react' -import { useDispatch } from 'react-redux' -import { Box, Collapse, Stack, Typography } from '@mui/material' +import React, { useEffect, useMemo } from 'react' import { useRouter } from 'next/router' -import { useSession } from 'next-auth/react' -import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -import BotParamsMap from '#/features/bot-params/bot-params-map' -import { useChats } from '#/features/chats' -import Title from '#/features/title/title' -import { TutorialContext } from '#/features/tutorial-context/tutorial-context' -import { ChatSelect } from '#/app/components/chat_select' -import { ResetFilters } from '#/app/components/filters/reset_filters' -import { setParams as setParametres } from '#/app/store/model-parametres-store' -import { useAppSelector } from '#/app/store/store' -import { DrawerCustom, Error, useModel } from '#/shared' -import model_api from '#/shared/api/models/api' -import styles from './chats-bot.module.scss' +import { CommonTitle } from '#/shared/ui/title' +import styles from './chat-bot.module.scss' import { Chats } from '#/widgets/chats' -import { c, getDeviceType, getOs } from '#/shared/lib/helpers' import { NextPageWithLayout } from '#/pages/_app' -import { getDefaultLayoutDynamic } from '#/widgets/layouts' import Head from 'next/head' -import LockSvg from '#/assets/svg/lock.svg?react' -import BlockedSvg from '#/assets/svg/blocked.svg?react' -import { IModelTag, IModel } from '#/entities/model-entity' -import { useThemeAndDevice } from '#/shared/lib/hooks' -import { SvgIcon } from '#/shared/ui/svg' +import { useChatBot } from '#/entities/model-entity' +import { ModelTags } from '#/widgets/model-tags' import { AllChatWindow } from '#/widgets/chat-window' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { ChatModelOptions } from '#/widgets/chat-model-options' +import { getPopupById, POPUP_CHAT_BOT_PARAMS } from '#/shared/ui/popup' export interface ChatBotPageProps {} const Page: NextPageWithLayout<ChatBotPageProps> = () => { - const [botParams, setBotParams] = React.useState<IModel | null>(null) - const [modelType, setModelType] = React.useState<string>('') - const [openFiltersMobile, setOpenFiltersMobile] = React.useState(false) - const [version, setVersion] = React.useState<string>('') - const [step, setStep] = React.useState<number>(0) + const { query } = useRouter() - const [params, setParams] = React.useState<boolean>(false) - const [file, setFile] = React.useState<File | null>(null) - const { showMessage } = useShowDataStore() + const { botParams, fetchBotParams } = useChatBot() - const deviceType = getDeviceType() - const deviceOs = getOs() + const chatBotOptionPopup = getPopupById(POPUP_CHAT_BOT_PARAMS) - const desktop = deviceType === 'desktop' - const { data } = useSession() - const router = useRouter() - - const { - chats, - currentChat, - chatSetting, - setChatSetting, - setChat, - removeChat, - createNewChat, - handleClickChatSetting, - isTryRename, - setIsTryRename, - } = useChats(modelType) - - const { messages, sendMessage, loading, getMessagesPagination, deleteMessage } = useModel(currentChat, showMessage, modelType) - - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useDispatch() - - const deleteMessageMemo = useCallback(deleteMessage, [currentChat, messages]) - - const { push } = useRouter() - - React.useEffect(() => { - if (data?.access) { - model_api.getBotParams(router.asPath.split('/')[2], data.access).then((res) => { - if (!res.title) return push('/404') - setBotParams(res) - setModelType(res.slug) - if (res.versions.length !== 0) { - setVersion(res.versions[0].slug) - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => (v.versions.includes(res.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } else { - setVersion('') - dispatch(setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } - }) - } - }, [data?.access, router.query]) - - const resetParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (botParams.versions?.length !== 0) { - setVersion(botParams.versions[0].slug) - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => (v.versions.includes(botParams.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } else { - setVersion(botParams.slug) - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } - } - } - - const setDefaultParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (version !== '') { - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => (v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } else { - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } - } - } - - const viewMobileSettings = () => { - setOpenFiltersMobile(true) - } - - const hideMobileSettings = () => { - setOpenFiltersMobile(false) - } - - const onSendMessage = (input: string, required: (string | null)[]) => { - if (required.includes('text') && (input === '' || input === null)) { - showMessage('Введите сообщение!') - return false - } - if (required.includes('image') && file === null) { - showMessage('Прикрепите изображение!') - return false - } - - let data = {} - if (version === '') { - data = { - ...includeParams, - } - } else { - data = { - version: version, - ...includeParams, - } - } - - sendMessage<any>({ - content: input, - file: file, - info: { - ...data, - }, - }) - return true - } - - const { theme } = useThemeAndDevice() + useEffect(() => { + fetchBotParams() + }, [query.slug]) return ( - <> + <main className={styles.page}> <Head> <title>{botParams ? botParams.title : 'Загрузка...'} - -
- - {botParams && - botParams.tags.map((tag, index) => ( - <div key={index} className={styles.tag}> - <SvgIcon width={23} height={23} url={tag.icon} className={styles.tag__icon} /> - - <span className={styles.tag__text}>{tag.title}</span> - </div> - ))} - </div> - } - type={'Чат-боты'} - linkBack={'/chat-bot'} + <div className={styles.header}> + <CommonTitle + type='Чат-боты' + linkBack='/chat-bot' + title={botParams ? botParams.title : 'Загрузка...'} + rightSlot={<ModelTags tags={botParams ? botParams.tags : []} />} + /> + </div> + <div className={styles.main}> + <div className={styles.main__chat}> + <AllChatWindow + setOpenFiltersMobile={() => chatBotOptionPopup.toogleState()} /> </div> - <Box sx={{ height: 'calc(100% - 100px)' }} className={styles.main}> - <Box className={styles.chatWindow}> - <AllChatWindow - deviceOs={deviceOs} - input_types={botParams?.inputs} - setFile={setFile} - file={file} - blocked={botParams?.blocked} - getMessagesPagination={getMessagesPagination} - sendMessage={onSendMessage} - deleteMessage={deleteMessageMemo} - openMobileFilters={viewMobileSettings} - isCalculating={true} - device={deviceType} - loading={loading} - messages={messages || []} - modelType={modelType} - modelTitle={botParams?.title} - currentVersion={version} - tags={botParams?.tags ?? []} - /> - {/* {botParams?.blocked && ( - <div className={styles.blocked}> - <BlockedSvg width={16} height={16} color='white' /> - <span className={c(styles.blocked__text)}> - Модель недоступна - </span> - </div> - )} */} - </Box> - <Box className={styles.settings}> - <Chats - desktop={desktop} - isTryRename={isTryRename} - setIsTryRename={setIsTryRename} - currentChat={currentChat} - setChat={setChat} - chats={chats} - chatSetting={chatSetting} - setChatSetting={setChatSetting} - removeChat={removeChat} - createNewChat={createNewChat} - handleClickChatSetting={handleClickChatSetting} - /> - {desktop && ( - <Stack className='pd-30 bg-color-block border-radius-main' spacing={2}> - {botParams?.versions && botParams.versions?.length !== 0 && ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - )} - {botParams && botParams.parameters?.length > 0 && ( - <Box - display={'flex'} - alignItems={'center'} - gap={'5px'} - sx={{ cursor: 'pointer' }} - onClick={() => { - setParams(!params) - }} - > - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ПАРАМЕТРЫ - </Typography> - <svg - width='14' - height='20' - viewBox='0 0 21 13' - fill='none' - xmlns='http://www.w3.org/2000/svg' - className={`${params ? 'rotate-180' : 'rotate-0'}`} - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M0.614851 0.615358C1.00866 0.221668 1.54271 0.000505666 2.09955 0.000505642C2.6564 0.000505617 3.19044 0.221668 3.58425 0.615357L10.4996 7.53066L17.4149 0.615357C17.8109 0.232825 18.3414 0.0211567 18.892 0.0259414C19.4426 0.0307261 19.9693 0.25158 20.3587 0.640937C20.748 1.03029 20.9689 1.557 20.9737 2.10761C20.9784 2.65823 20.7668 3.18869 20.3842 3.58476L11.9843 11.9848C11.5904 12.3784 11.0564 12.5996 10.4996 12.5996C9.94271 12.5996 9.40866 12.3784 9.01485 11.9848L0.614851 3.58476C0.221162 3.19095 -4.3461e-07 2.6569 -4.5895e-07 2.10006C-4.8329e-07 1.54321 0.221162 1.00917 0.614851 0.615358Z' - fill='#7f7df3' - /> - </svg> - </Box> - )} - - {botParams && botParams.parameters?.length > 0 ? ( - <Collapse in={params} orientation='vertical' collapsedSize={0} sx={{}}> - <BotParamsMap currentVersion={version} params={botParams?.parameters} /> - <ResetFilters desktop={desktop} closeDrawer={hideMobileSettings} reset={resetParams} /> - </Collapse> - ) : ( - <Typography - sx={{ - color: '#6e6e6e', - fontSize: '15px', - fontWeight: '500', - }} - > - Параметры отсутствуют - </Typography> - )} - </Stack> - )} - {!desktop && ( - <DrawerCustom open={openFiltersMobile} onClose={hideMobileSettings}> - <Stack spacing={1} padding={2.4}> - {botParams?.versions && botParams.versions?.length !== 0 && ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - margin: '20px 0px 0px !important', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - )} - {botParams && botParams.parameters?.length > 0 ? ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - margin: '20px 0px 0px !important', - }} - > - ПАРАМЕТРЫ - </Typography> - <BotParamsMap currentVersion={version} params={botParams?.parameters} /> - <ResetFilters closeDrawer={hideMobileSettings} desktop={desktop} reset={resetParams} /> - </> - ) : ( - <Typography - sx={{ - color: '#6e6e6e', - fontSize: '15px', - fontWeight: '500', - }} - > - Параметры отсутствуют - </Typography> - )} - </Stack> - </DrawerCustom> - )} - </Box> - </Box> - </TutorialContext.Provider> - </> + <Chats className={styles.main__chats} /> + <ChatModelOptions className={styles.main__options} /> + </div> + </main> ) } @@ -22,3 +22,9 @@ margin-right: 0px; } } + +.title { + font-size: 24px; + margin-top: 25px; + font-weight: 700; +} @@ -1,55 +1,36 @@ import * as React from 'react' -import { useEffect, useState } from 'react' -import { Box, Typography } from '@mui/material' -import CircularProgress from '@mui/material/CircularProgress' -import { useSession } from 'next-auth/react' +import { useEffect } from 'react' import { useAppSelector } from '#/app/store/store' -import model_api from '#/shared/api/models/api' import { NextPageWithLayout } from '#/pages/_app' -import { ChatModelCard, IShortModel } from '#/entities/model-entity' +import { ChatModelCard } from '#/entities/model-entity' import styles from './chat-bots.module.scss' +import { Loader } from '#/shared' +import { useChatBots } from '../model' const Page: NextPageWithLayout = () => { - const [bots, setBots] = useState<IShortModel[] | null>(null) - const { data } = useSession() - const user = useAppSelector((state) => state.user) + const { accessed_models } = useAppSelector((state) => state.user.payment_plan.plan) - useEffect(() => { - if (!data) return + const { bots, fetchChatBots, pending } = useChatBots() - model_api.getBots(data.access).then((res) => { - setBots(res) - }) - }, [data]) + useEffect(() => { + fetchChatBots() + }, []) return ( <> - <Typography sx={{ fontSize: 24, fontWeight: 'bold', marginTop: '25px' }}> - Чат-боты - </Typography> + <p className={styles.title}>Чат-боты</p> <div className={styles.cards}> - {bots ? ( + {!pending ? ( bots.map((item) => ( <ChatModelCard - accessed_models={user.payment_plan.plan.accessed_models} - key={item.uid} {...item} + accessed_models={accessed_models} + key={item.uid} /> )) ) : ( - <CircularProgress - size={50} - thickness={3} - sx={{ - color: '#7F7DF3', - position: 'absolute', - top: '40%', - left: 0, - right: 0, - margin: '0 auto', - }} - /> + <Loader size={50} /> )} </div> </> @@ -1 +1,2 @@ -export * from './ui' \ No newline at end of file +export * from './ui' +export * from './model' \ No newline at end of file @@ -1,151 +0,0 @@ -.container { - width: 100%; - border-radius: 15px; - height: 100%; - position: absolute; - bottom: 0; - left: 0; - background-color: var(--new-ui-main-color); - opacity: 0; - z-index: -1; - transition: all 0.3s ease-in-out; - - &_active { - opacity: 1; - z-index: 100; - } -} - -.header { - width: 70%; - margin-bottom: 50px; - - @media screen and (max-width: 768px) { - width: 100%; - margin-bottom: 0px; - } -} - -@keyframes fadein { - 0% { - opacity: 0; - } - 50% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -// .blocked { -// display: flex; -// align-items: center; -// justify-content: center; -// background-color: white; -// border-radius: 100px; -// padding-right: 20px; -// padding: 8px 12px; -// width: max-content; - -// span { -// color: #ff2372; -// font-weight: 500; -// padding-left: 10px; -// font-size: 14px; -// width: max-content; -// } -// } - -.icon { -} - -.tags { - min-width: 100%; - display: flex; - gap: 8px; - align-items: center; - justify-content: flex-end; - - @media screen and (max-width: 768px) { - justify-content: flex-start; - } -} - -.tag { - display: flex; - align-items: center; - justify-content: center; - background-color: white; - border-radius: 100px; - width: 50px; - height: 50px; - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.1); - transition: width 0.3s ease-in-out; - - @media screen and (max-width: 768px) { - height: 40px; - width: 40px; - } - - &__icon { - color: var(--air-color); - } - - &:hover { - padding: 12px; - width: 200px; - - .tag__icon { - animation: fadein 0.6s ease-in-out; - } - - .tag__text { - opacity: 1; - position: static; - animation: fadein 0.6s ease-in-out; - display: block; - } - } - - &__text { - position: absolute; - color: var(--air-color); - font-weight: 500; - padding-left: 10px; - font-size: 14px; - opacity: 0; - display: none; - min-width: max-content !important; - } -} - -.blocked { - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-color: #c32528; - padding: 20px; - gap: 10px; - position: absolute; - - left: 0; - - width: 100%; - border-radius: 15px; - top: 0; - - @media screen and (max-width: 768px) { - top: unset; - // bottom: -22px; - z-index: 1; - border-radius: 15px; - } - - &__text { - color: white !important; - font-size: 16px; - font-weight: 600; - } -} @@ -1,373 +0,0 @@ -import { useEffect, useMemo } from 'react' -import { Box, Collapse, Stack, Typography } from '@mui/material' -import Head from 'next/head' -import { useRouter } from 'next/router' -import { useSession } from 'next-auth/react' - -import styles from './image-model.module.scss' - -import { ChatSelect } from '#/app/components/chat_select' -import { ResetFilters } from '#/app/components/filters/reset_filters' -import { Layout } from '#/app/layout' -import BlockedSvg from '#/assets/svg/blocked.svg?react' -import LockSvg from '#/assets/svg/lock.svg?react' -import { useImageBot } from '#/entities/model-entity/model/use-image-bot' -import BotParamsMap from '#/features/bot-params/bot-params-map' -import { useImageBotCreateImage } from '#/features/image-bot-create-image' -import { useImagesBotFilters } from '#/features/image-bot-filters' -import { useImagesUniqInput } from '#/features/image-bot-input' -import { useImageBotPagination } from '#/features/image-bot-pagination' -import Title from '#/features/title/title' -import { NextPageWithLayout } from '#/pages/_app' -import { DrawerCustom, Error, Loader } from '#/shared' -import { c, getDeviceType, getOs } from '#/shared/lib/helpers' -import { useThemeAndDevice } from '#/shared/lib/hooks' -import { Device, DeviceOs } from '#/shared/lib/types/entities' -import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' -import { SvgIcon } from '#/shared/ui/svg' -import { ImageMessagesList, useImagesPagination } from '#/widgets/messages' -import { ModelInput } from '#/features/model-input' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' - -const ImageModelPage: NextPageWithLayout = () => { - const { query } = useRouter() - - const { botParams, version, modelType, fetchBotParams, resetParams, setDefaultParams, setVersion } = useImageBot(query.slug as string) - - const deviceType = getDeviceType() - const deviceOs = getOs() - - const { ios, desktop } = useThemeAndDevice(deviceType, deviceOs) - - const { showMessage } = useShowDataStore() - - const router = useRouter() - - const { data: session } = useSession() - - const { openFiltersMobile, setOpenFiltersMobile, params, setParams, includeParams } = useImagesBotFilters() - - const { refScrollMobile, refScrollDesktop, mobileScrollContainer, onObserverMounted, setMessages, fetchMessages, loading, offset, messages } = - useImageBotPagination(deviceType) - - const { createImage, isComplete, createLoading } = useImageBotCreateImage(showMessage, modelType, deviceType, setMessages, mobileScrollContainer) - - const { onCreateImage, onLoadImage, image, setImage } = useImagesUniqInput(version, includeParams, createImage) - - async function onFetch() { - await Promise.all([fetchBotParams()]) - } - - useEffect(() => { - if (!session) return - onFetch() - onObserverMounted() - }, [session?.access]) - - const { theme } = useThemeAndDevice() - - return ( - <> - <Head> - <title>{botParams ? botParams?.title : 'Загрузка...'} - -
- - {botParams && - botParams.tags.map((tag, index) => ( - <div key={index} className={styles.tag}> - <SvgIcon width={23} height={23} url={tag.icon} className={styles.tag__icon} /> - - <span className={styles.tag__text}>{tag.title}</span> - </div> - ))} - </div> - } - /> - </div> - <Box - display={'flex'} - justifyContent='space-between' - alignItems='start' - flexDirection={desktop ? 'row' : 'column-reverse'} - sx={{ - marginBottom: desktop ? 0 : 2, - width: desktop ? '97%' : '100%', - marginTop: desktop ? 3 : '15px', - }} - > - <Box - sx={{ - width: desktop ? '73%' : '100%', - marginLeft: 0, - display: 'flex', - flexDirection: desktop ? 'column' : 'column-reverse', - }} - > - {desktop ? ( - <> - <Stack - alignItems='center' - sx={{ - marginBottom: '15px', - position: 'relative', - zIndex: 1, - }} - > - {botParams && ( - <ModelInput - currentVersion={version} - styles={'images'} - input_types={botParams.inputs} - image={image} - value={prompt} - desktop={desktop} - blocked={botParams.blocked} - loading={createLoading} - imageLoad={onLoadImage} - sendMessage={onCreateImage} - unpinImage={() => setImage(null)} - viewMobileSettings={() => setOpenFiltersMobile(true)} - /> - )} - {botParams?.blocked && ( - <div className={styles.blocked}> - <BlockedSvg width={16} height={16} color='white' /> - <span className={c(styles.blocked__text)}>Модель недоступна</span> - </div> - )} - </Stack> - <Box - className={'bg-color-block border-radius-main'} - sx={{ - padding: '30px', - position: 'relative', - }} - > - <ImageMessagesList - isComplete={loading} - device={deviceType} - images={messages} - getMessagesPagination={fetchMessages} - /> - <Box - sx={{ - position: 'absolute', - bottom: '0', - visibility: 'hidden', - height: '800px', - width: '100%', - }} - ref={refScrollDesktop} - ></Box> - </Box> - </> - ) : ( - <Box className={'bg-color-block border-radius-main'} sx={{ position: 'relative' }}> - <div className={c(styles.container, loading && offset.current === 0 && styles.container_active)}> - <div - style={{ - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - }} - > - <Loader /> - </div> - </div> - <div style={{ position: 'relative', borderRadius: '13px' }}> - <Box - sx={{ - padding: '30px', - height: `calc(100dvh - 116px - 61px - 15px ${botParams?.blocked ? '- 17px' : '- 23.5px'})`, - // height: `calc(100dvh - 116px - 61px - 15px - 23.5px)`, - overflowY: 'scroll', - overflowX: 'hidden', - position: 'relative', - }} - ref={mobileScrollContainer} - className={'smallScroll'} - > - <div style={{ position: 'absolute', top: 300 }} ref={refScrollMobile}></div> - <ImageMessagesList - isComplete={loading} - device={deviceType} - images={messages} - getMessagesPagination={fetchMessages} - /> - </Box> - - <Stack alignItems='center' sx={{ zIndex: 10, position: 'relative', margin: 1.25 }}> - {botParams && ( - <ModelInput - currentVersion={version} - styles={'images'} - input_types={botParams.inputs} - image={image} - blocked={botParams.blocked} - value={prompt} - desktop={desktop} - loading={createLoading} - imageLoad={onLoadImage} - sendMessage={onCreateImage} - unpinImage={() => setImage(null)} - viewMobileSettings={() => setOpenFiltersMobile(true)} - /> - )} - {botParams?.blocked && ( - <div className={styles.blocked}> - <BlockedSvg width={21} height={21} color='white' /> - <span className={c(styles.blocked__text)}>Модель недоступна</span> - </div> - )} - </Stack> - </div> - </Box> - )} - </Box> - {desktop && ( - <Stack spacing={2} className='pd-30 bg-color-block border-radius-main' sx={{ width: '25.5%', height: 'auto' }}> - {botParams?.versions && botParams.versions.length !== 0 ? ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - ) : ( - <></> - )} - {botParams && botParams.parameters?.length > 0 && ( - <Box - display={'flex'} - alignItems={'center'} - gap={'5px'} - sx={{ cursor: 'pointer' }} - onClick={() => { - setParams(!params) - }} - > - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ПАРАМЕТРЫ - </Typography> - <svg - width='14' - height='20' - viewBox='0 0 21 13' - fill='none' - xmlns='http://www.w3.org/2000/svg' - className={`${params ? 'rotate-180' : 'rotate-0'}`} - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M0.614851 0.615358C1.00866 0.221668 1.54271 0.000505666 2.09955 0.000505642C2.6564 0.000505617 3.19044 0.221668 3.58425 0.615357L10.4996 7.53066L17.4149 0.615357C17.8109 0.232825 18.3414 0.0211567 18.892 0.0259414C19.4426 0.0307261 19.9693 0.25158 20.3587 0.640937C20.748 1.03029 20.9689 1.557 20.9737 2.10761C20.9784 2.65823 20.7668 3.18869 20.3842 3.58476L11.9843 11.9848C11.5904 12.3784 11.0564 12.5996 10.4996 12.5996C9.94271 12.5996 9.40866 12.3784 9.01485 11.9848L0.614851 3.58476C0.221162 3.19095 -4.3461e-07 2.6569 -4.5895e-07 2.10006C-4.8329e-07 1.54321 0.221162 1.00917 0.614851 0.615358Z' - fill='#7f7df3' - /> - </svg> - </Box> - )} - - {botParams && botParams.parameters?.length > 0 ? ( - <Collapse in={params} orientation='vertical' collapsedSize={0} sx={{}}> - <BotParamsMap currentVersion={version} params={botParams?.parameters} /> - <ResetFilters desktop={desktop} closeDrawer={() => setOpenFiltersMobile(false)} reset={resetParams} /> - </Collapse> - ) : ( - <Typography - sx={{ - color: '#6e6e6e', - fontSize: '15px', - fontWeight: '500', - }} - > - Параметры отсутствуют - </Typography> - )} - </Stack> - )} - <DrawerCustom open={openFiltersMobile} onClose={() => setOpenFiltersMobile(false)}> - <Stack spacing={1} padding={2.4}> - {botParams?.versions && botParams.versions.length !== 0 ? ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - margin: '20px 0px 0px !important', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - ) : ( - <></> - )} - {botParams && botParams.parameters?.length > 0 ? ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - margin: '20px 0px 0px !important', - }} - > - ПАРАМЕТРЫ - </Typography> - <BotParamsMap currentVersion={version} params={botParams?.parameters} /> - <ResetFilters closeDrawer={() => setOpenFiltersMobile(false)} desktop={desktop} reset={resetParams} /> - </> - ) : ( - <Typography - sx={{ - color: '#6e6e6e', - fontSize: '15px', - fontWeight: '500', - }} - > - Параметры отсутствуют - </Typography> - )} - </Stack> - </DrawerCustom> - </Box> - </> - ) -} - -export default ImageModelPage @@ -1,25 +0,0 @@ -.cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); - - align-items: flex-start; - justify-content: flex-start; - justify-items: stretch; - - @media screen and (max-width: 1000px) { - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - } - gap: 20px; - padding-top: 40px; - - margin-right: 60px; - - @media screen and (min-width: 1820px) { - margin-right: 240px; - grid-template-columns: repeat(auto-fit, 360px); - } - - @media screen and (max-width: 1000px) { - margin-right: 0px; - } -} @@ -1,60 +0,0 @@ -import * as React from 'react' -import { useEffect, useState } from 'react' -import { Box, Typography } from '@mui/material' -import CircularProgress from '@mui/material/CircularProgress' -import { useSession } from 'next-auth/react' - -import { useAppSelector } from '#/app/store/store' -import model_api from '#/shared/api/models/api' -import { NextPageWithLayout } from '#/pages/_app' -import { ImageModelCard, IShortModel } from '#/entities/model-entity' - -import styles from './image-models.module.scss' - -export const ImageModelsPage: NextPageWithLayout = () => { - const [bots, setBots] = useState<IShortModel[] | null>(null) - const user = useAppSelector((state) => state.user) - const { data, status } = useSession() - - useEffect(() => { - if (!data) return - - model_api.getImages(data.access).then((res) => setBots(res)) - }, [status]) - - return ( - <> - <Typography sx={{ fontSize: 24, fontWeight: 'bold', marginTop: '25px' }}> - Изображения - </Typography> - <div className={styles.cards}> - {bots ? ( - bots.map((item) => { - return ( - <ImageModelCard - accessed_models={user.payment_plan.plan.accessed_models} - {...item} - key={item.uid} - /> - ) - }) - ) : ( - <CircularProgress - size={50} - thickness={3} - sx={{ - color: '#7F7DF3', - position: 'absolute', - top: '40%', - left: 0, - right: 0, - margin: '0 auto', - }} - /> - )} - </div> - </> - ) -} - -export default ImageModelsPage @@ -1,2 +0,0 @@ -export { default as ImageModelsPage } from './image-models' -export { default as ImageModelPage } from './image-model' @@ -43,7 +43,6 @@ const Register: NextPageWithLayout = () => { if (localStorage.getItem('referral')) { setIsReferral(false) dispatch(addReferral(localStorage.getItem('referral'))) - console.log(localStorage.getItem('referral')) } else { setIsReferral(true) } @@ -126,10 +126,18 @@ export const SecurityList = ({ <Box className={styles.title2}> <Box> <Typography className={styles.name}>Сотрудники безопасности</Typography> - {persons?.length !== 0 && <Typography className={styles.count}>{persons?.length}</Typography>} - <ArrowUpOrDown directionY={showPersons} onClick={() => setShowPersons((prev) => !prev)} className={styles.arrow} /> + {persons?.length !== 0 && ( + <Typography className={styles.count}>{persons?.length}</Typography> + )} + <ArrowUpOrDown + directionY={showPersons} + onClick={() => setShowPersons((prev) => !prev)} + className={styles.arrow} + /> </Box> - <Typography onClick={() => setInviteModal(true)}>Добавить сотрудника</Typography> + <Typography onClick={() => setInviteModal(true)}> + Добавить сотрудника + </Typography> </Box> {showPersons && ( <Box className={styles.listWrap}> @@ -151,93 +159,162 @@ export const SecurityList = ({ <Table sx={{ minWidth: 650 }} aria-label='simple table'> <TableHead> <TableRow> - <TableCell align='left'>Пользователь</TableCell> + <TableCell align='left'> + Пользователь + </TableCell> <TableCell align='left'>Роль</TableCell> <TableCell align='left'>Статус</TableCell> - <TableCell align='left'>Лимит токенов</TableCell> + <TableCell align='left'> + Лимит токенов + </TableCell> <TableCell align='left'></TableCell> <TableCell align='left'></TableCell> <TableCell align='left'></TableCell> </TableRow> </TableHead> <TableBody> - {searchPersons?.map((person: ResponseGetPersons) => { - const statusPerson = translateEmailStatus(person.acceptance_status) - return ( - <TableRow - key={person.email} - sx={{ - '&:last-child td, &:last-child th': { border: 0 }, - }} - > - <TableCell className={styles.tableEmail} align='left'> - {formatEmail(person.email)} - </TableCell> - <TableCell align='left'>{RoleSelect[person.account_type]}</TableCell> - <TableCell align='left'> - <span> - {statusPerson + ' '} - {statusPerson === 'Приглашен' ? formatDateStatus(person.created_at) : null} - </span> - {statusPerson === 'Приглашен' ? ( + {searchPersons?.map( + (person: ResponseGetPersons) => { + const statusPerson = + translateEmailStatus( + person.acceptance_status + ) + return ( + <TableRow + key={person.email} + sx={{ + '&:last-child td, &:last-child th': + { border: 0 }, + }} + > + <TableCell + className={ + styles.tableEmail + } + align='left' + > + {formatEmail( + person.email + )} + </TableCell> + <TableCell align='left'> + { + RoleSelect[ + person + .account_type + ] + } + </TableCell> + <TableCell align='left'> + <span> + {statusPerson + ' '} + {statusPerson === + 'Приглашен' + ? formatDateStatus( + person.created_at + ) + : null} + </span> + {statusPerson === + 'Приглашен' ? ( + <button + className={ + styles.refreshStatusButton + } + onClick={() => { + passResendModal.setState( + true + ) + }} + > + <Image + src={ + '/svg/refresh-outline.svg' + } + width={14} + height={ + 14 + } + alt={ + 'Отправить повторно' + } + /> + Отправить + повторно + </button> + ) : null} + </TableCell> + <TableCell> + {Math.floor( + +person.token_limit + )} + </TableCell> + <TableCell + className={styles.change} + > <button - className={styles.refreshStatusButton} onClick={() => { - passResendModal.setState(true, { - callback: () => { - console.log('Смена пароля') - }, - }) + passChangeModal.setState( + true + ) }} > - <Image - src={'/svg/refresh-outline.svg'} - width={14} - height={14} - alt={'Отправить повторно'} - /> - Отправить повторно + Сменить пароль </button> - ) : null} - </TableCell> - <TableCell>{Math.floor(+person.token_limit)}</TableCell> - <TableCell className={styles.change}> - <button - onClick={() => { - passChangeModal.setState(true, { - callback: () => { - console.log('Смена пароля') - }, - }) - }} - > - Сменить пароль - </button> - </TableCell> + </TableCell> - <TableCell - onClick={() => - setCurrentPerson(persons?.find((el) => el.email === person.email) || null) - } - className={styles.tableLimit} - align='center' - > - <span className={styles.change}>Изменить</span> - </TableCell> - <TableCell className={styles.tableLimit} align='center'> - <XMark - setNewPersons={(persons) => - setPersons( - (prev) => - prev?.filter((el) => el.email !== persons?.email) || null + <TableCell + onClick={() => + setCurrentPerson( + persons?.find( + (el) => + el.email === + person.email + ) || null ) } - person={person} - /> - </TableCell> - </TableRow> - ) - })} + className={ + styles.tableLimit + } + align='center' + > + <span + className={ + styles.change + } + > + Изменить + </span> + </TableCell> + <TableCell + className={ + styles.tableLimit + } + align='center' + > + <XMark + setNewPersons={( + persons + ) => + setPersons( + (prev) => + prev?.filter( + ( + el + ) => + el.email !== + persons?.email + ) || + null + ) + } + person={person} + /> + </TableCell> + </TableRow> + ) + } + )} </TableBody> </Table> </TableContainer> @@ -246,7 +323,11 @@ export const SecurityList = ({ </Box> )} - <InviteSecurityModal open={inviteModal} onClose={() => setInviteModal(false)} showNewPersons={showNewPerson} /> + <InviteSecurityModal + open={inviteModal} + onClose={() => setInviteModal(false)} + showNewPersons={showNewPerson} + /> {currentPerson && ( <LimitModal list={securityList} @@ -277,8 +358,16 @@ export const InviteSecurityModal: FC<InviteModalProps> = ({ open, onClose, showN const onSubmit = async (data: any) => { setIsLoading(true) - const group: string[] | undefined = businessGroups?.map((el) => (el.title === currentGroup ? el.uid : '')) - const res = await invitePerson(role, data.limit, data.email, group && group[0], session?.access) + const group: string[] | undefined = businessGroups?.map((el) => + el.title === currentGroup ? el.uid : '' + ) + const res = await invitePerson( + role, + data.limit, + data.email, + group && group[0], + session?.access + ) if (res === null) { showMessage('Что-то пошло не так', 'error') @@ -319,7 +408,11 @@ export const InviteSecurityModal: FC<InviteModalProps> = ({ open, onClose, showN {/* Поле с выбором роли сотрудника */} <Typography className={styles2.hint}>Выберите роль</Typography> - <Select list={Object.values(securityRole)} onChange={(e) => setRole(e.target.value as InviteRoles)} value={role}></Select> + <Select + list={Object.values(securityRole)} + onChange={(e) => setRole(e.target.value as InviteRoles)} + value={role} + ></Select> {/* Поле с выбором группы */} <Typography className={styles2.hint}>Выберите бизнес-группу</Typography> @@ -332,10 +425,16 @@ export const InviteSecurityModal: FC<InviteModalProps> = ({ open, onClose, showN value={currentGroup} ></Select> - <Typography className={styles2.hint}>Укажите адрес электронной почты</Typography> + <Typography className={styles2.hint}> + Укажите адрес электронной почты + </Typography> <TextField fullWidth - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} + sx={ + theme === 'light' + ? { ...InputStyleLight } + : { ...InputStyleDark } + } {...register('email', emailOptions)} onChange={(e) => { handleEmailChange(e) @@ -350,7 +449,10 @@ export const InviteSecurityModal: FC<InviteModalProps> = ({ open, onClose, showN <Box> <ButtonGray onClick={onClose} text='Отмена' /> </Box> - <ButtonUI onClick={handleSubmit(onSubmit, checkError)} text='Отправить' /> + <ButtonUI + onClick={handleSubmit(onSubmit, checkError)} + text='Отправить' + /> </Box> )} </Box> @@ -0,0 +1,21 @@ +import { useChatBot } from '#/entities/model-entity' +import { getDeviceType } from '#/shared' +import { getPopupById, POPUP_CHAT_BOT_PARAMS } from '#/shared/ui/popup' +import { useState } from 'react' + +export const useChatModelOptions = () => { + const deviceType = getDeviceType() + const desktop = deviceType === 'desktop' + const [params, setParams] = useState(false) + const filtersMobilePopup = getPopupById(POPUP_CHAT_BOT_PARAMS) + const [openSelect, setOpenSelect] = useState(false) + + return { + desktop, + params, + setParams, + filtersMobilePopup, + openSelect, + setOpenSelect, + } +} @@ -0,0 +1,54 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; + padding: 30px; + background-color: var(--new-ui-main-color); + border-radius: 15px; + + &__heading { + color: var(--new-ui-gray-color); + font-weight: 600; + font-size: 14px; + letter-spacing: 0.1px; + text-transform: uppercase; + } + &__caption { + color: #6e6e6e; + font-size: 15px; + font-weight: 500px; + } + &__button { + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + + &_hidden { + display: none; + } + } + &__params { + display: flex; + flex-direction: column; + transition: max-height 0.25s; + max-height: 100dvh; + &_close { + max-height: 0px; + overflow: hidden; + } + } + + @media screen and (max-width: 1000px) { + padding: 20px; + + &__heading { + margin-top: 20px; + } + } +} + +.arrow { + color: #7f7df3; + transition: all 250ms; +} @@ -0,0 +1,59 @@ +import BotParamsMap from '#/features/bot-params/ui/bot-params-map' +import { c } from '#/shared' +import styles from './chat-model-options.module.scss' +import ArrowDownSvg from '#/assets/svg/arrow-down.svg?react' +import { HtmlHTMLAttributes, useState } from 'react' +import { useChatBot } from '#/entities/model-entity' +import { ModelParamsSelect } from '#/features/model-params-select' +import { ResetBotFilters } from '#/features/reset-bot-filters' + +interface ChatModelOptionsProps extends HtmlHTMLAttributes<HTMLDivElement> { + isMobile?: boolean +} + +export const ChatModelOptions = ({ className, isMobile = false }: ChatModelOptionsProps) => { + const [opened, setOpened] = useState(isMobile ? true : false) + + const { inference, botParams } = useChatBot() + + return ( + <div className={c(styles.container, className)}> + {botParams && botParams.inferences && ( + <> + <p className={styles.container__heading}>версии</p> + <ModelParamsSelect /> + </> + )} + {inference && inference.parameters.length > 0 ? ( + <> + <button + className={c( + styles.container__button, + isMobile && styles.container__button_hidden + )} + onClick={() => setOpened((x) => !x)} + > + <p className={styles.container__heading}>параметры</p> + <ArrowDownSvg + className={c(opened && 'rotate-180', styles.arrow)} + width='14' + height='20' + /> + </button> + + <div + className={c( + !opened && styles.container__params_close, + styles.container__params + )} + > + <BotParamsMap /> + <ResetBotFilters /> + </div> + </> + ) : ( + <p className={styles.container__caption}>Параметры отсутствуют</p> + )} + </div> + ) +} @@ -0,0 +1 @@ +export * from './chat-model-options' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,3 +1,37 @@ +.wrap { + display: flex; + flex-direction: column; + justify-content: space-between; + border-radius: 15px; + background-color: var(--new-ui-main-color); + padding: 0px 16px 10px; + height: 100%; + overflow: hidden; + + @media screen and (max-width: 1000px) { + border-radius: 13px; + padding: 0px 10px 10px; + height: calc(100dvh - 200px); + } + + &_tags { + @media screen and (max-width: 1000px) { + height: calc(100dvh - 240px); + } + } + + &__input{ + position: relative; + z-index: 10; + } +} + +// html[data-theme="dark"] { +// .wrap{ +// background-color: ; +// } +// } + .blocked { align-items: center; justify-content: center; @@ -1,8 +1,4 @@ -import React, { useCallback } from 'react' -import { Stack } from '@mui/material' -import axios from 'axios' - -import { useAppSelector } from '#/app/store/store' +import React, { useMemo } from 'react' import { ChatMessagesList } from '#/widgets/messages' import BlockedSvg from '#/assets/svg/blocked.svg?react' @@ -11,146 +7,58 @@ import styles from './chat-window.module.scss' import 'intro.js/introjs.css' import { c } from '#/shared' -import { Device, DeviceOs } from '#/shared/lib/types/entities' -import { IModelInputs, IModelTag } from '#/entities/model-entity' -import { Message } from '#/entities/message' +import { Model, useChatBotParams } from '#/entities/model-entity' import { ModelInput } from '#/features/model-input' +import { useShowDataStore, useThemeAndDevice } from '#/shared/lib/hooks' +import { useChatBotInput } from '#/features/chat-bot-input' +import { useChatImageLoad } from '#/features/chat-image-load' +import { useChatMessageActions } from '#/features/use-chat-message-actions' -interface Elements { - element: React.ReactElement - position: string - onClick?: () => void +export interface ChatProps { + setOpenFiltersMobile: React.Dispatch<React.SetStateAction<boolean>> } -export interface ChatProps<T> { - device: Device - deviceOs?: DeviceOs - modelType: string - messages: Message[] - sendMessage: (message: string, required: (string | null)[]) => boolean - openMobileFilters: () => void - setting?: T - loading: boolean - file?: any - setFile: React.Dispatch<React.SetStateAction<File | null>> - isCalculating: boolean - elements?: Elements[] - model?: string - blocked?: boolean - getMessagesPagination?: () => Promise<void> - input_types?: IModelInputs[] - deleteMessage: (message_uid: string) => void - modelTitle: string | undefined - currentVersion: string - tags: IModelTag[] -} +export function AllChatWindow<T>({ setOpenFiltersMobile }: ChatProps) { + const { botParams: model, inference, inferenceInputs: inputs } = useChatBotParams() -function Chat<T>({ - device, - loading, - messages, - modelType, - sendMessage, - openMobileFilters, - setFile, - file, - getMessagesPagination, - input_types, - deleteMessage, - modelTitle, - blocked, - currentVersion, - deviceOs, - tags, -}: ChatProps<T>) { - const desktop = device === 'desktop' + const { sendMessage } = useChatMessageActions() - const theme = useAppSelector((state) => state.theme.theme) - const [resendValue, setResendValue] = React.useState<string>('') + const { onSendMessage, file, setFile } = useChatBotInput(inference, sendMessage) - const onLoadImage = useCallback((event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => { - if (event?.target.files) { - setFile(event.target.files[0]) - } - if (file) { - setFile(file) - } - }, []) + const { showMessage } = useShowDataStore() - const handleSetResetValue = useCallback( - (value: string) => { - if (resendValue === value) { - setResendValue((prev) => prev + ' ') - } else { - setResendValue(value) - } - }, - [resendValue] - ) + const inputTypes = useMemo(() => inputs.map((p) => p.type), [inputs]) - const clearImage = () => { - setFile(null) - } + const requiredTypes = useMemo( + () => inputs.filter((p) => p.required).map((p) => p.type), + [inputs] + ) return ( <> - <Stack - className='tutorial-chat-gpt' - direction='column' - justifyContent='space-between' - alignItems='stretch' - spacing={0} - sx={{ - borderRadius: desktop ? '15px' : '13px', - backgroundColor: desktop ? (theme === 'light' ? 'white' : '#151518') : theme === 'light' ? 'white' : '#151518', - padding: desktop ? (blocked ? 2 : 2) : 1.25, - paddingTop: 0, - paddingBottom: 1.25, - width: '100%', - height: desktop ? '75vh' : `calc(100dvh - ${tags.length == 0 ? '200px' : '240px'})`, - overflow: 'hidden', - }} - > - <ChatMessagesList - onLoadImage={onLoadImage} - setResendValue={handleSetResetValue} - device={device} - getMessagesPagination={getMessagesPagination} - messageResponse={messages} - modelType={modelType} - deleteMessage={deleteMessage} - modelTitle={modelTitle} - loading={loading} - /> + <div className={c(styles.wrap, model && !!model.tags.length && styles.wrap_tags)}> + <ChatMessagesList model={model} /> - <div style={{ position: 'relative', zIndex: 10 }}> + <div className={c(styles.wrap__inputx)}> <ModelInput - resendValue={resendValue} - currentVersion={currentVersion} - viewMobileSettings={openMobileFilters} - input_types={input_types} - styles={'chats'} - loading={loading} - desktop={desktop} - blocked={blocked} - sendMessage={sendMessage} - image={file} - unpinImage={clearImage} - imageLoad={onLoadImage} + file={file} + setFile={setFile} + viewMobileSettings={() => setOpenFiltersMobile((x) => !x)} + inputTypes={inputTypes} + blocked={model ? !model.enabled : false} + sendMessage={(message) => onSendMessage(message, requiredTypes)} /> - {blocked && ( + {(model ? !model.enabled : false) && ( <div className={styles.blocked}> <BlockedSvg width={21} height={21} color='white' /> - <span className={c(styles.blocked__text)}>Модель недоступна</span> + <span className={c(styles.blocked__text)}> + Модель недоступна + </span> </div> )} </div> - </Stack> + </div> </> ) } - -Chat.displayName = 'Chat' - -export const AllChatWindow = Chat @@ -0,0 +1,76 @@ +.chats { + display: flex; + align-items: center; + justify-content: space-between; + + &__title { + color: #a4aab5; + font-weight: 600; + font-size: 14px; + letter-spacing: 0px; + + @media screen and (max-width: 1000px) { + display: none; + } + } + + &__create { + background-color: var(--new-ui-main-color); + padding: 9px 11px; + border-radius: 10px; + margin-top: 3px; + position: relative; + z-index: 10; + } +} + +.wrap { + width: 100%; + background-color: var(--new-ui-main-color); + border-radius: 15px; + padding: 30px; + height: 100%; + display: flex; + flex-direction: column; + + @media screen and (max-width: 1000px) { + margin: 15px 0 0; + align-items: center; + display: flex; + } + + @media (max-width: 1000px) { + padding: 10px 20px; + display: flex; + flex-direction: row-reverse; + } +} + +.controls { + height: 100%; + position: sticky; + overflow-y: scroll; + display: flex; + flex-direction: column; + overflow-x: hidden; + overflow: scroll; + width: 100%; + + @media (max-width: 1000px) { + display: flex; + height: 100%; + flex-direction: row; + overflow-x: scroll; + overflow-y: hidden; + } + + &__item { + padding: 0; + border: none; + width: max-content; + + &:hover { + background-color: transparent; + } + } +} @@ -0,0 +1,49 @@ +import React, { HtmlHTMLAttributes, useEffect } from 'react' + +import { useChats } from '#/features/chats' +import { c } from '#/shared' + +import styles from './chats.module.scss' +import { CommonTooltip } from '#/shared/ui/tooltip' +import PlusSvg from '#/assets/svg/plus.svg?react' +import { ChatButton, useChatActions } from '#/entities/chat' +import { useChatBotParams } from '#/entities/model-entity' +import { CommonScrollbar } from '#/shared/ui/scrollbar' + +export interface ChatProps extends HtmlHTMLAttributes<HTMLDivElement> {} + +export const Chats = ({ className, ...props }: ChatProps) => { + const { chats, fetchChats } = useChats() + + const { onCreateChat } = useChatActions() + + const { botParams } = useChatBotParams() + + useEffect(() => { + fetchChats() + }, []) + + return ( + <div className={c(styles.wrap, className)} {...props}> + <div className={styles.chats}> + <p className={styles.chats__title}>ЧАТЫ</p> + <CommonTooltip content='Создать чат'> + <button + onClick={() => { + if (!botParams) return + onCreateChat({ model: botParams.slug, title: 'Новый чат' }) + }} + className={styles.chats__create} + > + <PlusSvg width={18} height={18} color='#A4AAB5' /> + </button> + </CommonTooltip> + </div> + <div className={c(styles.controls)}> + {chats.map((chat) => ( + <ChatButton key={chat.uid} {...chat} /> + ))} + </div> + </div> + ) +} @@ -0,0 +1 @@ +export * from './chats' \ No newline at end of file @@ -1,86 +0,0 @@ -.title { - @media screen and (max-width: 768px) { - display: none; - } -} - -.wrap { - width: 100%; - background-color: var(--new-ui-main-color); - border-radius: 15px; - padding: 30px; - height: fit-content; - //margin-bottom: 15px; - max-height: 350px; - overflow-y: auto; - - @media (max-width: 768px) { - padding: 10px 20px; - display: flex; - flex-direction: row-reverse; - } - - .container { - display: block; - - @media (max-width: 768px) { - display: flex; - overflow-x: scroll; - scrollbar-width: none; - width: 100%; - -ms-overflow-style: none; - &::-webkit-scrollbar { - width: 2px; - } - } - - .item { - padding: 0; - border: none; - width: 100%; - - &:hover { - background-color: transparent; - } - - .main { - display: flex; - width: 100%; - border: none; - justify-content: space-between; - background-color: transparent; - border-radius: 13px; - padding: 12px 15px; - text-transform: none; - - &_active { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - border: none; - border-radius: 13px; - padding: 12px 15px; - text-transform: none; - background-color: rgba(130, 128, 255, 0.15); - - @media (max-width: 768px) { - margin-right: 10px; - // margin-top: 10px; - width: 125px; - max-width: 130px; - overflow: hidden; - padding: 8px 12px; - justify-content: space-between; - } - } - - @media (max-width: 768px) { - // margin-top: 10px; - width: 100px; - padding: 0; - } - } - } - } -} @@ -1,338 +0,0 @@ -import React from 'react' -import { - Box, - Input, - Menu, - MenuItem, - Popover, - TextField, - ToggleButton, - ToggleButtonGroup, - Typography, -} from '@mui/material' -import axios from 'axios' -import Image from 'next/image' -import { useSession } from 'next-auth/react' - -import { Chat, ChatsReturn } from '#/features/chats' -import { useAppSelector } from '#/app/store/store' -import styles2 from '#/app/styles/account.module.css' -import { TooltipCustom } from '#/shared' -import { api } from '#/shared/api/endpoints' - -import styles from './chats.module.scss' - -export const Chats = ({ - chats, - currentChat, - removeChat, - setChatSetting, - setChat, - chatSetting, - createNewChat, - handleClickChatSetting, - isTryRename, - setIsTryRename, - desktop, -}: ChatsReturn) => { - const theme = useAppSelector((state) => state.theme.theme) - - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const open = Boolean(anchorEl) - const [localChat, setLocalChat] = React.useState<string>('') - const [rename, setRename] = React.useState<boolean>(false) - const [newTitle, setNewTitle] = React.useState<string>('') - const [localChats, setLocalChats] = React.useState<Chat[] | null>(null) - const { data } = useSession() - const inputElement = React.useRef(null) - - const handleClick = (event: React.MouseEvent<HTMLElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - - React.useEffect(() => { - setLocalChats(chats) - }, [chats]) - - React.useEffect(() => { - if (currentChat) setLocalChat(currentChat) - }, [currentChat]) - - React.useEffect(() => { - if (inputElement.current) { - // @ts-ignore - inputElement.current.getElementsByTagName('input')[0].focus() - } - }, [rename]) - - const renameChat = () => { - if (newTitle !== '' && newTitle !== 'Новый чат') { - api.updRenameChat(localChat, newTitle, data?.access).then((res) => { - if (res.data && chats) - setLocalChats( - chats.filter((el) => (el.uid === res.data.uid ? (el.title = res.data.title) : el)) - ) - setNewTitle('') - }) - } - } - - return ( - <Box className={styles.wrap} sx={{ margin: desktop ? '0 0 15px 0' : '15px 0 0' }}> - <Box display='flex' alignItems='center' justifyContent='space-between'> - <Typography - className={styles.title} - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ЧАТЫ - </Typography> - <TooltipCustom title='Создать чат'> - <Box - onClick={createNewChat} - sx={{ - backgroundColor: theme !== 'dark' ? 'white' : '#151518', - padding: '9px 11px', - borderRadius: '10px', - cursor: 'pointer', - }} - > - <Image src={'/plus.svg'} style={{ marginTop: '3px' }} width={18} height={18} alt={''} /> - </Box> - </TooltipCustom> - </Box> - <ToggleButtonGroup - className={styles.container} - color='primary' - exclusive - aria-label='chats' - sx={{ - overflowY: 'scroll', - maxHeight: '210px', - '::-webkit-scrollbar': { - height: '5px', - width: '2px', - }, - '::-webkit-scrollbar-track': { - background: 'initial', - margin: '0', - }, - '::-webkit-scrollbar-thumb': { - backgroundColor: 'rgba(217, 217, 217, 0.49)', - borderRadius: '5px', - }, - }} - > - {!(localChats === null || localChats?.length <= 0) && - localChats?.map((el) => { - return ( - <ToggleButton - onClick={() => { - if (currentChat !== el.uid && !open) { - setChat(el?.uid) - setLocalChat(el.uid) - setRename(false) - } - }} - disableRipple - key={el?.uid} - className={styles.item} - value={el.uid} - > - {el.uid === localChat && rename ? ( - <TextField - ref={inputElement} - fullWidth - label={''} - placeholder={el.title} - value={newTitle} - onChange={(e) => setNewTitle(e.target.value)} - variant='outlined' - sx={{ - '& .MuiInputBase-input': { - padding: '12px 5px 12px 15px', - borderRadius: '13px', - outline: 'none', - width: '100%', - color: '#A4AAB5', - fontSize: '15px', - fontWeight: '500', - }, - '& .MuiInputBase-root': { - borderRadius: '13px !important', - backgroundColor: 'rgba(130, 128, 255, 0.15)', - }, - '& .MuiFormControl-root-MuiTextField-root': { - borderRadius: '13px', - }, - }} - InputProps={{ - endAdornment: ( - <Box - sx={{ cursor: 'pointer' }} - display={'flex'} - alignItems={'center'} - onClick={() => { - setRename(false) - renameChat() - }} - > - <svg - width='20' - height='22' - viewBox='0 0 20 22' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - d='M15.8464 21.75H3.41786C2.51138 21.75 1.64204 21.3899 1.00107 20.7489C0.360093 20.1079 0 19.2386 0 18.3321V3.41786C0 2.51138 0.360093 1.64204 1.00107 1.00107C1.64204 0.360093 2.51138 0 3.41786 0H12.7393C12.9864 0.000223714 13.2234 0.098571 13.398 0.273429L18.9909 5.90357C19.1657 6.07822 19.264 6.31514 19.2643 6.56229V18.3321C19.2643 19.2386 18.9042 20.1079 18.2632 20.7489C17.6222 21.3899 16.7528 21.75 15.8464 21.75ZM3.41786 1.86429C3.00583 1.86429 2.61067 2.02797 2.31932 2.31932C2.02797 2.61067 1.86429 3.00583 1.86429 3.41786V18.3321C1.86429 18.7441 2.02797 19.1394 2.31932 19.4307C2.61067 19.722 3.00583 19.8857 3.41786 19.8857H15.8464C16.2584 19.8857 16.6537 19.722 16.945 19.4307C17.2363 19.1394 17.4 18.7441 17.4 18.3321V6.91029L12.354 1.86429H3.41786Z' - fill='#8280FF' - /> - <path - d='M15.5357 20.8177H13.6714V13.0498H5.5928V20.8177H3.72852V12.7391C3.72852 12.3271 3.8922 11.9319 4.18355 11.6406C4.4749 11.3492 4.87006 11.1855 5.28209 11.1855H13.9821C14.3941 11.1855 14.7893 11.3492 15.0806 11.6406C15.372 11.9319 15.5357 12.3271 15.5357 12.7391V20.8177Z' - fill='#8280FF' - /> - <path - d='M10.2163 6.83619H5.31942C5.10887 6.83456 4.9007 6.79148 4.7068 6.70941C4.51291 6.62733 4.33708 6.50785 4.18935 6.35783C4.04163 6.20779 3.9249 6.03012 3.84584 5.83498C3.76677 5.63983 3.72692 5.43103 3.72856 5.22047V0.932617H5.59285V4.9719H9.94284V0.932617H11.8071V5.22047C11.8087 5.43103 11.769 5.63983 11.6899 5.83498C11.6108 6.03012 11.494 6.20779 11.3464 6.35783C11.1986 6.50785 11.0228 6.62733 10.8289 6.70941C10.635 6.79148 10.4268 6.83456 10.2163 6.83619Z' - fill='#8280FF' - /> - </svg> - </Box> - ), - }} - /> - ) : ( - <Box - className={ - el?.uid === currentChat - ? styles.main_active - : styles.main - } - > - <Typography - className={ - el?.uid === currentChat - ? styles2.toggle_text_active_chat - : styles2.toggle_text - } - > - {el?.title} - </Typography> - - {el?.uid === currentChat && ( - <Image - onClick={handleClick} - aria-describedby={ - Boolean(chatSetting) ? 'simple-popover' : undefined - } - src={'/svg/chat-setting.svg'} - alt={'Настройка чата'} - width={18} - height={18} - /> - )} - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === 'dark' ? '#303035' : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === 'dark' ? '#303035' : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - setRename(true) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 54 39' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - d='M19.5 2H32M32 2H44.5M32 2V37M32 37H19.5M32 37H44.5' - stroke='#8280FF' - strokeWidth='2.5' - /> - <path - d='M24.5 12H7C4.23857 12 2 14.2386 2 17V27H24.5' - stroke='#8280FF' - strokeWidth='2.5' - /> - <path - d='M39.5 27H47C49.7615 27 52 24.7615 52 22V12H39.5' - stroke='#8280FF' - strokeWidth='2.5' - /> - </svg> - Переименовать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '12px', - }} - onClick={() => { - removeChat(localChat) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - )} - </ToggleButton> - ) - })} - </ToggleButtonGroup> - </Box> - ) -} @@ -1 +1 @@ -export { Chats } from './chats' +export * from './ui' \ No newline at end of file @@ -1,4 +1,4 @@ -import { IShortModel } from '#/entities/model-entity' +import { ShortModel } from '#/entities/model-entity' import React, { useMemo } from 'react' import styles from './image-popup-model.module.scss' @@ -8,7 +8,7 @@ import ClockSvg from '#/assets/svg/clock.svg?react' import LightningSvg from '#/assets/svg/lightning.svg?react' import LockSvg from '#/assets/svg/lock.svg?react' -interface ImagePopupModelProps extends IShortModel { +interface ImagePopupModelProps extends ShortModel { active: boolean setActive: (slug: string) => void averageTokenCost: number @@ -29,34 +29,33 @@ export const ImagePopupModel = ({ title, description, slug, - blocked, - actual_stat, + enabled, active, averageTokenCost, setActive, }: ImagePopupModelProps) => { // жесткий костыль))) - временный - const formatedTime = useMemo(() => { - if (!actual_stat) return null + // const formatedTime = useMemo(() => { + // if (!actual_stat) return null - const seconds = Number(actual_stat.generation_time.split(':')[2]) + // const seconds = Number(actual_stat.generation_time.split(':')[2]) - const range = Math.ceil(seconds - seconds / 3) + // const range = Math.ceil(seconds - seconds / 3) - return `${range} - ${seconds} сек.` - }, [actual_stat]) + // return `${range} - ${seconds} сек.` + // }, [actual_stat]) - const costZone = useMemo(() => { - if (!actual_stat) return null + // const costZone = useMemo(() => { + // if (!actual_stat) return null - const remainder = Number(actual_stat.tokens_cost) - averageTokenCost + // const remainder = Number(actual_stat.tokens_cost) - averageTokenCost - return costZoneColors.find((item) => item.remainder > remainder) - }, [actual_stat, averageTokenCost]) + // return costZoneColors.find((item) => item.remainder > remainder) + // }, [actual_stat, averageTokenCost]) return ( - <div onClick={() => !blocked && setActive(slug)} className={styles.model}> - {blocked && ( + <div onClick={() => enabled && setActive(slug)} className={styles.model}> + {!enabled && ( <div className={c(styles.model__blocked)}> <LockSvg color='#8280FF' width={20} height={20} /> <span>Недоступно в текущем тарифе</span> @@ -67,7 +66,7 @@ export const ImagePopupModel = ({ <p className={styles.model__title}>{title}</p> <p className={styles.model__description}>{description}</p> - {actual_stat && ( + {/* {actual_stat && ( <div className={styles.model__badges}> <div className={c(styles.model__badge, styles.model__time)}> <ClockSvg width={16} height={16} /> @@ -82,7 +81,7 @@ export const ImagePopupModel = ({ {Number(actual_stat.tokens_cost)} </div> </div> - )} + )} */} </div> {active && <SuccessRoundedSvg width={24} height={24} />} @@ -1,10 +1,10 @@ -import { Device } from "#/shared/lib/types/entities" - +import { Device } from '#/shared/lib/types/entities' export interface LayoutProps { - children: React.ReactNode - titlePage?: string - title?: string | React.ReactNode | null - device?: Device - isLoader?: boolean -} \ No newline at end of file + children: React.ReactNode + titlePage?: string + title?: string | React.ReactNode | null + device?: Device + isLoader?: boolean + height?: string +} @@ -23,13 +23,22 @@ import { LayoutProps } from '../types' import { change } from '#/features/pending' import { ChangePasswordPlate } from '#/features/change-password-corp' import { ResendInvationPlate } from '#/features/resend-invation-corp ' +import { PopupBackdrop } from '#/shared/ui/popup' import { ErrorReportPlate } from '#/features/error-report' import { CSSTransition, SwitchTransition } from 'react-transition-group' import { CommonButton } from '#/shared/ui/button' import styles from './default.module.scss' import { SlowLoading } from '#/features/slow-loading' - -export const Layout: React.FC<LayoutProps> = ({ children, titlePage, title = titlePage, isLoader, device = getDeviceType() }) => { +import { ChatBotOptionsPopup } from '#/entities/chat/ui/chat-bot-options-popup' + +export const Layout: React.FC<LayoutProps> = ({ + children, + titlePage, + title = titlePage, + isLoader, + height, + device = getDeviceType(), +}) => { const { data: sessionData } = useSession() const appState = useAppSelector((state) => state) const dispatch = useAppDispatch() @@ -78,7 +87,10 @@ export const Layout: React.FC<LayoutProps> = ({ children, titlePage, title = tit }, [data]) useEffect(() => { - if ((sessionData && !sessionStorage.getItem('firstRender')) || (sessionData && !localStorage.getItem('global_settings'))) { + if ( + (sessionData && !sessionStorage.getItem('firstRender')) || + (sessionData && !localStorage.getItem('global_settings')) + ) { dispatch(getUserAccountSettings(sessionData.access)) sessionStorage.setItem('firstRender', 'true') } @@ -103,7 +115,10 @@ export const Layout: React.FC<LayoutProps> = ({ children, titlePage, title = tit targetDevice: device, targetType: 'sidemenu', }) - if (setting) setting?.value?.sidemenu_state === 'opened' ? setSidemenuDefaultOpen(true) : setSidemenuDefaultOpen(false) + if (setting) + setting?.value?.sidemenu_state === 'opened' + ? setSidemenuDefaultOpen(true) + : setSidemenuDefaultOpen(false) } }, [appState.settings.state]) @@ -148,7 +163,7 @@ export const Layout: React.FC<LayoutProps> = ({ children, titlePage, title = tit id={'layout'} sx={{ width: '100%', - height: desktop ? '100%' : '100dvh', + height: desktop ? (height ? height : '100%') : '100dvh', padding: desktop ? '0px' : '10px', }} > @@ -159,7 +174,10 @@ export const Layout: React.FC<LayoutProps> = ({ children, titlePage, title = tit > {desktop ? ( <Box display='flex' justifyContent='center'> - <SideMenu device={device} sidemenuDefaultOpen={sidemenuDefaultOpen} /> + <SideMenu + device={device} + sidemenuDefaultOpen={sidemenuDefaultOpen} + /> <Box width='100%' sx={{ @@ -169,7 +187,10 @@ export const Layout: React.FC<LayoutProps> = ({ children, titlePage, title = tit }} > <InfoBar device={device} token={''} /> - <Box sx={{ paddingBottom: '15px' }} width='100%'> + <Box + sx={{ paddingBottom: '15px' }} + width='100%' + > {children} </Box> </Box> @@ -184,7 +205,9 @@ export const Layout: React.FC<LayoutProps> = ({ children, titlePage, title = tit <ChangePasswordPlate /> <ResendInvationPlate /> + <PopupBackdrop /> <ErrorReportPlate /> + <ChatBotOptionsPopup /> </Box> </> ) @@ -2,13 +2,15 @@ import React from 'react' import ReactMarkdown from 'react-markdown' import dynamic from 'next/dynamic' import remarkGfm from 'remark-gfm' +import { useThemeAndDevice } from '#/shared/lib/hooks' interface IProps { content: string - theme: 'light' | 'dark' } -export const Markdown = ({ content, theme }: IProps) => { +export const Markdown = ({ content }: IProps) => { + const { theme } = useThemeAndDevice() + const LazyCode = dynamic(() => import('#/widgets/chat-gpt-field/ui/code')) return ( @@ -32,7 +34,11 @@ export const Markdown = ({ content, theme }: IProps) => { </> ) : ( <> - <code style={{ borderRadius: 15 }} {...props} className={className}> + <code + style={{ borderRadius: 15 }} + {...props} + className={className} + > {children} </code> </> @@ -1,6 +1,6 @@ import { MessageSend } from '#/entities/message' -export function formDataHelper(file: File, dataForSend: MessageSend<any>): FormData { +export function formDataHelper(file: File, dataForSend: MessageSend): FormData { const FD = new FormData() FD.append('file', file) FD.append('info', JSON.stringify(dataForSend.info)) @@ -1,23 +0,0 @@ -import React from 'react' -import { Box } from '@mui/material' -import Link from 'next/link' - -import { Message } from '#/shared/lib/types/model' - -export function AnswerWrap(props: { message: Message }) { - if (props.message.file) { - const messageText = props.message.from_model - ? 'Ваша ссылка для скачивания \n (нажмите для просмотра)' - : 'Тут ссылка на ваш файл (нажмите для просмотра)' - - return ( - <Box> - <Link onClick={(e) => e.stopPropagation()} target='_blank' href={props.message.file.toString()}> - {messageText} - </Link> - </Box> - ) - } - - return <>{props.message.content}</> -} @@ -1,44 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' - -const previewMessages = ['Что такое орбитальная механика?', 'Как приготовить омлет?', 'Кто такие Фиксики?'] - -export function PreviewView(props: any) { - return ( - <Box className='content-center-translate'> - <Typography className='text'>Не знаете, с чего начать?</Typography> - <Typography className='text' sx={{ marginBottom: '15px' }}> - Попробуйте, например, вот так: - </Typography> - <Box - sx={{ - textAlign: 'center', - width: '100%', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }} - > - {previewMessages.map((el) => { - return ( - <Box - key={el} - onClick={() => props.setValue(el)} - sx={{ - cursor: 'pointer', - padding: '15px', - color: '#8280FF', - width: 'fit-content', - marginTop: '15px', - backgroundColor: 'rgba(130, 128, 255, 0.10)', - borderRadius: '12px', - }} - > - {el} - </Box> - ) - })} - </Box> - </Box> - ) -} @@ -1,933 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material' -import Stack from '@mui/material/Stack' -import Image from 'next/image' - -import FullScreenModal from '#/features/image-modal/full-screen-modal' -import { useAppSelector } from '#/app/store/store' -import { TooltipCustom } from '#/shared' -import { Message } from '#/shared/lib/types/model' -import { BotMessage } from '#/widgets/messages' -import { Markdown } from '../../markdown/markdown' -import { formatDate } from '#/shared/lib/helpers' - -interface IMessagesList { - // messageResponse: Message[] | null - // mode?: string - // sendMessage?: (title: string) => void - // getMessagesPagination?: () => Promise<void> - - device: 'mobile' | 'desktop' - modelType: string - isNewMessage: boolean - message: Message - deleteMessage?: (message_uid: string) => void - modelTitle: string | undefined - setResendValue: (value: string) => void - onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void -} - -export const UserMessage = React.memo(function UserMessage(props: IMessagesList) { - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const open = Boolean(anchorEl) - const desktop = props.device === 'desktop' - const [modal, setModal] = useState<boolean>(false) - const [loaded, setLoaded] = useState(false) - - const copy = (text: string) => { - navigator.clipboard.writeText(text) - } - const handleClick = (event: React.MouseEvent<HTMLElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - - const fileName = useMemo(() => { - if (!props.message.file) return 'Подбираем имя файла...' - const matches = (props.message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/) - if (!matches) return 'Подбираем имя файла...' - return `${matches[1]}.${matches[2]}` - }, []) - - const resend = async () => { - props.setResendValue(props.message.content) - if (props.message.file !== null) { - fetch(props.message.file as string, { - method: 'GET', - headers: {}, - }).then((response) => { - response.arrayBuffer().then(function (buffer) { - const url = new Blob([buffer]) - const binaryFile = new File([url], 'image.png') - if (props.onLoadImage) { - props.onLoadImage(null, binaryFile) - } - }) - }) - } - } - - useEffect(() => { - console.log(props.message.file) - }, [props.message.file]) - - const flexStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'start', - gap: '12px', - color: '#7f7df3 !important', - fontWeight: '600 !important', - lineHeight: '140%', - letterSpacing: '0.2px', - cursor: 'pointer', - } - - const theme = useAppSelector((state) => state.theme.theme) - - return ( - <> - {props.message.file && ( - <FullScreenModal - modal={modal} - setModal={setModal} - image={props.message.file.toString()} - /> - )} - <span className='tutorial-message'> - {!props.message.from_model ? ( - // <Slide className='tutorial-message-me' direction='left' in={props.isNewMessage} mountOnEnter unmountOnExit> - <Box> - <Box - className='smallScroll' - sx={{ - width: 'fit-content', - marginLeft: 'auto', - maxWidth: desktop ? '68%' : '100%', - marginTop: 2, - marginBottom: 2, - }} - > - <Stack direction='row' justifyContent='end' alignItems='center'> - <Typography - variant='body2' - sx={{ - color: theme === 'light' ? '#868686' : '#A6A5A5', - lineHeight: '19.6px', - fontSize: '13px', - fontWeight: '600', - marginLeft: 1, - }} - > - {formatDate(props.message.created_at, { - hour: 'numeric', - minute: 'numeric', - })} - </Typography> - </Stack> - - <Box display='flex' alignItems='center' justifyContent='end'> - {props.message.file ? ( - /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg\.wav/.test( - props.message.file as string - ) ? ( - <Box - sx={{ - display: 'flex', - alignItems: 'end', - flexDirection: 'column', - height: '60%', - width: '60%', - }} - > - <Box sx={{ position: 'relative' }}> - <Image - onClick={() => { - if (!props.message.file) - return - setModal(true) - }} - onLoadingComplete={() => - setLoaded(true) - } - style={{ - objectFit: 'contain', - height: '100%', - width: '100%', - borderRadius: '13px', - cursor: 'pointer', - opacity: loaded - ? '100%' - : '0%', - minWidth: '100px', - minHeight: '100px', - }} - width={500} - height={500} - src={ - props.message.file - .toString() - .split('?type')[0] - } - alt={ - 'К сожалению, изображение не загрузилось' - } - /> - {!loaded && ( - <Skeleton - sx={{ - background: - theme === 'dark' - ? '#2d2d2f' - : '#EFF0F2', - position: 'absolute', - top: '0', - left: '0', - zIndex: '1', - borderRadius: 5, - width: '100%', - height: '100%', - }} - variant='rectangular' - /> - )} - </Box> - <Box display='flex' alignItems='center'> - <Box - marginRight={1} - sx={{ cursor: 'pointer' }} - > - <Box - onClick={handleClick} - aria-controls={ - open - ? 'message-menu' - : undefined - } - aria-haspopup='true' - aria-expanded={ - open ? 'true' : undefined - } - > - <TooltipCustom - title={'Открыть меню'} - > - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === - 'dark' - ? '#303035' - : '#EFF0F2', - color: '#8280FF', - borderRadius: - '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === - 'dark' - ? '#303035' - : '#EFF0F2', - borderRadius: - '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: - 'center', - gap: '10px', - }} - onClick={() => { - copy( - props.message - .content - ) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: - 'center', - gap: '5px', - }} - onClick={() => { - if ( - props.deleteMessage - ) - props.deleteMessage( - props - .message - .uid - ) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={ - '/svg/important.svg' - } - /> - )} - </TooltipCustom> - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: - props.message.content - .length > 30 - ? 'pre-wrap' - : 'pre', - }} - > - {/*<Markdown content={props.message.content} />*/} - {props.message.content} - </Typography> - </Box> - </Box> - ) : ( - <Box - sx={{ - display: 'flex', - alignItems: 'end', - flexDirection: 'column', - gap: '5px', - }} - > - <Box display={'flex'}> - <Box - onClick={() => { - if (props.message.file) { - window.open( - props.message - .file as string, - '_blank' - ) - } - }} - className='smallScroll' - sx={{ - overflowY: 'scroll', - position: 'relative', - padding: '15px 23px', - - border: `1px solid ${ - theme === 'dark' - ? '#303035' - : '#EFF0F2' - }`, - - boxShadow: 'none', - fontSize: '15px', - marginTop: 0.5, - textAlign: 'left', - fontFamily: - 'Raleway,sans-serif', - borderRadius: '13px', - '& p': { - color: 'inherit', - fontStyle: 'inherit', - }, - ...flexStyle, - }} - > - <Box - sx={{ - width: '16px', - height: '18px', - }} - > - <svg - width='16' - height='18' - viewBox='0 0 15 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M10.3286 0.578662C9.95416 0.20795 9.44851 0 8.92158 0H2C0.895433 0 0 0.895431 0 2V17C0 18.1046 0.895431 19 2 19H13C14.1046 19 15 18.1046 15 17V6.03743C15 5.50349 14.7865 4.99173 14.4071 4.61609L10.3286 0.578662ZM10.0669 5.37846C9.79079 5.37846 9.56693 5.1546 9.56693 4.87846V2.19238C9.56693 1.74815 10.103 1.52452 10.4187 1.83705L13.132 4.52313C13.4495 4.83737 13.227 5.37846 12.7803 5.37846H11.7815H10.0669Z' - fill='#7f7df3' - /> - </svg> - </Box> - - <Markdown - content={fileName} - theme={theme} - /> - </Box> - </Box> - <Box display='flex' alignItems='center'> - <Box - marginRight={1} - sx={{ cursor: 'pointer' }} - > - <Box - onClick={handleClick} - aria-controls={ - open - ? 'message-menu' - : undefined - } - aria-haspopup='true' - aria-expanded={ - open ? 'true' : undefined - } - > - <TooltipCustom - title={'Открыть меню'} - > - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === - 'dark' - ? '#303035' - : '#EFF0F2', - color: '#8280FF', - borderRadius: - '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === - 'dark' - ? '#303035' - : '#EFF0F2', - borderRadius: - '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: - 'center', - gap: '10px', - }} - onClick={() => { - copy( - props.message - .content - ) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: - 'center', - gap: '5px', - }} - onClick={() => { - if ( - props.deleteMessage - ) - props.deleteMessage( - props - .message - .uid - ) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={ - '/svg/important.svg' - } - /> - )} - </TooltipCustom> - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: - props.message.content - .length > 30 - ? 'pre-wrap' - : 'pre', - }} - > - {/*<Markdown content={props.message.content} />*/} - {props.message.content} - </Typography> - </Box> - </Box> - ) - ) : ( - <Box display='flex' alignItems='center'> - <Box marginRight={1} sx={{ cursor: 'pointer' }}> - <Box - onClick={handleClick} - aria-controls={ - open ? 'message-menu' : undefined - } - aria-haspopup='true' - aria-expanded={ - open ? 'true' : undefined - } - > - <TooltipCustom title={'Открыть меню'}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === 'dark' - ? '#303035' - : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === 'dark' - ? '#303035' - : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) - props.deleteMessage( - props.message.uid - ) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: - props.message.content.length > 30 - ? 'pre-wrap' - : 'pre', - }} - > - {/*<Markdown content={props.message.content} />*/} - {props.message.content} - </Typography> - </Box> - )} - </Box> - </Box> - </Box> - ) : ( - <BotMessage - modelTitle={props.modelTitle} - deleteMessage={props.deleteMessage} - isNewMessage={props.isNewMessage} - message={props.message} - desktop={desktop} - theme={theme} - modelType={props.modelType} - /> - )} - </span> - </> - ) -}) @@ -16,13 +16,10 @@ export function useImagesPagination(showError: (message: string) => void, type: const [offset, setOffset] = useState<number>(0) - const dispatch = useAppDispatch() - async function onFetch() { if (!data) return setLoading(true) - console.log(offset) const { data: answer, ...response } = await getImagesGalery(data.access, offset) setLoading(false) @@ -12,7 +12,7 @@ export const useMessages = (messages: Message[]) => { .map((el) => { if (el.file && (el.file as any).includes('.zip')) return null if (/\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg/.test(el.file as string)) return el - return null + return null }) .filter((el) => el !== null) as Message<string>[] }, [messages]) @@ -1,843 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material' -import Stack from '@mui/material/Stack' -import Image from 'next/image' - -import { useAppSelector } from '#/app/store/store' -import { TooltipCustom } from '#/shared' -import { Message } from '#/shared/lib/types/model' -import { BotMessage } from '#/widgets/messages' -import { Markdown } from '../../markdown/markdown' -import { formatDate } from '#/shared/lib/helpers' - -interface IMessagesList { - // messageResponse: Message[] | null - // mode?: string - // sendMessage?: (title: string) => void - // getMessagesPagination?: () => Promise<void> - - device: 'mobile' | 'desktop' - modelType: string - isNewMessage: boolean - setCurrentSrc: (value: string) => void - message: Message - setModal: (value: boolean) => void - deleteMessage?: (message_uid: string) => void - modelTitle: string | undefined - setResendValue: (value: string) => void - onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void -} - -export const UserMessage = React.memo(function UserMessage({ setCurrentSrc, setModal, ...props }: IMessagesList) { - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const open = Boolean(anchorEl) - const desktop = props.device === 'desktop' - const [loaded, setLoaded] = useState(false) - - const copy = (text: string) => { - navigator.clipboard.writeText(text) - } - const handleClick = (event: React.MouseEvent<HTMLElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - - const fileName = useMemo(() => { - if (!props.message.file) return 'Подбираем имя файла...' - const matches = (props.message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/) - if (!matches) return 'Подбираем имя файла...' - return `${matches[1]}.${matches[2]}` - }, []) - - const resend = async () => { - props.setResendValue(props.message.content) - if (props.message.file !== null) { - fetch(props.message.file as string, { - method: 'GET', - headers: {}, - }).then((response) => { - response.arrayBuffer().then(function (buffer) { - const url = new Blob([buffer]) - const binaryFile = new File([url], 'image.png') - if (props.onLoadImage) { - props.onLoadImage(null, binaryFile) - } - }) - }) - } - } - - useEffect(() => { - console.log(props.message.file) - }, [props.message.file]) - - const flexStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'start', - gap: '12px', - color: '#7f7df3 !important', - fontWeight: '600 !important', - lineHeight: '140%', - letterSpacing: '0.2px', - cursor: 'pointer', - } - - const theme = useAppSelector((state) => state.theme.theme) - - return ( - <> - <span className='tutorial-message'> - {!props.message.from_model ? ( - // <Slide className='tutorial-message-me' direction='left' in={props.isNewMessage} mountOnEnter unmountOnExit> - <Box> - <Box - className='smallScroll' - sx={{ - width: 'fit-content', - marginLeft: 'auto', - maxWidth: desktop ? '68%' : '100%', - marginTop: 2, - marginBottom: 2, - }} - > - <Stack direction='row' justifyContent='end' alignItems='center'> - <Typography - variant='body2' - sx={{ - color: theme === 'light' ? '#868686' : '#A6A5A5', - lineHeight: '19.6px', - fontSize: '13px', - fontWeight: '600', - marginLeft: 1, - }} - > - {formatDate(props.message.created_at, { - hour: 'numeric', - minute: 'numeric', - })} - </Typography> - </Stack> - - <Box display='flex' alignItems='center' justifyContent='end'> - {props.message.file ? ( - /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg/.test( - props.message.file as string - ) ? ( - <Box - sx={{ - display: 'flex', - alignItems: 'end', - flexDirection: 'column', - height: '60%', - width: '60%', - }} - > - <Box sx={{ position: 'relative' }}> - <Image - onClick={() => { - if (!props.message.file) return - setCurrentSrc(props.message.file.toString()) - setModal(true) - }} - onLoadingComplete={() => setLoaded(true)} - style={{ - objectFit: 'contain', - height: '100%', - width: '100%', - borderRadius: '13px', - cursor: 'pointer', - opacity: loaded ? '100%' : '0%', - minWidth: '100px', - minHeight: '100px', - }} - width={500} - height={500} - src={props.message.file.toString().split('?type')[0]} - alt={'К сожалению, изображение не загрузилось'} - /> - {!loaded && ( - <Skeleton - sx={{ - background: - theme === 'dark' - ? '#2d2d2f' - : '#EFF0F2', - position: 'absolute', - top: '0', - left: '0', - zIndex: '1', - borderRadius: 5, - width: '100%', - height: '100%', - }} - variant='rectangular' - /> - )} - </Box> - <Box display='flex' alignItems='center'> - <Box marginRight={1} sx={{ cursor: 'pointer' }}> - <Box - onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - > - <TooltipCustom title={'Открыть меню'}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === 'dark' - ? '#303035' - : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === 'dark' - ? '#303035' - : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) - props.deleteMessage( - props.message.uid - ) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: - props.message.content.length > 30 - ? 'pre-wrap' - : 'pre', - }} - > - {/*<Markdown content={props.message.content} />*/} - {props.message.content} - </Typography> - </Box> - </Box> - ) : ( - <Box - sx={{ - display: 'flex', - alignItems: 'end', - flexDirection: 'column', - gap: '5px', - }} - > - <Box display={'flex'}> - <Box - onClick={() => { - if (props.message.file) { - window.open( - props.message.file as string, - '_blank' - ) - } - }} - className='smallScroll' - sx={{ - overflowY: 'scroll', - position: 'relative', - padding: '15px 23px', - - border: `1px solid ${ - theme === 'dark' ? '#303035' : '#EFF0F2' - }`, - - boxShadow: 'none', - fontSize: '15px', - marginTop: 0.5, - textAlign: 'left', - fontFamily: 'Raleway,sans-serif', - borderRadius: '13px', - '& p': { - color: 'inherit', - fontStyle: 'inherit', - }, - ...flexStyle, - }} - > - <Box - sx={{ - width: '16px', - height: '18px', - }} - > - <svg - width='16' - height='18' - viewBox='0 0 15 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M10.3286 0.578662C9.95416 0.20795 9.44851 0 8.92158 0H2C0.895433 0 0 0.895431 0 2V17C0 18.1046 0.895431 19 2 19H13C14.1046 19 15 18.1046 15 17V6.03743C15 5.50349 14.7865 4.99173 14.4071 4.61609L10.3286 0.578662ZM10.0669 5.37846C9.79079 5.37846 9.56693 5.1546 9.56693 4.87846V2.19238C9.56693 1.74815 10.103 1.52452 10.4187 1.83705L13.132 4.52313C13.4495 4.83737 13.227 5.37846 12.7803 5.37846H11.7815H10.0669Z' - fill='#7f7df3' - /> - </svg> - </Box> - - <Markdown content={fileName} theme={theme} /> - </Box> - </Box> - <Box display='flex' alignItems='center'> - <Box marginRight={1} sx={{ cursor: 'pointer' }}> - <Box - onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - > - <TooltipCustom title={'Открыть меню'}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === 'dark' - ? '#303035' - : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === 'dark' - ? '#303035' - : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) - props.deleteMessage( - props.message.uid - ) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: - props.message.content.length > 30 - ? 'pre-wrap' - : 'pre', - }} - > - {/*<Markdown content={props.message.content} />*/} - {props.message.content} - </Typography> - </Box> - </Box> - ) - ) : ( - <Box display='flex' alignItems='center'> - <Box marginRight={1} sx={{ cursor: 'pointer' }}> - <Box - onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - > - <TooltipCustom title={'Открыть меню'}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === 'dark' ? '#303035' : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === 'dark' ? '#303035' : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) - props.deleteMessage(props.message.uid) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: - props.message.content.length > 30 ? 'pre-wrap' : 'pre', - }} - > - {/*<Markdown content={props.message.content} />*/} - {props.message.content} - </Typography> - </Box> - )} - </Box> - </Box> - </Box> - ) : ( - <BotMessage - modelTitle={props.modelTitle} - deleteMessage={props.deleteMessage} - isNewMessage={props.isNewMessage} - message={props.message} - desktop={desktop} - theme={theme} - modelType={props.modelType} - /> - )} - </span> - </> - ) -}) @@ -1,5 +0,0 @@ -.button { - background-color: transparent; - border: 0; - cursor: pointer; -} @@ -1,345 +0,0 @@ -import React, { useEffect } from 'react' -import { Box, Menu, MenuItem, Slide, Typography } from '@mui/material' -import Stack from '@mui/material/Stack' -import Image from 'next/image' -import styles from './bot-message.module.scss' - -import { useAppSelector } from '#/app/store/store' -import { TooltipCustom } from '#/shared' -import { Markdown } from '#/widgets/markdown/markdown' -import { formatDate } from '#/shared/lib/helpers' - -export function BotMessage(props: any) { - // const LazyCode = dynamic(() => import('src/widgets/chat-gpt-field/ui/code')) - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const open = Boolean(anchorEl) - const [markdownMessage, setMarkDownMessage] = React.useState<string>('') - - const copy = (text: string) => { - navigator.clipboard.writeText(text) - } - const handleClick = (event: React.MouseEvent<HTMLElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - const theme = useAppSelector((state) => state.theme.theme) - - useEffect(() => { - if (props.message.file) { - let splitFileName = props.message.file.split('/')[4].split('?')[0] - let fileName: string = '' - if (splitFileName.length > 50) { - fileName = - splitFileName.slice(0, 25) + - '...' + - splitFileName - .slice(splitFileName.length - 8, splitFileName.length) - .split('.')[0] + - '.' + - splitFileName.split('.')[1] - } else { - fileName = splitFileName - } - setMarkDownMessage(fileName) - } - }, [props]) - - const flexStyle = props.message.file - ? { - display: 'flex', - alignItems: 'center', - justifyContent: 'start', - gap: '12px', - color: '#7f7df3 !important', - fontWeight: '600 !important', - lineHeight: '140%', - letterSpacing: '0.2px', - cursor: 'pointer', - } - : {} - - return ( - // <Slide direction='right' in={props.isNewMessage} mountOnEnter unmountOnExit> - <Box> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - marginTop: 2, - marginBottom: 2, - }} - > - {props.desktop && ( - <Box sx={{ marginRight: 1 }}> - <Image - height={36} - width={36} - src='/svg/chatgpt/avatar.svg' - alt={''} - /> - </Box> - )} - <Box - sx={{ - paddingLeft: 0, - width: 'fit-content', - maxWidth: props.desktop ? '68%' : '95%', - marginRight: 'auto', - }} - > - <Stack direction='row' justifyContent='space-between'> - <Typography - variant='body2' - sx={{ - color: props.theme === 'light' ? '#868686' : '#A6A5A5', - lineHeight: '16.8px', - fontSize: '13px', - fontWeight: '600', - marginRight: 2, - }} - > - {props.modelTitle} - </Typography> - <Typography - variant='body2' - sx={{ - color: props.theme === 'light' ? '#868686' : '#A6A5A5', - lineHeight: '19.6px', - fontSize: '13px', - fontWeight: '600', - marginRight: 4, - }} - > - {formatDate(props.message.created_at, { - hour: 'numeric', - minute: 'numeric', - })} - </Typography> - </Stack> - <Box display={'flex'} width='100%'> - <Box - onClick={() => { - if (props.message.file) { - window.open(props.message.file, '_blank') - } - }} - className='smallScroll' - sx={{ - overflowY: 'scroll', - position: 'relative', - padding: '15px 23px', - border: `1px solid ${ - props.theme === 'dark' ? '#303035' : '#EFF0F2' - }`, - color: props.theme === 'light' ? '#5E5E5E' : '#A6A5A5', - boxShadow: 'none', - lineHeight: '22.5px', - fontSize: '15px', - marginTop: 0.5, - textAlign: 'left', - fontFamily: 'Raleway,sans-serif', - borderRadius: '13px', - '& p': { - color: 'inherit', - fontStyle: 'inherit', - }, - ...flexStyle, - }} - > - {props.message.file ? ( - <svg - width='16' - height='18' - viewBox='0 0 15 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M10.3286 0.578662C9.95416 0.20795 9.44851 0 8.92158 0H2C0.895433 0 0 0.895431 0 2V17C0 18.1046 0.895431 19 2 19H13C14.1046 19 15 18.1046 15 17V6.03743C15 5.50349 14.7865 4.99173 14.4071 4.61609L10.3286 0.578662ZM10.0669 5.37846C9.79079 5.37846 9.56693 5.1546 9.56693 4.87846V2.19238C9.56693 1.74815 10.103 1.52452 10.4187 1.83705L13.132 4.52313C13.4495 4.83737 13.227 5.37846 12.7803 5.37846H11.7815H10.0669Z' - fill='#7f7df3' - /> - </svg> - ) : ( - '' - )} - <Markdown - content={ - props.message.file - ? props.message.file.match(/(.+)\/(.+)\?/)[2] - : props.message.content - } - theme={theme} - /> - </Box> - <Box marginLeft={1}> - <Box - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - sx={{ - position: 'sticky', - top: '20px', - }} - > - <TooltipCustom title={'Открыть меню'}> - <button - className={styles.button} - onClick={handleClick} - > - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </button> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: - theme === 'dark' ? '#303035' : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: - theme === 'dark' ? '#303035' : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy( - props.message.content - ? props.message.content - : props.message.file - .split('/')[4] - .split('?')[0] - ) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - props.deleteMessage(props.message.uid) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - </Box> - </Box> - </Box> - </Box> - ) -} @@ -0,0 +1,22 @@ + + +.observed{ + width: 100%; + height: 1000px; + position: absolute; + left: 0; + top: 0; +} + +.list{ + height: 100%; + + &__scroll{ + padding-top: 30px; + text-align: center; + } +} + +.preview{ + padding-top: 30px; +} \ No newline at end of file @@ -1,220 +1,94 @@ -import React, { memo, useEffect, useMemo, useRef, useState } from 'react' -import { Box, CircularProgress } from '@mui/material' -import { useSession } from 'next-auth/react' - -import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' +import React, { memo, useEffect, useRef } from 'react' import { IsNextDay } from '#/widgets/messages/ui/is-next-day' -import { PreviewView } from '#/widgets/messages/message-components/preview-view' -import { ImageModal } from '#/features/image-modal' -import { ClientOnly } from '#/shared' -import { createPortal } from 'react-dom' -import { Message } from '#/entities/message' +import { Loader, c } from '#/shared' +import { Model } from '#/entities/model-entity' +import { useChatBotPagination } from '#/features/chat-bot-pagination' +import { useCurrentChat } from '#/features/chats' +import { CommonScrollbar } from '#/shared/ui/scrollbar' +import styles from './chat-messages-list.module.scss' +import { useRouter } from 'next/router' +import { useChatBotMessages } from '#/entities/message' +import { PreviewView } from '.' +import { CSSTransition, SwitchTransition, TransitionGroup } from 'react-transition-group' interface IMessagesList { - device: 'mobile' | 'desktop' - modelType: string - messageResponse: Message[] | null - mode?: string - sendMessage?: (title: string) => void - getMessagesPagination?: () => Promise<void> - deleteMessage?: (message_uid: string) => void - modelTitle: string | undefined - setResendValue: (value: string) => void - onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void - loading: boolean + model: Model | null } -export const ChatMessagesList: React.FC<IMessagesList> = memo( - ({ - messageResponse, - onLoadImage, - setResendValue, - modelTitle, - device, - modelType, - mode, - getMessagesPagination, - deleteMessage, - loading, - }) => { - const paginationScroll = React.useRef<any>() - const [isPaginating, setIsPaginating] = React.useState(false) - const [chatScrollHeight, setChatScrollHeight] = React.useState(0) - const [scrollBottom, setScrollBottom] = React.useState(0) - - const desktop = device === 'desktop' - const { status } = useSession() - const [modal, setModal] = useState<boolean>(false) - const [isNewMessage, setIsNewMessage] = useState(false) - - const [currentSrc, setCurrentSrc] = useState<string | null>(null) - - const onlyImageMessage = useMemo(() => { - if (!messageResponse) return [] - return messageResponse - .map((el) => { - if (el.file && (el.file as any).includes('.zip')) return null - if (/\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg/.test(el.file as string)) { - return { ...el, file: el.file?.toString().split('?type')[0]! } - } - return null - }) - .filter((el) => el !== null) as Message<string>[] - }, [messageResponse]) - - React.useEffect(() => { - const block = paginationScroll.current +export const ChatMessagesList: React.FC<IMessagesList> = ({ model }) => { + const { currentChat } = useCurrentChat() - if (messageResponse != undefined && !isPaginating) { - setChatScrollHeight(paginationScroll.current.scrollHeight) + const { query } = useRouter() - const time = setTimeout(() => { - if (block) { - //@ts-ignore - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - } - }, 250) - return () => clearTimeout(time) - } else if (messageResponse != undefined && isPaginating) { - if (block) { - //@ts-ignore - block.scrollTop = block.scrollHeight - chatScrollHeight - setChatScrollHeight(paginationScroll.current.scrollHeight) - } - } - setIsPaginating(false) - }, [messageResponse]) + const { refScroll, scrollContainer, onObserverMounted, loading, loaded } = + useChatBotPagination() - useEffect(() => { - setIsNewMessage(true) - }, [messageResponse]) + const { messages } = useChatBotMessages() - const handleScroll = () => { - setScrollBottom( - paginationScroll.current?.scrollHeight - - paginationScroll.current?.scrollTop - - paginationScroll.current?.clientHeight - ) + useEffect(() => { + if (!currentChat) return + onObserverMounted() + }, [currentChat, query.slug]) - if (paginationScroll.current && messageResponse?.length !== 0) { - const { scrollTop, scrollHeight, clientHeight } = paginationScroll.current - if (scrollTop === 0) { - if (getMessagesPagination) { - setIsPaginating(true) - getMessagesPagination() - } - } - } - } + const transitionRef = useRef(null) - return ( - <> - <ClientOnly> + return ( + <> + {/* <ClientOnly> {createPortal( <ImageModal modal={modal} current={currentSrc} setModal={setModal} + images={onlyImageMessage} onSlideFalse={async () => { getMessagesPagination && (await getMessagesPagination()) }} - images={onlyImageMessage} />, document.getElementById('modal-container')! )} - </ClientOnly> - - <Box - sx={{ - overflowY: 'scroll', - overflowX: 'hidden', - '::-webkit-scrollbar': { - display: 'none', - }, - paddingRight: desktop ? 2 : 0, - paddingLeft: desktop ? 1 : 0, - }} - ref={paginationScroll} - onScroll={handleScroll} + </ClientOnly> */} + + <SwitchTransition> + <CSSTransition + timeout={400} + key={loaded && !messages.length ? 'preview' : 'list'} + nodeRef={transitionRef} + classNames='my-node' > - {scrollBottom > 500 && ( - <Box - sx={{ - position: 'absolute', - left: 0, - right: 0, - width: 'fit-content', - cursor: 'pointer', - margin: '0 auto', - bottom: '150px', - zIndex: 10, - }} - onClick={() => { - //@ts-ignore - const block = paginationScroll.current - - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }} - > - <ArrowDownScroll /> - </Box> - )} - - {loading && ( - <Box - sx={{ - position: 'absolute', - left: 0, - right: 0, - width: 'fit-content', - margin: '0 auto', - top: '10px', - zIndex: 10, - }} - > - <CircularProgress - size={18} - thickness={3} - sx={{ - color: '#7F7DF3', - }} - /> - </Box> - )} - - {messageResponse?.length === 0 && status === 'authenticated' ? ( - <>{desktop && modelType !== 'deepl' && <PreviewView setValue={setResendValue} />}</> + {loaded && !messages.length ? ( + <div className={styles.preview} ref={transitionRef}> + <PreviewView setValue={() => {}} /> + </div> ) : ( - messageResponse?.map((message, idx) => { - return ( - <IsNextDay - onLoadImage={onLoadImage} - setResendValue={setResendValue} - key={idx} - message={message} - setCurrentSrc={setCurrentSrc} - setModal={setModal} - index={idx - 1} - messageResponse={messageResponse} - isNewMessage={isNewMessage} - modelType={modelType} - modelTitle={modelTitle} - deleteMessage={deleteMessage} - device={device} - /> - ) - }) + <div className={styles.list} ref={transitionRef}> + <CommonScrollbar blockedScrollTop ref={scrollContainer}> + {loading && ( + <div className={styles.list__scroll}> + <Loader /> + </div> + )} + <div className={c(styles.observed)} ref={refScroll}></div> + <TransitionGroup> + {messages.map((message, idx) => ( + <CSSTransition + key={message.uid} + timeout={400} + classNames='my-node' + > + <IsNextDay + model={model} + message={message} + index={idx - 1} + /> + </CSSTransition> + ))} + </TransitionGroup> + </CommonScrollbar> + </div> )} - </Box> - </> - ) - } -) - -ChatMessagesList.displayName = 'ChatMessagesList' + </CSSTransition> + </SwitchTransition> + </> + ) +} @@ -1,8 +1,8 @@ export * from './image-messages-list' export * from './chat-messages-list' export * from './answer-wrap' -export * from './bot-message' -export * from './preview-view' -export * from './user-message' +export * from '../../../entities/message/ui/bot-message' +export * from '../../preview-view/ui/preview-view' +export * from '../../../entities/message/ui/user-message' export * from './is-next-day' export * from './image-icons' @@ -0,0 +1,12 @@ + +.date{ + background-color: #8280FF26; + color: var(--air-color); + width: fit-content; + margin: 0 auto; + padding: 7px 25px; + border-radius: 100px; + font-size: 15px; + margin-top: 40px; + font-weight: 600; +} \ No newline at end of file @@ -1,134 +1,50 @@ import React, { memo, useMemo } from 'react' import { Box, Typography } from '@mui/material' -import { Message } from '#/shared/lib/types/model' import { getDateFromString } from '#/widgets/messages/lib/date-from-string' import { getDayMontsString } from '#/widgets/messages/lib/day-months-string' -import { UserMessage } from '#/widgets/messages' +import { BotMessage, UserMessage } from '#/widgets/messages' +import { Message, useChatBotMessages } from '#/entities/message' +import { useThemeAndDevice } from '#/shared/lib/hooks' +import { Model } from '#/entities/model-entity' +import { Loader, c } from '#/shared' +import styles from './is-next-day.module.scss' interface IProps { message: Message index: number - messageResponse: Message[] | null - modelTitle: string | undefined - deleteMessage?: (message_uid: string) => void - setCurrentSrc: (value: string) => void - isNewMessage: boolean - setModal: (value: boolean) => void - modelType: string - device: 'mobile' | 'desktop' - setResendValue: (value: string) => void - onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void + model: Model | null + // setModal: (value: boolean) => void + // setCurrentSrc: (value: string) => void } -export const IsNextDay = memo( - ({ - message, - index, - onLoadImage, - messageResponse, - modelTitle, - deleteMessage, - setCurrentSrc, - setModal, - isNewMessage, - modelType, - device, - setResendValue, - }: IProps) => { - if (messageResponse && message.created_at) { - const date = getDateFromString(message.created_at) - const prevDate = getDateFromString(messageResponse[index]?.created_at) +export const IsNextDay = memo(({ message, index, model }: IProps) => { + const { messages } = useChatBotMessages() - if (date && prevDate) { - if (date.day > prevDate.day && date.month === prevDate.month) { - return ( - <> - <Box display={'flex'} justifyContent={'center'}> - <Typography - sx={{ - backgroundColor: '#8280FF26', - color: '#8280FF', - display: 'inline-block', - padding: '7px 25px', - borderRadius: '100px', - fontSize: '15px', - marginY: '40px', - fontWeight: '600', - }} - > - {getDayMontsString(message.created_at)} - </Typography> - </Box> - <UserMessage - onLoadImage={onLoadImage} - setResendValue={setResendValue} - modelTitle={modelTitle} - deleteMessage={deleteMessage} - setCurrentSrc={setCurrentSrc} - key={message.uid} - setModal={setModal} - message={message} - isNewMessage={isNewMessage} - modelType={modelType} - device={device} - /> - </> - ) - } - } - if (!prevDate) { - return ( - <> - <Box display={'flex'} justifyContent={'center'}> - <Typography - sx={{ - backgroundColor: '#8280FF26', - color: '#8280FF', - display: 'inline-block', - padding: '7px 25px', - borderRadius: '100px', - fontSize: '15px', - marginY: '40px', - fontWeight: '600', - }} - > - {getDayMontsString(message.created_at)} - </Typography> - </Box> - <UserMessage - setModal={setModal} - onLoadImage={onLoadImage} - setResendValue={setResendValue} - modelTitle={modelTitle} - setCurrentSrc={setCurrentSrc} - deleteMessage={deleteMessage} - key={message.uid} - message={message} - isNewMessage={isNewMessage} - modelType={modelType} - device={device} - /> - </> - ) - } - } - return ( - <UserMessage - onLoadImage={onLoadImage} - setResendValue={setResendValue} - modelTitle={modelTitle} - deleteMessage={deleteMessage} - setCurrentSrc={setCurrentSrc} - key={message.uid} - setModal={setModal} - message={message} - isNewMessage={isNewMessage} - modelType={modelType} - device={device} - /> - ) - } -) + const date = getDateFromString(message.created_at) + const prevDate = getDateFromString(messages[index]?.created_at) + + const dateVisible = useMemo(() => { + if (!date) return false + if (!prevDate) return true + if (date.day > prevDate.day && date.month === prevDate.month) return true + return false + }, [date, prevDate]) + + if (!model) return <Loader /> + + return ( + <> + {dateVisible && ( + <p className={c(styles.date)}>{getDayMontsString(message.created_at)}</p> + )} + {message.from_model ? ( + <BotMessage model={model} message={message} /> + ) : ( + <UserMessage key={message.uid} model={model} message={message} /> + )} + </> + ) +}) IsNextDay.displayName = 'IsNextDay' @@ -1,44 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' - -const previewMessages = ['Что такое орбитальная механика?', 'Как приготовить омлет?', 'Кто такие Фиксики?'] - -export function PreviewView(props: any) { - return ( - <Box className='content-center-translate'> - <Typography className='text'>Не знаете, с чего начать?</Typography> - <Typography className='text' sx={{ marginBottom: '15px' }}> - Попробуйте, например, вот так: - </Typography> - <Box - sx={{ - textAlign: 'center', - width: '100%', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }} - > - {previewMessages.map((el) => { - return ( - <Box - key={el} - onClick={() => props.setValue(el)} - sx={{ - cursor: 'pointer', - padding: '15px', - color: '#8280FF', - width: 'fit-content', - marginTop: '15px', - backgroundColor: 'rgba(130, 128, 255, 0.10)', - borderRadius: '12px', - }} - > - {el} - </Box> - ) - })} - </Box> - </Box> - ) -} @@ -1,806 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material' -import Stack from '@mui/material/Stack' -import Image from 'next/image' - -import { useAppSelector } from '#/app/store/store' -import { TooltipCustom } from '#/shared' -import { Message } from '#/shared/lib/types/model' -import { BotMessage } from '#/widgets/messages' -import { Markdown } from '../../markdown/markdown' -import { formatDate } from '#/shared/lib/helpers' -import styles from './bot-message.module.scss' - -interface IMessagesList { - // messageResponse: Message[] | null - // mode?: string - // sendMessage?: (title: string) => void - // getMessagesPagination?: () => Promise<void> - - device: 'mobile' | 'desktop' - modelType: string - isNewMessage: boolean - setCurrentSrc: (value: string) => void - message: Message - setModal: (value: boolean) => void - deleteMessage?: (message_uid: string) => void - modelTitle: string | undefined - setResendValue: (value: string) => void - onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void -} - -export const UserMessage = React.memo(function UserMessage({ setCurrentSrc, setModal, ...props }: IMessagesList) { - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const open = Boolean(anchorEl) - const desktop = props.device === 'desktop' - const [loaded, setLoaded] = useState(false) - - const copy = (text: string) => { - navigator.clipboard.writeText(text) - } - const handleClick = (event: React.MouseEvent<HTMLElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - - const fileName = useMemo(() => { - if (!props.message.file) return 'Подбираем имя файла...' - const matches = (props.message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/) - if (!matches) return 'Подбираем имя файла...' - return `${matches[1]}.${matches[2]}` - }, []) - - const resend = async () => { - props.setResendValue(props.message.content) - if (props.message.file !== null) { - fetch(props.message.file as string, { - method: 'GET', - headers: {}, - }).then((response) => { - response.arrayBuffer().then(function (buffer) { - const url = new Blob([buffer]) - const binaryFile = new File([url], 'image.png') - if (props.onLoadImage) { - props.onLoadImage(null, binaryFile) - } - }) - }) - } - } - - const flexStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'start', - gap: '12px', - color: '#7f7df3 !important', - fontWeight: '600 !important', - lineHeight: '140%', - letterSpacing: '0.2px', - cursor: 'pointer', - } - - const theme = useAppSelector((state) => state.theme.theme) - - return ( - <> - <span className='tutorial-message'> - {!props.message.from_model ? ( - // <Slide className='tutorial-message-me' direction='left' in={props.isNewMessage} mountOnEnter unmountOnExit> - <Box> - <Box - className='smallScroll' - sx={{ - width: 'fit-content', - marginLeft: 'auto', - maxWidth: desktop ? '68%' : '100%', - marginTop: 2, - marginBottom: 2, - }} - > - <Stack direction='row' justifyContent='end' alignItems='center'> - <Typography - variant='body2' - sx={{ - color: theme === 'light' ? '#868686' : '#A6A5A5', - lineHeight: '19.6px', - fontSize: '13px', - fontWeight: '600', - marginLeft: 1, - }} - > - {formatDate(props.message.created_at, { - hour: 'numeric', - minute: 'numeric', - })} - </Typography> - </Stack> - - <Box display='flex' justifyContent='end'> - {props.message.file ? ( - /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg|\.wav/.test(props.message.file as string) ? ( - <Box - sx={{ - display: 'flex', - alignItems: 'end', - flexDirection: 'column', - height: '60%', - width: '60%', - }} - > - <Box sx={{ position: 'relative' }}> - <Image - onClick={() => { - if (!props.message.file) return - setCurrentSrc(props.message.file.toString().split('?type')[0]) - setModal(true) - }} - onLoadingComplete={() => setLoaded(true)} - style={{ - objectFit: 'contain', - height: '100%', - width: '100%', - borderRadius: '13px', - cursor: 'pointer', - opacity: loaded ? '100%' : '0%', - minWidth: '100px', - minHeight: '100px', - }} - width={500} - height={500} - src={props.message.file.toString().split('?type')[0]} - alt={'К сожалению, изображение не загрузилось'} - /> - {!loaded && ( - <Skeleton - sx={{ - background: theme === 'dark' ? '#2d2d2f' : '#EFF0F2', - position: 'absolute', - top: '0', - left: '0', - zIndex: '1', - borderRadius: 5, - width: '100%', - height: '100%', - }} - variant='rectangular' - /> - )} - </Box> - <Box display='flex'> - <Box marginRight={1}> - <Box - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - > - <TooltipCustom title={'Открыть меню'}> - <button className={styles.button} onClick={handleClick}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle - cx='14.5' - cy='4.5' - r='3.5' - stroke='#A4AAB5' - strokeWidth='2' - /> - </svg> - </button> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - {props.message.content && props.message.content.length > 0 && ( - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: props.message.content.length > 30 ? 'pre-wrap' : 'pre', - }} - > - {props.message.content} - </Typography> - )} - </Box> - </Box> - ) : ( - <Box - sx={{ - display: 'flex', - alignItems: 'end', - flexDirection: 'column', - gap: '5px', - }} - > - <Box display={'flex'}> - <Box - onClick={() => { - if (props.message.file) { - window.open(props.message.file as string, '_blank') - } - }} - className='smallScroll' - sx={{ - overflowY: 'scroll', - position: 'relative', - padding: '15px 23px', - - border: `1px solid ${theme === 'dark' ? '#303035' : '#EFF0F2'}`, - - boxShadow: 'none', - fontSize: '15px', - marginTop: 0.5, - textAlign: 'left', - fontFamily: 'Raleway,sans-serif', - borderRadius: '13px', - '& p': { - color: 'inherit', - fontStyle: 'inherit', - }, - ...flexStyle, - }} - > - <Box - sx={{ - width: '16px', - height: '18px', - }} - > - <svg - width='16' - height='18' - viewBox='0 0 15 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M10.3286 0.578662C9.95416 0.20795 9.44851 0 8.92158 0H2C0.895433 0 0 0.895431 0 2V17C0 18.1046 0.895431 19 2 19H13C14.1046 19 15 18.1046 15 17V6.03743C15 5.50349 14.7865 4.99173 14.4071 4.61609L10.3286 0.578662ZM10.0669 5.37846C9.79079 5.37846 9.56693 5.1546 9.56693 4.87846V2.19238C9.56693 1.74815 10.103 1.52452 10.4187 1.83705L13.132 4.52313C13.4495 4.83737 13.227 5.37846 12.7803 5.37846H11.7815H10.0669Z' - fill='#7f7df3' - /> - </svg> - </Box> - - <Markdown content={fileName} theme={theme} /> - </Box> - </Box> - <Box display='flex'> - <Box marginRight={1}> - <Box - onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - > - <TooltipCustom title={'Открыть меню'}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle cx='14.5' cy='4.5' r='3.5' stroke='#A4AAB5' strokeWidth='2' /> - </svg> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - {props.message.content && props.message.content.length > 0 && ( - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - )} - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - {props.message.content && props.message.content.length > 0 && ( - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: props.message.content.length > 30 ? 'pre-wrap' : 'pre', - }} - > - {props.message.content} - </Typography> - )} - </Box> - </Box> - ) - ) : ( - <Box - sx={{ - display: 'flex', - width: '100%', - }} - > - <Box marginRight={1}> - <Box - aria-controls={open ? 'message-menu' : undefined} - aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} - sx={{ - position: 'sticky', - top: '20px', - }} - > - <TooltipCustom title={'Открыть меню'}> - <button className={styles.button} onClick={handleClick}> - <svg - width='22' - height='22' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <rect - x='1' - y='1' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='1' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <rect - x='11' - y='11' - width='7' - height='7' - rx='2' - stroke='#A4AAB5' - strokeWidth='2' - /> - <circle cx='14.5' cy='4.5' r='3.5' stroke='#A4AAB5' strokeWidth='2' /> - </svg> - </button> - </TooltipCustom> - </Box> - <Menu - autoFocus={false} - open={open} - anchorEl={anchorEl} - id='message-menu' - onClose={handleClose} - onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', - borderRadius: '15px', - }, - }} - > - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} - onClick={() => { - copy(props.message.content) - }} - > - <svg - width='14' - height='14' - viewBox='0 0 19 19' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <g id='Group 29094'> - <rect - id='Rectangle 3340' - x='0.85' - y='3.85' - width='14.3' - height='14.3' - rx='2.15' - fill='transparent' - stroke='#8280FF' - strokeWidth='1.7' - /> - <path - id='Vector 40' - d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' - stroke='#8280FF' - strokeWidth='1.7' - strokeLinecap='round' - strokeLinejoin='round' - /> - </g> - </svg> - Копировать - </MenuItem> - <MenuItem - autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '5px', - }} - onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) - }} - > - <svg - width='21' - height='14' - viewBox='0 0 60 42' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' - fill='#8280FF' - /> - </svg> - Удалить - </MenuItem> - </Menu> - </Box> - <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> - {!props.message.is_sent && ( - <Image - onClick={resend} - style={{ - cursor: 'pointer', - marginRight: '5px', - }} - height={20} - width={20} - alt='1' - src={'/svg/important.svg'} - /> - )} - </TooltipCustom> - {props.message.content && props.message.content.length > 0 && ( - <Typography - className='smallScroll' - sx={{ - overflowY: 'scroll', - padding: '10px 15px', - backgroundColor: '#7F7DF3', - borderRadius: '13px', - color: '#FFFFFF', - lineHeight: '21px', - fontSize: '15px', - fontWeight: '400px', - marginTop: 0.4, - textAlign: 'left', - whiteSpace: props.message.content.length > 30 ? 'pre-wrap' : 'pre', - }} - > - {props.message.content} - </Typography> - )} - </Box> - )} - </Box> - </Box> - </Box> - ) : ( - <BotMessage - modelTitle={props.modelTitle} - deleteMessage={props.deleteMessage} - isNewMessage={props.isNewMessage} - message={props.message} - desktop={desktop} - theme={theme} - modelType={props.modelType} - /> - )} - </span> - </> - ) -}) @@ -0,0 +1,2 @@ + +export * from './model-tags' \ No newline at end of file @@ -1,23 +1,3 @@ -@keyframes fadein { - 0% { - opacity: 0; - } - 50% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -.header { - width: 70%; - - @media screen and (max-width: 768px) { - width: 100%; - } -} - .tag { display: flex; align-items: center; @@ -76,62 +56,4 @@ @media screen and (max-width: 768px) { justify-content: flex-start; } -} - -.main { - width: 100%; - display: flex; - justify-content: flex-start; - align-items: stretch; - gap: 20px; - // margin-bottom: 24px; - - .chatWindow { - width: 70%; - position: relative; - } - - .settings { - width: 25%; - } - - @media (max-width: 768px) { - width: 100%; - display: flex; - - flex-direction: column-reverse; - - .chatWindow { - width: 100%; - } - - .settings { - width: 100%; - } - } -} - -.blocked { - display: flex; - align-items: center; - justify-content: center; - background-color: #c32528; - // padding-right: 20px; - padding: 8px 15px; - gap: 5px; - position: absolute; - left: 0; - bottom: -10px; - width: 100%; - border-radius: 0px 0px 15px 15px; - - // @media screen and (max-width: 768px) { - // display: none; - // } - - &__text { - color: white; - font-size: 14px; - font-weight: 600; - } -} +} \ No newline at end of file @@ -0,0 +1,26 @@ +import { ModelTag } from '#/entities/model-entity' +import { SvgIcon } from '#/shared/ui/svg' +import React from 'react' +import styles from './model-tags.module.scss' + +interface ModelTagsProps { + tags: ModelTag[] +} + +export const ModelTags = ({ tags }: ModelTagsProps) => { + return ( + <div className={styles.tags}> + {tags.map((tag, index) => ( + <div key={index} className={styles.tag}> + <SvgIcon + width={23} + height={23} + url={tag.icon} + className={styles.tag__icon} + /> + <span className={styles.tag__text}>{tag.title}</span> + </div> + ))} + </div> + ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -21,9 +21,6 @@ export const useNavigationSearchModel = (links: NavigationSearchLink[]) => { ) }, [search, links]) - useEffect(() => { - console.log(filteredLinks) - }, [filteredLinks]) // hooks const ctrlF = (e: KeyboardEvent) => { @@ -45,9 +42,6 @@ export const useNavigationSearchModel = (links: NavigationSearchLink[]) => { const value = filteredLinks.at(0) - console.log(value) - console.log(filteredLinks) - if (value) { push('/' + value.url) setSearchOpen(false) @@ -0,0 +1 @@ +export * from './messages' \ No newline at end of file @@ -0,0 +1,5 @@ +export const previewMessages = [ + 'Что такое орбитальная механика?', + 'Как приготовить омлет?', + 'Кто такие Фиксики?', +] @@ -0,0 +1,2 @@ + +export * from './preview-view' \ No newline at end of file @@ -0,0 +1,29 @@ +.preview { + display: flex; + flex-direction: column; + align-items: center; + + &__question { + margin-bottom: 5px; + font-weight: 500; + font-size: 20px; + } + + &__quote { + color: var(--new-ui-gray-text-color); + margin-bottom: 20px; + } + + &__messages { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + } +} + +.message { + padding: 15px !important; + border-radius: 15px !important; + cursor: pointer; +} @@ -0,0 +1,32 @@ +import React from 'react' +import { Box, Typography } from '@mui/material' +import { previewMessages } from '../config' +import styles from './preview-view.module.scss' +import { c } from '#/shared' +import { CommonBagde } from '#/shared/ui/badge' + +export interface PreviewViewProps { + setValue: (str: string) => void +} + +export function PreviewView({ setValue }: PreviewViewProps) { + return ( + <div className={styles.preview}> + <p className={styles.preview__question}>Не знаете, с чего начать?</p> + <p className={styles.preview__quote}>Попробуйте, например, вот так:</p> + <div className={styles.preview__messages}> + {previewMessages.map((el) => { + return ( + <CommonBagde + key={el} + onClick={() => setValue(el)} + className={c(styles.message)} + > + {el} + </CommonBagde> + ) + })} + </div> + </div> + ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -11,14 +11,14 @@ import { Balance } from '#/shared' import { NavigateTopBar } from '#/widgets/top-bar-model/ui/navigate-top-bar' import { NavigateTopBarMobile } from '#/widgets/top-bar-model/ui/navigate-top-bar-mobile' -interface IModelTopBar { +interface ModelTopBar { device: 'mobile' | 'desktop' favorites: any[] | [] token: string | null title: string } -export const ModelTopBar: React.FC<IModelTopBar> = memo(({ device, token, favorites, title }) => { +export const ModelTopBar: React.FC<ModelTopBar> = memo(({ device, token, favorites, title }) => { const theme = useAppSelector((state) => state.theme.theme) const [anchorElModel, setAnchorElModel] = React.useState<HTMLElement | null>(null) @@ -39,7 +39,9 @@ export const ModelTopBar: React.FC<IModelTopBar> = memo(({ device, token, favori Router.push('/account') } - const LazyTutorial = dynamic(() => import('#/features/tutorial-gpt').then((component) => component.Tutorial)) + const LazyTutorial = dynamic(() => + import('#/features/tutorial-gpt').then((component) => component.Tutorial) + ) const LazyArrowIcon = dynamic(() => import('./arrow-up-or-down')) @@ -67,7 +69,9 @@ export const ModelTopBar: React.FC<IModelTopBar> = memo(({ device, token, favori > <Stack direction='row' alignItems='center'> {desktop && ( - <Button onClick={openMenuModel}>{desktop && <MenuIcon sx={{ color: '#7F7DF3' }} />}</Button> + <Button onClick={openMenuModel}> + {desktop && <MenuIcon sx={{ color: '#7F7DF3' }} />} + </Button> )} <Stack onClick={openMenuModel as any} direction='row' alignItems='center'> <Typography @@ -87,7 +91,11 @@ export const ModelTopBar: React.FC<IModelTopBar> = memo(({ device, token, favori </Stack> {desktop ? ( - <NavigateTopBar favorites={favorites} closeMenu={closeMenuModel} anchorEl={anchorElModel} /> + <NavigateTopBar + favorites={favorites} + closeMenu={closeMenuModel} + anchorEl={anchorElModel} + /> ) : ( <NavigateTopBarMobile anchorEl={anchorElModel} @@ -98,12 +106,19 @@ export const ModelTopBar: React.FC<IModelTopBar> = memo(({ device, token, favori </Stack> <Box display='flex' alignItems='center'> - {desktop && status === 'authenticated' && pathname === '/chatgpt' && <LazyTutorial />} + {desktop && status === 'authenticated' && pathname === '/chatgpt' && ( + <LazyTutorial /> + )} <Stack direction='row' className='tutorial-balance' sx={{ - backgroundColor: theme === 'light' ? (desktop ? 'white' : 'transparent') : '#373737', + backgroundColor: + theme === 'light' + ? desktop + ? 'white' + : 'transparent' + : '#373737', marginRight: 1.2, padding: desktop ? '8px 15px' : '8px 0px', borderRadius: '10px', @@ -6,9 +6,9 @@ import { useRouter } from 'next/router' import { useAppSelector } from '#/app/store/store' -import { IModelMenu } from './navigate-top-bar' +import { ModelMenu } from './navigate-top-bar' -export const NavigateTopBarMobile: React.FC<IModelMenu> = ({ anchorEl, closeMenu, favorites }) => { +export const NavigateTopBarMobile: React.FC<ModelMenu> = ({ anchorEl, closeMenu, favorites }) => { const theme = useAppSelector((state) => state.theme.theme) const { pathname } = useRouter() @@ -8,13 +8,13 @@ import { useSession } from 'next-auth/react' import { useAppSelector } from '#/app/store/store' import { api } from '#/shared/api/endpoints' -export interface IModelMenu { +export interface ModelMenu { anchorEl: HTMLElement | null closeMenu: () => void favorites?: any[] | [] } -export const NavigateTopBar: React.FC<IModelMenu> = ({ anchorEl, closeMenu }) => { +export const NavigateTopBar: React.FC<ModelMenu> = ({ anchorEl, closeMenu }) => { const theme = useAppSelector((state) => state.theme.theme) const [favorites, setFavorites] = React.useState<any[] | []>([]) @@ -1,10 +1,10 @@ { - "useTabs": true, - "tabWidth": 5, - "singleQuote": true, - "trailingComma": "es5", - "jsxBracketSameLine": false, - "semi": false, - "printWidth": 150, - "jsxSingleQuote": true + "useTabs": true, + "tabWidth": 5, + "singleQuote": true, + "trailingComma": "es5", + "jsxBracketSameLine": false, + "semi": false, + "printWidth": 100, + "jsxSingleQuote": true } @@ -24,6 +24,7 @@ "@sentry/webpack-plugin": "^3.3.1", "@testing-library/user-event": "^14.6.1", "@types/cookie": "^0.5.1", + "@types/event-source-polyfill": "^1.0.5", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195", "@types/node": "18.15.2", @@ -42,6 +43,7 @@ "eslint-config-next": "13.2.4", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-simple-import-sort": "^10.0.0", + "event-source-polyfill": "^1.0.31", "husky": "^8.0.0", "i18next": "^23.4.1", "intro.js-react": "^1.0.0", @@ -60,6 +62,8 @@ "react-i18next": "^13.0.3", "react-markdown": "^8.0.7", "react-redux": "^8.0.5", + "react-responsive": "^10.0.1", + "react-scrollbars-custom": "^4.1.1", "react-syntax-highlighter": "^15.5.0", "react-transition-group": "^4.4.5", "remark-gfm": "^3.0.1", @@ -6091,6 +6095,12 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/event-source-polyfill": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz", + "integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -7765,6 +7775,16 @@ "node": ">=6" } }, + "node_modules/cnbuilder": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cnbuilder/-/cnbuilder-3.1.0.tgz", + "integrity": "sha512-9sxWwwprkPNTVQnx92WYPEWCmyZRqbf9VWUYABU4rl0mLL17VAV2MvvOApkMA6bQVXRLJ8jN//8Yp6drElpLWA==", + "license": "MIT", + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/xobotyi" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8000,6 +8020,12 @@ "node": ">=4" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "license": "BSD" + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -9312,6 +9338,12 @@ "node": ">=0.10.0" } }, + "node_modules/event-source-polyfill": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz", + "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==", + "license": "MIT" + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -10163,6 +10195,12 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/i18next": { "version": "23.7.6", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.7.6.tgz", @@ -12615,6 +12653,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/matchmediaquery": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz", + "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==", + "license": "MIT", + "dependencies": { + "css-mediaquery": "^0.1.2" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -14711,6 +14758,65 @@ } } }, + "node_modules/react-responsive": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.1.tgz", + "integrity": "sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.4.2", + "prop-types": "^15.6.1", + "shallow-equal": "^3.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-scrollbars-custom": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-scrollbars-custom/-/react-scrollbars-custom-4.1.1.tgz", + "integrity": "sha512-FOI1EMrdN5qziwvFAuH4saOo0+ggxeTuKLPpy6nezNdLUnDW1vCHGq0VSSfTSRstDgfXgwff5TMP9NeJ8NiFOQ==", + "license": "MIT", + "dependencies": { + "cnbuilder": "^3.1.0", + "react-draggable": "^4.4.5", + "zoom-level": "^2.5.0" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/xobotyi" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/react-scrollbars-custom/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-scrollbars-custom/node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-syntax-highlighter": { "version": "15.5.0", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", @@ -15465,6 +15571,12 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==", + "license": "MIT" + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -17323,6 +17435,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zoom-level": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/zoom-level/-/zoom-level-2.5.0.tgz", + "integrity": "sha512-7UlRWU4Q3uCMCeDVMOm7eBrIu145OqsIJ3p6zq58l8UsSYwKWxc6zEapC5YA9tIeh0oheb4cT9Kk2Wq353loFg==", + "license": "MIT", + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/xobotyi" + } + }, "node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", @@ -47,6 +47,7 @@ "@sentry/webpack-plugin": "^3.3.1", "@testing-library/user-event": "^14.6.1", "@types/cookie": "^0.5.1", + "@types/event-source-polyfill": "^1.0.5", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195", "@types/node": "18.15.2", @@ -65,6 +66,7 @@ "eslint-config-next": "13.2.4", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-simple-import-sort": "^10.0.0", + "event-source-polyfill": "^1.0.31", "husky": "^8.0.0", "i18next": "^23.4.1", "intro.js-react": "^1.0.0", @@ -83,6 +85,8 @@ "react-i18next": "^13.0.3", "react-markdown": "^8.0.7", "react-redux": "^8.0.5", + "react-responsive": "^10.0.1", + "react-scrollbars-custom": "^4.1.1", "react-syntax-highlighter": "^15.5.0", "react-transition-group": "^4.4.5", "remark-gfm": "^3.0.1",