Binary files /dev/null and b/public/pages/image-model/1.jpg differ Binary files /dev/null and b/public/pages/image-model/10.jpg differ Binary files /dev/null and b/public/pages/image-model/11.jpg differ Binary files /dev/null and b/public/pages/image-model/12.jpg differ Binary files /dev/null and b/public/pages/image-model/13.jpg differ Binary files /dev/null and b/public/pages/image-model/14.jpg differ Binary files /dev/null and b/public/pages/image-model/15.jpg differ Binary files /dev/null and b/public/pages/image-model/16.jpg differ Binary files /dev/null and b/public/pages/image-model/17.jpg differ Binary files /dev/null and b/public/pages/image-model/18.jpg differ Binary files /dev/null and b/public/pages/image-model/19.jpg differ Binary files /dev/null and b/public/pages/image-model/2.jpg differ Binary files /dev/null and b/public/pages/image-model/20.jpg differ Binary files /dev/null and b/public/pages/image-model/21.jpg differ Binary files /dev/null and b/public/pages/image-model/22.jpg differ Binary files /dev/null and b/public/pages/image-model/23.jpg differ Binary files /dev/null and b/public/pages/image-model/24.jpg differ Binary files /dev/null and b/public/pages/image-model/25.jpg differ Binary files /dev/null and b/public/pages/image-model/26.jpg differ Binary files /dev/null and b/public/pages/image-model/27.jpg differ Binary files /dev/null and b/public/pages/image-model/3.jpg differ Binary files /dev/null and b/public/pages/image-model/4.jpg differ Binary files /dev/null and b/public/pages/image-model/5.jpg differ Binary files /dev/null and b/public/pages/image-model/6.jpg differ Binary files /dev/null and b/public/pages/image-model/7.jpg differ Binary files /dev/null and b/public/pages/image-model/8.jpg differ Binary files /dev/null and b/public/pages/image-model/9.jpg differ @@ -1,3 +1,5 @@ - - + + \ No newline at end of file @@ -1,3 +1,4 @@ - - + + \ No newline at end of file @@ -29,3 +29,43 @@ } } } + +.blocked { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 20px; + right: 20px; + background-color: white; + border-radius: 100px; + padding-right: 20px; + padding: 8px 12px; + + span { + color: #ff2372; + font-weight: 500; + padding-left: 10px; + font-size: 14px; + } +} +.locked { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 15px; + padding-right: 20px; + + span { + color: white; + padding-top: 10px; + font-weight: 500; + } +} @@ -2,26 +2,32 @@ import React, { useEffect, useMemo } from 'react' import { Avatar, Box, Button, Typography } from '@mui/material' 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.module.scss' +import { useThemeAndDevice } from '#/shared/lib/hooks' export type CardProps = { title: string icon: string text: string uid: string + blocked?: boolean slug: string accessed_models?: string[] | null } -const ChatCard = ({ text, icon, title, uid, slug, accessed_models }: CardProps) => { +const ChatCard = ({ text, icon, title, uid, slug, accessed_models, blocked }: CardProps) => { const link = useMemo(() => { return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `chat-bot/${slug}` }, [accessed_models]) + const { theme } = useThemeAndDevice() + return ( - + @@ -30,42 +36,24 @@ const ChatCard = ({ text, icon, title, uid, slug, accessed_models }: CardProps) {text} - {accessed_models && !accessed_models.includes(slug) && ( - - - - - + + + Модель недоступна + + + ) : ( + accessed_models && + !accessed_models.includes(slug) && ( +
+ + Недоступно в текущем тарифе +
+ ) )}
@@ -5,7 +5,7 @@ import axios from 'axios' import { UniqInput } from '#/app/components/uniq_input' import { useAppSelector } from '#/app/store/store' import { ChatProps } from '#/shared/lib/types/model' -import { ChatMessagesList } from '#/widgets/messages/chat-messages-list' +import { ChatMessagesList } from '#/widgets/messages' import 'intro.js/introjs.css' @@ -1,3 +1,5 @@ + + :root[data-theme='light'] { --air-color: #8280ff; --background-color-main: #ffffff; @@ -18,6 +20,7 @@ --new-ui-bg-app-color: #eff0f2; --new-ui-main-color: white; --new-ui-gray-color: #a4aab5; + --new-ui-element-bg: #ffffff; --new-ui-text-color: #2b2b42; --new-ui-border: 2px solid #eff0f2; --new-ui-btn-danger-bg: #ff23721a; @@ -42,10 +45,11 @@ --border-color2: #2c2c2c; --bg-audio: #303030; - --new-ui-bg-app-color: #303035; + --new-ui-bg-app-color: #151518; --new-ui-border: 1px solid #40404e; - --new-ui-main-color: #151518; + --new-ui-main-color: #1d1d21; --new-ui-gray-color: #a4aab5; + --new-ui-element-bg: #1d1d21; --new-ui-text-color: white; --new-ui-btn-danger-bg: #ff23721a; --new-ui-ctrl-f-button-bg: #242428; @@ -68,6 +72,13 @@ body { font-feature-settings: 'lnum' 1; } +button { + outline: none; + border: none; + background-color: transparent; + cursor: pointer; +} + a { color: inherit; text-decoration: none; @@ -151,8 +162,12 @@ p { /* Изменение цвета активного элемента в выпадающем списке */ .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 { @@ -414,3 +429,54 @@ textarea { transition: all; transition-duration: 250ms; } + +.title-h1 { + font-weight: 600; + font-size: 34px; + @media (max-width: 768px) { + font-size: 28px; + } +} + +.title-h2 { + font-weight: 600; + font-size: 28px; + + @media (max-width: 768px) { + font-size: 24px; + } +} + +.item-enter { + opacity: 0; + transform: translateY(-20px); +} +.item-enter-active { + opacity: 1; + transform: translateY(0); + transition: all 500ms; +} +.item-exit { + opacity: 1; + transform: translateY(0); +} +.item-exit-active { + opacity: 0; + transform: translateY(-20px); + transition: all 500ms; +} + +.opacity-enter { + opacity: 0; +} +.opacity-enter-active { + opacity: 1; + transition: all 500ms; +} +.opacity-exit { + opacity: 1; +} +.opacity-exit-active { + opacity: 0; + transition: all 500ms; +} @@ -0,0 +1,11 @@ + + + \ No newline at end of file @@ -0,0 +1,4 @@ + + + \ No newline at end of file @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file @@ -0,0 +1,11 @@ + + + + + + + + + + + @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,6 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,8 @@ + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,4 @@ + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + + + @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -1,8 +1,8 @@ -import { AudioModel } from '#/app/types/model' +import { IShortModel } 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 @@ +export * from './styles.static' \ No newline at end of file @@ -0,0 +1,8 @@ + + +export interface ImageStyle { + category: string; + image: string; + uid: string; + label: string; +} \ No newline at end of file @@ -0,0 +1,53 @@ +.style { + border-radius: 10px; + position: relative; + &__image { + border-radius: 10px; + width: 100%; + height: 100%; + object-fit: cover; + } + + &__popup { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba($color: #151518, $alpha: 0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + border-radius: 10px; + + opacity: 0; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } + + span { + color: var(--new-ui-text-color); + max-width: 80px; + text-align: center; + font-size: 14px; + } + } + + &__favorite{ + position: absolute; + top: 10px; + right: 10px; + z-index: 1; + background-color: rgba($color: #A4AAB5, $alpha: 0.1); + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + } +} @@ -0,0 +1,39 @@ +import React from 'react' +import { ImageStyle } from '../types' +import styles from './image-style.module.scss' +import Image from 'next/image' +import HeartSvg from '#/assets/svg/heart.svg?react' +import { c } from '#/shared' + +type ImageStyleProps = ImageStyle & + React.HTMLAttributes & { + inFavorites: boolean + onCnageFavorite: Function + } + +export const ScopedImageStyle = ({ + label, + image, + className, + inFavorites, + onCnageFavorite, + ...props +}: ImageStyleProps) => { + return ( + + + {label} + + ) +} @@ -0,0 +1 @@ +export * from './image-style' \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './ui' +export * from './types' \ No newline at end of file @@ -0,0 +1 @@ +export * from './message.routes' \ No newline at end of file @@ -0,0 +1,15 @@ +import { API_URL } from '#/shared/lib/constants' +import axios 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' + + return await axios.post(API_URL + `/media/image/${model}`, dataForSend, { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': HeaderDataType, + }, + }) +} @@ -0,0 +1 @@ +export * from './message' \ No newline at end of file @@ -0,0 +1,20 @@ +export interface MessageSend { + content: string + file?: File | null + info: T +} + +export interface Message { + content: string + created_at: string + elapsed_time: string + file: File | null | string + from_model: boolean + info: object + is_favourite: boolean + is_sent: boolean + uid: string + model: string +} + +export interface OptimisticMessage extends Omit {} \ No newline at end of file @@ -0,0 +1,101 @@ +.container { + padding: 5px; + background-color: var(--new-ui-element-bg); + display: flex; + gap: 30px; + border-radius: 15px; + height: 100%; + + @media screen and (max-width: 1000px) { + flex-direction: column; + } +} + +.description { + padding: 10px; + width: max-content; + min-width: 220px; + display: flex; + flex-direction: column; + justify-content: space-between; + &__trash { + background-color: rgba($color: #b01e1e, $alpha: 0.25) !important; + // margin-left: auto; + } + &__time { + font-weight: 600; + font-size: 20px; + width: max-content; + margin-bottom: 7px; + } + &__content { + color: #a4aab5; + } + &__model { + display: flex !important; + align-items: center; + width: max-content; + gap: 5px; + margin-bottom: 20px; + } + &__controls { + display: flex; + align-items: center; + gap: 5px; + padding-top: 20px; + } +} + +@keyframes skeleton { + 0% { + background-color: rgba($color: #a4aab5, $alpha: 0.1); + } + 100% { + background-color: rgba($color: #a4aab5, $alpha: 0.3); + } +} + +.images { + display: grid; + grid-template-columns: repeat(4, 1fr); + height: 100%; + width: 100%; + gap: 5px; + @media screen and (max-width: 1400px) { + grid-template-columns: repeat(2, 1fr); + } + @media screen and (max-width: 600px) { + grid-template-columns: repeat(1, 1fr); + } + &__skeleton { + width: 100%; + height: 100%; + border-radius: 10px; + background-color: rgba($color: #a4aab5, $alpha: 0.1); + animation: skeleton 1s infinite ease-in-out alternate; + position: absolute; + top: 0; + left: 0; + } + &__imagebox { + position: relative; + } + + &__image { + width: 100%; + height: auto; + border-radius: 10px; + min-height: 314px; + object-fit: cover; + + &_empty { + visibility: collapse; + } + } +} + +.button { + padding: 9px 12px; + background-color: rgba(#a4aab5, 0.1); + border-radius: 8px; +} @@ -0,0 +1,73 @@ +import React, { memo, useMemo, useState } from 'react' +import { Message } from '../types' +import styles from './image-message.module.scss' +import Image from 'next/image' +import { c, formatDate } from '#/shared' +import { CommonBagde } from '#/shared/ui/badge' +import NeuralSvg from '#/assets/svg/neural.svg?react' +import RepeatSvg from '#/assets/svg/repeat.svg?react' +import DownloadSvg from '#/assets/svg/download.svg?react' +import TrashSvg from '#/assets/svg/trash.svg?react' + +interface ImageMessageProps extends Omit { + file: string | null + ref: any +} + +export const ImageMessage = memo(({ file, uid, created_at, model, content }: ImageMessageProps) => { + const formatedDate = useMemo(() => { + return Math.round((new Date().getTime() - new Date(created_at).getTime()) / 1000 / 60) + }, [created_at]) + + const [imageLoadStates, setImageLoadStates] = useState(new Array(4).fill(false)) + + return ( +
+
+
+

{formatedDate} мин. назад

+ + + {model} + +

+ {content.length < 30 ? content : `${content.slice(0, 30)}...`} +

+
+
+ + + +
+
+
+ {new Array(4).fill(file).map((el, index) => ( +
+ + setImageLoadStates((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + } + height={300} + src={el} + alt={`image-${uid}-${index}`} + /> + + {!imageLoadStates[index] &&
} +
+ ))} +
+
+ ) +}) @@ -0,0 +1,11 @@ + + +.popup{ + min-width: 200px; + &__title{ + color: #A4AAB5; + font-weight: 600; + font-size: 16px; + margin-bottom: 20px; + } +} \ No newline at end of file @@ -0,0 +1,29 @@ +import { getPopupById, PopupTemplate } from '#/shared/ui/popup' +import { POPUP_IMAGE_SETTINGS } from '#/shared/ui/popup' +import React, { useEffect } from 'react' +import styles from './image-settings.popup.module.scss' +import BotParamsMap from '#/features/bot-params/bot-params-map' +import { IModelParams } from '#/entities/model-entity' + +export const ImageSettingsPopup = () => { + const popup = getPopupById(POPUP_IMAGE_SETTINGS) + + const botParams = popup.getStoreProperty('botParams') + const currentVersion = popup.getStoreProperty('currentVersion') + + return ( + +
+

НАСТРОЙКИ

+ {currentVersion && ( +
+ +
+ )} +
+
+ ) +} @@ -0,0 +1,2 @@ +export * from './image-message' +export * from './image-settings.popup' \ No newline at end of file @@ -0,0 +1,3 @@ +export * from './types' +export * from './api' +export * from './ui' \ No newline at end of file @@ -0,0 +1,11 @@ +import { API_URL } from '#/shared/lib/constants' +import axios from 'axios' +import { IModel } from '../types' + +export async function getBotParams(slug: string, token?: string) { + return await axios.get(API_URL + `/ml_models/${slug}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,11 @@ +import { API_URL } from '#/shared/lib/constants' +import axios from 'axios' +import { IShortModel } from '../types' + +export async function getModelsImages(token?: string) { + return await axios.get(API_URL + '/ml_models/?category=images', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,2 @@ +export * from './bot.route' +export * from './images-bots.route' @@ -0,0 +1,2 @@ +export * from './use-images-bots' +export * from './use-image-bot' \ No newline at end of file @@ -0,0 +1,118 @@ +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, + } +} @@ -0,0 +1,24 @@ +import { useState } from 'react' +import { getModelsImages } from '../api' +import { useSession } from 'next-auth/react' +import { IShortModel } from '../types' + +export function useImagesBots() { + const [bots, setBots] = useState([]) + + const { data } = useSession() + + async function fetchBots() { + if (!data) return + + const response = await getModelsImages(data.access) + + if (response.status < 400) setBots(response.data) + } + + return { + bots, + fetchBots, + setBots + } +} @@ -0,0 +1 @@ +export * from './model.types' \ No newline at end of file @@ -0,0 +1,55 @@ +type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' + +export interface IShortModel { + uid: string + title: string + description: string + slug: string + image: string + blocked: boolean + actual_stat: { + generation_time: string + tokens_cost: string + } +} + +export interface IModel { + uid: string + title: string + description: string + slug: string + image: string + settings: { is_active: boolean } + parameters: IModelParams[] + versions: IModelVersions[] + inputs: IModelInputs[] +} +export interface IModelParams { + name: string + description: string + key: string + type: string + required: boolean + values: { + availables: string[] + default: any + end: number + start: number + step: number + } + versions: string[] +} +export interface IModelVersions { + name: string + description: string + default: boolean + slug: string +} +export interface IModelInputs { + // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' + type: string + required: boolean + versions: string[] +} + +export default ModelForChats @@ -0,0 +1,3 @@ +export * from './api' +export * from './model' +export * from './types' \ No newline at end of file @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { Stack } from '@mui/material' import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' @@ -21,7 +21,7 @@ export default function BotParamsMap({ params, currentVersion }: IProps) { dispatch(setParams(payload)) } - React.useEffect(() => { + useEffect(() => { if (currentVersion && params) { const res = params.map((el) => el.versions.length !== 0 && !el.versions.includes(currentVersion) ? null : el @@ -0,0 +1 @@ +export * from './use-image-bot-create' \ No newline at end of file @@ -0,0 +1,56 @@ +import { getUserBalance } from '#/entities/balance' +import { Message, MessageSend, sendImage } from '#/entities/message' +import { useAppDispatch } from '#/app/store/store' +import { Device } from '#/shared/lib/types/entities' +import { formDataHelper } from '#/widgets/messages' +import { useSession } from 'next-auth/react' +import { useState } from 'react' + +export function useImageBotCreateImage( + showError: (message: string) => void, + type: string, + device: Device, + setLoading: (value: boolean) => void, + setMessages: React.Dispatch> +) { + const [isComplete, setIsComplete] = useState(false) + + const { data } = useSession() + + const dispatch = useAppDispatch() + + const createImage = async (dataForSend: MessageSend) => { + const { content, file } = dataForSend + + setIsComplete(false) + setLoading(true) + + const dataSending = file ? formDataHelper(file, dataForSend) : dataForSend + + const { data: messages } = await sendImage(type, dataSending, data?.access) + + if (typeof messages === 'string') { + showError(messages) + setLoading(false) + return + } + + dispatch(getUserBalance(data?.access)) + + setMessages((prev: Message[]) => { + if (!prev || !prev.length) return messages + + if (device === 'desktop') { + return [...messages, ...prev] + } + return [...prev, ...messages] + }) + + setIsComplete(true) + } + + return { + createImage, + isComplete, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-bot-filters' \ No newline at end of file @@ -0,0 +1,16 @@ +import { useAppSelector } from '#/app/store/store' +import { useState } from 'react' + +export function useImagesBotFilters() { + const [openFiltersMobile, setOpenFiltersMobile] = useState(false) + const [params, setParams] = useState(false) + const includeParams = useAppSelector((state) => state.params.params) + + return { + openFiltersMobile, + setOpenFiltersMobile, + params, + setParams, + includeParams, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-uniq-input' \ No newline at end of file @@ -0,0 +1,64 @@ +import { useShowData } from '#/shared' +import { MessageSend } from '#/shared/lib/types/model' +import { useState, ChangeEvent } from 'react' + +export function useImagesUniqInput( + version: string, + includeParams: object, + createImage: (dataForSend: MessageSend) => any +) { + const [image, setImage] = useState(null) + + const { error, showError } = useShowData() + + function onLoadImage(event: ChangeEvent) { + if (event.target.files) { + setImage(event.target.files[0]) + } + } + + function onCreateImage(input: string, required: (string | null)[]) { + // про switch не слышали люди)) + if (required.includes('text') && (input === '' || input === null)) { + showError('Введите сообщение!') + return false + } + if (required.includes('image') && image === null) { + showError('Прикрепите изображение!') + return false + } + if (required.includes('zip') && image === null) { + showError('Прикрепите архив!') + return false + } + + // Снова какой то пиз**ц + let data = {} + if (version === '') { + data = { + ...includeParams, + } + } else { + data = { + version: version, + ...includeParams, + } + } + + createImage({ + content: input, + file: image, + info: { + ...data, + }, + }) + return true + } + + return { + image, + setImage, + onLoadImage, + onCreateImage, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-bot-pagination' \ No newline at end of file @@ -0,0 +1,102 @@ +import { useShowData } from '#/shared' +import { getImagesGalery, useMessagesStore } from '#/widgets/messages' +import { useMediaQuery } from '@mui/material' +import { useSession } from 'next-auth/react' +import { useMemo, useRef } from 'react' +import { Limit, LimitSize } from '../types' + +export function useImageBotPagination( + container: React.RefObject +) { + const refScroll = useRef(null) + const offset = useRef(0) + + const { messages, loading, setLoading, addMessages } = useMessagesStore() + + const { showError } = useShowData() + + const { data } = useSession() + + const limits: Record = { + small: { + active: useMediaQuery('(max-height: 600px)'), + limit: 10, + firstLimit: 20, + }, + medium: { + active: useMediaQuery( + '(min-height: 600px) and (max-height: 900px)' + ), + limit: 15, + firstLimit: 30, + }, + large: { + active: useMediaQuery('(min-height: 900px)'), + limit: 20, + firstLimit: 40, + }, + } + + const revertedMessages = useMemo(() => [...messages].reverse(), [messages]) + + const fetchMessages = async (count?: number) => { + if (!data) return + + setLoading(true) + const { data: answer, ...response } = await getImagesGalery( + data.access, + offset.current, + count || 10 + ) + setLoading(false) + + if (response.status >= 400 || !Array.isArray(answer)) + return showError('Ошибка загрузки чата') + + addMessages(answer) + + offset.current = offset.current + answer.length + } + + const callback = async function (entries: IntersectionObserverEntry[]) { + if (!entries[0].isIntersecting) return + + if (!refScroll.current) return + + const scrollBottom = + container.current!.scrollHeight - container.current!.scrollTop + + const active = Object.values(limits).find((item) => item.active) + + if (offset.current > 0) await fetchMessages(active?.limit) + else await fetchMessages(active?.firstLimit) + + setTimeout(() => { + if (!container.current) return + container.current.scroll({ + top: container.current!.scrollHeight - scrollBottom + 70, + behavior: 'smooth', + }) + }, 500) + } + + function onObserverMounted() { + if (!refScroll.current) return + + const observer = new IntersectionObserver(callback, { + rootMargin: '400px', + }) + + observer.observe(refScroll.current) + } + + return { + refScroll, + onObserverMounted, + messages, + loading, + setLoading, + fetchMessages, + revertedMessages, + } +} @@ -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,3 @@ + + +export * from './use-images-library' \ No newline at end of file @@ -0,0 +1,4 @@ +export interface ImageWithState { + image: string + state: boolean +} \ No newline at end of file @@ -0,0 +1,73 @@ +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import { ImageWithState } from './types' +import { Message } from '#/shared/lib/types/model' + +export const useImagesLibrary = ( + images: string[], + current: string | null, + reverse: boolean, + setModal: Dispatch> +) => { + const [imagesWithState, setImagesWithState] = useState(getImagesWithState(images)) + const [initialCount, setInitialCount] = useState(0) + + const esc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + setModal(false) + } + } + + function getImagesWithState(images: string[]) { + const imagesWithState = images.map((image) => ({ + image, + state: false, + })) + + return imagesWithState + } + + function updateImageState(image: string, state: boolean) { + setImagesWithState((prev) => + prev.map((item) => (item.image === image ? { ...item, state } : item)) + ) + } + + const currentImageIndex = useMemo( + () => + !current ? undefined : imagesWithState.findIndex((image) => image.image === current), + [imagesWithState, current] + ) + + useEffect(() => { + if (initialCount === images.length) { + return + } + + setInitialCount(images.length) + + const filteredMessages = images.filter((item) => + !imagesWithState.find((i) => i.image === item) ? true : false + ) + + if (filteredMessages.length === images.length) { + return setImagesWithState(getImagesWithState(filteredMessages)) + } + + setImagesWithState([...imagesWithState, ...getImagesWithState(filteredMessages)]) + }, [images]) + + useEffect(() => { + window.addEventListener('keydown', esc) + return () => window.removeEventListener('keydown', esc) + }, []) + + return { + imagesWithState, + currentImageIndex, + initialCount, + setInitialCount, + setImagesWithState, + updateImageState, + } +} @@ -0,0 +1,54 @@ +import { debounce } from 'lodash' +import { useCallback, useEffect, useState } from 'react' +import { Swiper as SwiperCore } from 'swiper' +import { ImageWithState } from './types' + +export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefined, reverse: boolean) => { + const [swiper, setSwiper] = useState(null) + + + const keydown = (e: KeyboardEvent) => { + if (e.key === 'ArrowRight') { + e.preventDefault() + slideNext() + } + if (e.key === 'ArrowLeft') { + e.preventDefault() + slidePrev() + } + } + + const slidePrev = function () { + const result = swiper?.slidePrev() + + if (!result && onSlideFalse && !reverse) { + onSlideFalse() + } + } + + const slideNext = function () { + const result = swiper?.slideNext() + + if (!result && onSlideFalse && reverse) { + onSlideFalse() + } + } + + useEffect(() => { + document.addEventListener('keydown', keydown, true) + return () => { + document.removeEventListener('keydown', keydown, true) + } + }, [swiper]) + + // useEffect(() => { + // swiper?.slideTo(currentImageIndex || 0) + // }, [currentImageIndex]) + + return { + swiper, + setSwiper, + slidePrev, + slideNext, + } +} @@ -0,0 +1,193 @@ +import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import Image from 'next/image' + +import styles from './modal-styles.module.scss' +import { ArrowDropDown } from '@mui/icons-material' +import { c, Loader, TooltipCustom } from '#/shared' +import { useImagesLibrary } from '../model' + +import { Swiper, SwiperSlide } from 'swiper/react' +import 'swiper/css' +import { useLibrarySwiper } from '../model/use-swiper' +import { ImageIcons, useImageIcons } from '#/widgets/messages' +import { Typography } from '@mui/material' +import { Message } from '#/entities/message' + +interface IProps { + modal: boolean + setModal: Dispatch> + current: string | null + setCurrent?: (value: string | null) => void + onSlideFalse?: (...args: any) => any + reverse?: boolean + images: Message[] +} + +export default function FullScreenModal({ modal, setModal, current, images, onSlideFalse, reverse = false }: IProps) { + const filteredMessages = useMemo(() => images.map((message) => message.file as string), [images]) + + const { imagesWithState, updateImageState, currentImageIndex, initialCount } = useImagesLibrary( + filteredMessages, + current, + reverse, + setModal + ) + + const { swiper, setSwiper, slideNext, slidePrev } = useLibrarySwiper(onSlideFalse, reverse) + + const { downloadFile } = useImageIcons() + + useEffect(() => { + if (!(initialCount < images.length && swiper)) return + + if (reverse) { + setTimeout(() => swiper.slideTo(initialCount, 1000), 1000) + return + } + + setTimeout(() => swiper.slideTo(images.length - initialCount - 1, 1000), 1000) + }, [images]) + + return ( +
setModal(false)} + > +
+
+
+ { + e.stopPropagation() + if (!swiper) return + const { file, content } = images[swiper.activeIndex] + downloadFile(file as string, content) + }} + width='24' + height='24' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + +
+
+ { + e.stopPropagation() + if (!swiper) return + const { file } = images[swiper.activeIndex] + window.open(file as string, '_blank') + }} + width='24' + height='24' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + +
+
setModal(false)}> + + + +
+
+
+ + {modal && ( + setSwiper(swiper)} + > + {imagesWithState.map(({ image, state }, index) => ( + + {!state && } +
+ {!image.includes('.svg') ? ( + e.stopPropagation()} + onLoadingComplete={() => { + updateImageState(image, true) + }} + loading='lazy' + src={image} + width={'1500'} + height={'1500'} + alt='К сожалению, изображение не загрузилось' + /> + ) : ( + e.stopPropagation()} + onLoad={() => updateImageState(image, true)} + alt='К сожалению, изображение не загрузилось' + className={styles.image_style} + /> + )} +
+
+ ))} +
+ )} + +
+
+ ) +} @@ -0,0 +1 @@ +export { default as ImageModal } from './full-screen-modal' @@ -0,0 +1,105 @@ +.close_block { +} +.download { +} +.actions { + position: absolute; + z-index: 105; + cursor: pointer; + right: 25px; + top: 25px; + display: flex; + align-items: center; + gap: 15px; +} + +.arrow { + position: absolute; + z-index: 1205; + cursor: pointer; + top: 50%; + + width: 50px; + height: 50px; + + background: transparent; + border: none; + outline: none; + + svg { + fill: white; + width: 50px; + height: 50px; + } + + &_left { + transform: translateY(-50%) rotate(90deg); + left: -40px; + @media screen and (max-width: 768px) { + left: -3px; + } + } + + &_right { + transform: translateY(-50%) rotate(-90deg); + right: -50px; + + @media screen and (max-width: 768px) { + right: -10px; + } + } +} + +.image_block { + position: absolute; + padding: 0 20px; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 105; + margin: auto; + width: fit-content; + height: fit-content; +} + +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.image_style { + width: auto; + max-width: 70vw; + height: 100%; + position: relative; + user-select: none; + display: flex; + justify-content: center; + align-items: center; + img { + width: 100%; + height: 100%; + object-fit: contain; + user-select: none; + } +} + +.slide { + width: 80vw !important; + display: flex !important; + align-items: center; + justify-content: center; + height: 100% !important; + max-height: unset; +} + +.swiper { + position: relative; + width: 100%; + max-width: 80vw; + max-height: 1000px; + height: 80vh; +} @@ -0,0 +1 @@ +export * from './ui' @@ -0,0 +1,23 @@ +import { ImageStyle } from '#/entities/image-style' +import { getModalById, PLATE_IMAGE_STYLES } from '#/features/modals' +import { useChosenStyle } from '#/widgets/images-style' +import { useEffect } from 'react' + +export function useImageInputStyles() { + const modal = getModalById(PLATE_IMAGE_STYLES) + + const { setStyle } = useChosenStyle() + + function onSetStyle(data: ImageStyle) { + setStyle(data) + modal.setState(false) + } + + function onStylesButtonClick() { + modal.setState(true, { + callback: onSetStyle, + }) + } + + return { onStylesButtonClick } +} @@ -0,0 +1,74 @@ +import { useAppSelector } from '#/app/store/store' +import { MessageSend, sendImage } from '#/entities/message' +import { IModel } from '#/entities/model-entity' +import { useImagesUniqInput } from '#/features/image-bot-input' +import { useMessagesStore } from '#/widgets/messages' +import { useSession } from 'next-auth/react' +import { useState } from 'react' + +export function useImageInput( + model: IModel | null, + version: string, + container: React.RefObject +) { + const [text, setText] = useState('') + const [image, setImage] = useState(null) + const [loading, setLoading] = useState(false) + + const { setMessages, messages, addOptimistic } = useMessagesStore() + + const includeParams = useAppSelector((state) => state.params.params) + + const { data: session } = useSession() + + function createMessage() { + const data = { + file: image, + content: text, + info: { + ...includeParams, + version, + }, + } + + onSend(data) + } + + async function onSend(param: MessageSend) { + if (!model || !session) return + + addOptimistic(param.content, param.info, model.slug) + + setLoading(true) + + setTimeout(() => { + if (!container.current) return + + const scrollBottom = container.current.scrollHeight - container.current.scrollTop + + container.current.scroll({ + top: container.current.scrollHeight + scrollBottom, + behavior: 'smooth', + }) + }, 500) + + const { data } = await sendImage(model.slug, param, session.access) + + setLoading(false) + + if (typeof data === 'string') return + + setMessages([...data, ...messages]) + } + + return { + onSend, + text, + setText, + image, + setImage, + sendImage, + createMessage, + loading, + } +} @@ -0,0 +1,2 @@ +export * from './image-input-styles' +export * from './image-input' \ No newline at end of file @@ -0,0 +1,122 @@ +.container { + display: flex; + align-items: center; + gap: 22px; +} +.wrapper { + display: flex; + flex-direction: column; + gap: 20px; + padding: 20px; + border-radius: 15px; + border: 1px solid rgba($color: #a4aab5, $alpha: 0.2); + background: var(--new-ui-element-bg); +} + +.controls { + display: flex; + flex-direction: column; + gap: 5px; +} + +.stroke { + height: 60px; + width: 1px; + background-color: #40404e; + + @media screen and (max-width: 800px) { + display: none; + } +} + +.rounded { + background-color: rgba($color: #a4aab5, $alpha: 0.1); + border-radius: 100%; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + + &__svg { + padding-left: 2px; + padding-top: 2px; + } +} + +.buttons { + display: flex; + align-items: center; + gap: 10px; + + @media screen and (max-width: 800px) { + flex-direction: column; + } +} + +.button { + background-color: rgba($color: #a4aab5, $alpha: 0.05); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #a4aab5; + gap: 5px; + height: 61px; + width: 61px; + font-size: 12px; + font-weight: 500; + border-radius: 15px; +} + +.textarea { + border: none; + outline: none; + min-width: 400px; + resize: none; + padding: 0 10px; + height: 50px !important; + + @media screen and (max-width: 800px) { + height: 70px !important; + min-width: 300px; + } + + @media screen and (max-width: 500px) { + min-width: 200px; + } +} + +.main { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + @media screen and (max-width: 800px) { + display: none; + } + &__settings { + position: relative; + } +} + +.mobcontols { + display: none; + gap: 10px; + position: relative; + &__generate { + width: 100%; + } + &__settings { + min-width: 50px; + min-height: 50px; + padding: 0px; + display: flex; + justify-content: center; + align-items: center; + } + + @media screen and (max-width: 800px) { + display: flex; + } +} @@ -0,0 +1,111 @@ +import React, { MouseEventHandler, useState } from 'react' +import styles from './image-input.module.scss' +import { c } from '#/shared' +import BranchesSvg from '#/assets/svg/branches.svg?react' +import PaintSvg from '#/assets/svg/paint.svg?react' +import RatioSvg from '#/assets/svg/ratio.svg?react' +import CubesSvg from '#/assets/svg/cubes.svg?react' +import SettingsSvg from '#/assets/svg/settings.svg?react' +import { SendBtn } from '#/shared/ui/send-button' +import { getModalById, PLATE_IMAGE_STYLES } from '#/features/modals' +import { useImageInput, useImageInputStyles } from '../model' +import { IModel } from '#/entities/model-entity' +import { CommonButton } from '#/shared/ui/button' +import { ImageSettingsPopup } from '#/entities/message' +import { getPopupById, POPUP_IMAGE_SETTINGS } from '#/shared/ui/popup' + +export interface ImageInputProps { + model: IModel | null + version: string + container: React.RefObject +} + +export const ImageInput = ({ model, version, container }: ImageInputProps) => { + const { createMessage, text, setText, loading } = useImageInput(model, version, container) + + const { onStylesButtonClick } = useImageInputStyles() + + const imageSettingsPopup = getPopupById(POPUP_IMAGE_SETTINGS) + + return ( +
+
+
+ + +
+ +
+ + +
+
+
+
+ + +
+ +
+
+ +
+ + Сгенерировать + + { + if (!model) return + + imageSettingsPopup.toogleState({ + botParams: model.parameters, + currentVersion: version, + }) + }} + className={styles.mobcontols__settings} + variant='primary-outline' + > + + + + +
+
+ ) +} @@ -0,0 +1 @@ +export * from './image-input' @@ -0,0 +1,2 @@ +export * from './ui' +export * from './model' @@ -0,0 +1 @@ +export * from './modals.config' \ No newline at end of file @@ -0,0 +1,2 @@ + +export const PLATE_IMAGE_STYLES = 'plate-image-styles' \ No newline at end of file @@ -0,0 +1 @@ +export * from './store.modals' \ No newline at end of file @@ -0,0 +1,78 @@ +import { create } from "zustand"; + +type Set = { + (partial: (state: PlatesStore) => Partial): void; +}; + +type Get = () => PlatesStore; + +function makeModalInstance(set: Set, get: Get, key: string) { + const ModalInstance: Modal = { + id: key, + state: false, + store: {}, + setState(state, store) { + const modals = get().modals; + + store = store ?? {}; + + set(() => ({ + modals: { ...modals, [this.id]: { ...modals[key], state, store } }, + })); + }, + setStoreProperty(key, value) { + const modals = get().modals; + + this.store[key] = value; + + set(() => ({ + modals: { ...modals, [this.id]: { ...modals[key], store: this.store } }, + })); + }, + getStoreProperty(key) { + return this.store[key]; + }, + }; + + return ModalInstance; +} + +export interface Modal { + id: string; + state: boolean; + store: Record; + setState: (state: boolean, store?: Record) => void; + setStoreProperty: (key: string, value: any) => void; + getStoreProperty: (key: string) => T | undefined; +} + +export interface PlatesStore { + modals: Record; + setModal: (key: string) => void; + getModal: (key: string) => Modal; +} + +export const usePlatesStore = create((set, get) => { + const modals: Record = {}; + + function setModal(key: string) { + set((state) => ({ + modals: { + ...state.modals, + [key]: Object.assign({}, makeModalInstance(set, get, key)), + }, + })); + } + + function getModal(key: string) { + return ( + get().modals[key] ?? Object.assign({}, makeModalInstance(set, get, key)) + ); + } + + return { + modals, + setModal, + getModal, + }; +}); @@ -0,0 +1 @@ +export { default as PlateTemplate } from "./template"; \ No newline at end of file @@ -0,0 +1,106 @@ +.template { + position: fixed; + width: 100%; + max-width: calc(100dvw); + height: 100dvh; + background-color: rgba(0, 0, 0, 0.4); + top: 0; + left: 0; + transition: all 300ms ease-in-out; + display: flex; + overflow: hidden; + opacity: 0; + visibility: hidden; + + &_visible { + opacity: 1; + visibility: visible; + z-index: 1000; + } + + &_invisible { + opacity: 0; + visibility: hidden; + z-index: -1; + } + + &_align-x-left { + justify-content: flex-start; + + > * { + margin-left: 10px; + } + } + + &_align-x-center { + justify-content: center; + } + + &_align-x-right { + justify-content: flex-end; + + > * { + margin-right: 10px; + } + } + + &_align-y-top { + align-items: flex-start; + + > * { + margin-top: 10px; + } + } + + &_align-y-center { + align-items: center; + } + + &_align-y-bottom { + align-items: flex-end; + + > * { + margin-bottom: 10px; + } + } + + &__content { + background-color: var(--new-ui-main-color); + border-radius: 20px; + position: relative; + overflow: hidden; + height: fit-content; + transition: all 0.5s ease-in-out; + } + + &__header { + display: flex; + justify-content: space-between; + padding: 25px; + } + + &__close-button { + position: relative; + z-index: 1; + transform: rotate(45deg); + } + + &__content-body { + transition: all 0.5s ease-in-out; + padding: 0px 32px 32px 32px; + overflow: hidden; + + @media screen and (max-width: 1000px) { + padding: 0px 10px 10px 10px; + } + + } + + &__animation { + transition: all 0.3s ease-in-out; + } + + &__animation-behavior { + transform: translateX(50px); + } +} @@ -0,0 +1,88 @@ +'use client' +import { useEffect } from 'react' +import { c } from '#/shared' +import { usePlatesStore } from '../model' +import styles from './template.module.scss' + +import PlusIcon from '#/assets/svg/plus.svg?react' +import { useThemeAndDevice } from '#/shared/lib/hooks' + +export type AlignX = 'left' | 'center' | 'right' +export type AlignY = 'top' | 'center' | 'bottom' + +export interface PlatesTemplateProps { + id: string + children: React.ReactNode + title?: string + closeModal?: () => void + hasTemplate?: boolean + hasBg?: boolean + alignX?: AlignX + alignY?: AlignY + animationClass?: string + animationBehaviorClass?: string +} + +export default function PlatesTemplate({ + id, + title, + children, + alignX = 'center', + alignY = 'center', + hasTemplate = true, + hasBg = true, + animationClass = styles['template__animation'], + animationBehaviorClass = styles['template__animation-behavior'], +}: PlatesTemplateProps) { + const { setModal, getModal } = usePlatesStore() + + const modal = getModal(id) + + const { theme } = useThemeAndDevice() + + useEffect(() => setModal(id), []) + + return ( +
{ + if (hasBg) modal.setState(false) + }} + > + {hasTemplate ? ( +
e.stopPropagation()} + > +
+

{title}

+ +
+
{children}
+
+ ) : ( +
{children}
+ )} +
+ ) +} @@ -0,0 +1,9 @@ +import { usePlatesStore } from './model'; + +export function getModalById(id: string) { + const { getModal } = usePlatesStore(); + return getModal(id); +} + +export * from './ui' +export * from './config' \ No newline at end of file @@ -4,7 +4,14 @@ import Link from 'next/link' import styles from '#/shared/styles/chats-bot-pages.module.scss' -export default function Title(props: any) { +export interface TitleProps { + type: string + title: string + linkBack?: string + rightSlot?: React.ReactNode +} + +export default function Title(props: TitleProps) { return ( @@ -12,11 +19,19 @@ export default function Title(props: any) { {' '} {props.type} •{' '} -  {props.title} + +  {props.title} + - - {props.title} - +
+ + {props.title} + +
{props.rightSlot}
+
) } @@ -3,4 +3,4 @@ import { getDefaultLayout } from "#/widgets/layouts"; ChatBotsPage.getLayout = getDefaultLayout({ titlePage: 'Чат-боты' }); -export default ChatBotsPage \ No newline at end of file +export default ChatBotsPage @@ -3,4 +3,4 @@ import { getDefaultLayout } from "#/widgets/layouts"; ImageModelsPage.getLayout = getDefaultLayout({ titlePage: 'Изображения' }) -export default ImageModelsPage \ No newline at end of file +export default ImageModelsPage @@ -1,4 +1,4 @@ -import React, { ReactElement, ReactNode } from 'react' +import React, { ReactElement, ReactNode, useEffect } from 'react' import { Provider } from 'react-redux' import { StyledEngineProvider, ThemeProvider } from '@mui/material' import axios from 'axios' @@ -12,7 +12,7 @@ import { store } from '#/app/store/store' import ErrorBoundary from './error-boundary' -import '#/app/styles/globals.css' +import '#/app/styles/globals.scss' import '#/app/styles/styles-pages/system.scss' import { NextPage } from 'next' import { pingFangFont } from '#/shared/lib/constants/font/font' @@ -21,6 +21,8 @@ axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, }) +axios.defaults.validateStatus = (status) => status < 500 + const inter = Raleway({ subsets: ['latin'] }) export type NextPageWithLayout

= NextPage & { @@ -35,6 +35,8 @@ export default function Document() {

+ + @@ -1,7 +1,8 @@ import axios from 'axios' -import { IModel, IShortModel } from '#/shared/api/models/models' +import { IModel } from '#/shared/api/models/models' import { API_URL } from '#/shared/lib/constants' +import { IShortModel } from '#/entities/model-entity' const model_api = { async getBots(token?: string): Promise { @@ -247,7 +247,7 @@ export function useModelImages(showError: (message: string) => void, type: st const { getData, sendData } = ModelsWithImagesEndpoints - const [messages, setMessages] = useState(null) + const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(false) @@ -28,3 +28,43 @@ } } } + +.blocked { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 20px; + right: 20px; + background-color: white; + border-radius: 100px; + padding-right: 20px; + padding: 8px 12px; + + span { + color: #ff2372; + font-weight: 500; + padding-left: 10px; + font-size: 14px; + } +} +.locked { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 15px; + padding-right: 20px; + + span { + color: white; + padding-top: 10px; + font-weight: 500; + } +} @@ -2,8 +2,11 @@ import React, { useMemo } from 'react' import { Box, Button, Typography } from '@mui/material' 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.module.scss' +import { useThemeAndDevice } from '../lib/hooks' interface IProps { text: string @@ -11,17 +14,20 @@ interface IProps { title: string slug: string uid: string + blocked: boolean accessed_models?: string[] | null } -export const Card = ({ text, icon, title, uid, slug, accessed_models }: IProps) => { +export const Card = ({ text, icon, title, blocked, slug, accessed_models }: IProps) => { const link = useMemo(() => { return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `/images/${slug}` }, [accessed_models]) + const { theme } = useThemeAndDevice() + return ( - + {text} - {accessed_models && !accessed_models.includes(slug) && ( - - - - - + + + Модель недоступна + + + ) : ( + accessed_models && + !accessed_models.includes(slug) && ( +
+ + Недоступно в текущем тарифе +
+ ) )}
@@ -0,0 +1,12 @@ +import dynamic from 'next/dynamic'; + +type ClientOnlyProps = { children: JSX.Element }; +const ClientOnly = (props: ClientOnlyProps) => { + const { children } = props; + + return children; +}; + +export default dynamic(() => Promise.resolve(ClientOnly), { + ssr: false, +}); @@ -0,0 +1 @@ +export { default as ClientOnly } from './client-only' @@ -0,0 +1 @@ +export * from './ui' @@ -21,3 +21,16 @@ export function formatAndSortDates(inputArray: { date: string; value: number }[] return el }) } + +export function formatDate( + date: string | number | Date, + options: Intl.DateTimeFormatOptions, + lang?: string +) { + if (typeof date !== "object") { + date = new Date(typeof date === "number" ? date : Date.parse(date)) + } + + const formatter = Intl.DateTimeFormat("ru", options) + return formatter.format(date) +} @@ -3,3 +3,5 @@ export { getTypeDevice } from './get-type-device' export { useConcat } from './reactive-concat' export * from './get-type-device' export * from './get-random-image' +export * from './string' +export * from './date-helper' @@ -0,0 +1,3 @@ +export function c(...args: Array) { + return args.filter(Boolean).join(" ") +} @@ -1,13 +1,11 @@ import { useAppSelector } from '#/app/store/store' +import { DeviceOs } from '../types/entities' -interface IThemeAndDevice { - theme: 'dark' | 'light' - desktop: boolean -} -export const useThemeAndDevice = (device?: 'desktop' | 'mobile'): IThemeAndDevice => { +export const useThemeAndDevice = (device?: 'desktop' | 'mobile', deviceOs?: DeviceOs) => { const theme = useAppSelector((state) => state.theme.theme) const desktop = device === 'desktop' + const ios = deviceOs === 'ios' - return { theme, desktop } + return { theme, desktop, ios } } @@ -35,10 +35,10 @@ margin: 0; margin-top: 15px; } - .bigTitle { - @media (max-width: 768px) { - display: none; - overflow: hidden; - } +} + +.bigTitle { + @media (max-width: 768px) { + font-size: 26px !important; } } @@ -0,0 +1,16 @@ +.badge { + display: inline-block; + padding: 3px 10px; + &_primary{ + color: var(--air-color); + font-weight: 500; + background-color: rgba($color: #8280ff, $alpha: 0.1); + border-radius: 5px; + } + + &_outline{ + color: rgba($color: #A4AAB5, $alpha: 0.6); + border: 1px solid rgba($color: #A4AAB5, $alpha: 0.6); + border-radius: 10px; + } +} \ No newline at end of file @@ -0,0 +1,18 @@ +import React from 'react' +import styles from './common-badge.module.scss' +import { c } from '#/shared/lib/helpers' + +export type CommonBagdeVariant = 'primary' | 'outline' + +interface CommonBagdeProps extends React.HTMLAttributes { + variant?: CommonBagdeVariant + className?: string +} + +export const CommonBagde = ({ variant = 'primary', children, className, ...props }: CommonBagdeProps) => { + return ( +
+ {children} +
+ ) +} @@ -0,0 +1 @@ +export * from './common-badge' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,57 @@ +.button { + border: none; + outline: none; + background-color: transparent; + padding: 15px 25px; + cursor: pointer; + + &_outline { + border-radius: 15px; + border: 1px solid white; + color: white; + font-weight: 600; + transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; + font-size: 15px; + + &:hover { + background-color: white; + color: black; + } + + &_disabled { + opacity: 0.5; + + &:hover { + background-color: transparent; + color: white; + } + } + } + + &_primary-outline { + border-radius: 15px; + border: 1px solid var(--air-color); + color: var(--air-color); + font-weight: 600; + transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; + font-size: 15px; + + &:hover { + background-color: var(--air-color); + color: white; + } + } + + &_primary { + background-color: var(--air-color); + color: white; + border-radius: 12px; + font-weight: 500; + font-size: 15px; + transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; + &:hover { + background-color: var(--text-color-purple); + color: white; + } + } +} @@ -0,0 +1,32 @@ +import { c } from '#/shared/lib/helpers' +import styles from './common-button.module.scss' + +export type ButtonVariant = 'outline' | 'primary' | 'primary-outline' + +export interface CommonButtonProps extends React.ButtonHTMLAttributes { + children?: React.ReactNode + variant?: ButtonVariant +} + +export const CommonButton = ({ + children, + variant = 'outline', + className, + disabled, + ...props +}: CommonButtonProps) => { + return ( + + ) +} @@ -0,0 +1 @@ +export * from './common-button' \ No newline at end of file @@ -1,5 +1,5 @@ .button { - background-color: #8280ff; + background-color: #8280FF; border-radius: 13px; text-transform: none; color: white; @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,12 @@ +.input { + border: none; + outline: none; + background-color: transparent; + border-radius: 10px; + padding: 12px 20px; + font-size: 16px; + &_primary{ + background-color: rgba($color: #A4AAB5, $alpha: 0.05); + border: 1px solid rgba($color: #A4AAB5, $alpha: 0.2); + } +} \ No newline at end of file @@ -0,0 +1,24 @@ +import React from 'react' +import styles from './common-input.module.scss' +import { c } from '#/shared/lib/helpers' +import { useThemeAndDevice } from '#/shared/lib/hooks' + +export type CommonInputVariant = 'primary' + +interface CommonInputProps extends React.InputHTMLAttributes { + variant?: CommonInputVariant + className?: string +} + +export const CommonInput = ({ variant = 'primary', className, ...props }: CommonInputProps) => { + const { theme } = useThemeAndDevice() + return ( + <> + + + ) +} @@ -0,0 +1 @@ +export * from './common-input' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './loader' \ No newline at end of file @@ -1,14 +1,16 @@ import React from 'react' -import CircularProgress from '@mui/material/CircularProgress' +import CircularProgress, { CircularProgressProps } from '@mui/material/CircularProgress' -export const Loader = (props: any) => { +export interface LoaderProps extends CircularProgressProps {} + +export const Loader = ({ size = 25, thickness = 2, ...props }: LoaderProps) => { return ( ) @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,25 @@ +@keyframes scroll { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-100%); + } +} + +.marque{ + display: flex; + flex-direction: column; + width: 100%; + overflow: hidden; + max-height: 100dvh; + gap: 10px; + &__wrap{ + width: 100%; + height: max-content; + gap: 10px; + display: flex; + flex-direction: column; + animation: scroll var(--scroll-time) linear infinite; + } +} \ No newline at end of file @@ -0,0 +1,30 @@ +import React, { useEffect, useRef } from 'react' +import styles from './common-marque.module.scss' +import { c } from '#/shared/lib/helpers' + +interface CommonMarqueProps { + children: React.ReactNode + className?: string + speed?: number + scrollTime?: number +} + +export const CommonMarque = ({ + children, + className, + scrollTime = 20, +}: CommonMarqueProps) => { + const container = useRef(null) + + useEffect(() => { + if(!container.current) return + container.current.style.setProperty('--scroll-time', `${scrollTime}s`) + }, []) + + return ( +
+
{children}
+
{children}
+
+ ) +} @@ -0,0 +1 @@ +export * from './common-marque' \ 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_IMAGE_SETTINGS = 'popup-image-settings' +export const POPUP_IMAGE_MODEL = 'popup-image-model' @@ -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): 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 + setState: (state: boolean, store?: Record) => void + toogleState: (store?: Record) => void + setStoreProperty: (key: string, value: any) => void + getStoreProperty: (key: string) => T | undefined +} + +export interface PopupStore { + popups: Record + setPopup: (key: string) => void + getPopup: (key: string) => Popup + hideAllPopups: () => void +} + +export const usePopupStore = create((set, get) => { + const popups: Record = {} + + 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,16 @@ +.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); + &_open { + visibility: visible; + opacity: 1; + z-index: 1; + } +} @@ -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 ( +
hideAllPopups()} + className={c( + styles.backdrop, + someIsOpened && styles.backdrop_open + )} + >
+ ) +} @@ -0,0 +1,2 @@ +export * from './template' +export * from './backdrop' @@ -0,0 +1,64 @@ +.popup { + display: flex; + flex-direction: column; + visibility: hidden; + opacity: 0; + position: absolute; + width: fit-content; + min-width: 100px; + min-height: 70px; + transition: all 0.3s ease-in-out; + z-index: 10; + + &_mobtransform { + width: 100vw; + max-height: 80dvh; + position: fixed; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + z-index: 1000; + overflow-y: scroll; + overflow-x: hidden; + transform: translateY(100%) !important; + transition: transform 0.4s ease-in-out, visibility 0.2s ease-in-out, + opacity 0.2s ease-in-out; + + &_open { + transform: translateY(0%) !important; + visibility: visible; + 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); + } + } + &_open { + visibility: visible; + opacity: 1; + } + &_primary { + background-color: var(--new-ui-element-bg); + border-radius: 15px; + padding: 30px; + border: 1px solid rgba($color: #a4aab5, $alpha: 0.1); + } +} @@ -0,0 +1,71 @@ +import React, { useEffect, useMemo } 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' + +export type PopupTemplateVariant = 'primary' + +interface PopupTemplateProps extends React.HTMLAttributes { + id: string + variant?: PopupTemplateVariant + className?: string + vertical?: 'top' | 'bottom' + horizontal?: 'left' | 'right' | 'center' + hasMobileTransform?: 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), []) + + return ( +
+ {children} +
+ ) +} + +export const PopupTemplate = (props: PopupTemplateProps) => { + const isMobile = useMemo(() => window.innerWidth < 1000, []) + + return ( + + {isMobile && props.hasMobileTransform ? ( + createPortal( +