@@ -0,0 +1,7 @@ + + + \ No newline at end of file @@ -0,0 +1,9 @@ + + + + \ No newline at end of file @@ -1,33 +1,18 @@ import { API_URL } from '#/shared/lib/constants' import axios, { AxiosResponse } from 'axios' -import { Message, MessageSend } from '../types' +import { MediaMessageListResponse, Message, MessageSend } from '../types' +import { api } from '#/shared/api' +import { objectToFormdata } from '#/shared/lib/helpers/form' -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 postImageMessage(uid: string, dto: MessageSend) { + return await api.post(`/media/image/${uid}`, objectToFormdata(dto)) +} - return await axios.post(API_URL + `/media/image/${model}`, dataForSend, { - withCredentials: true, +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}`, - 'Content-Type': HeaderDataType, }, }) } - -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}`, - }, - } - ) -} @@ -0,0 +1,34 @@ + import { create } from 'zustand' + import { Message } from '../types' + + export interface ImageBotMessagesStore { + messages: Message[] + loading: boolean + loaded: boolean + setMessages: (messages: Message[]) => void + setLoading: (loading: boolean) => void + setLoaded: (loaded: boolean) => void + } + + export const useImageBotMessages = create((set, get) => { + function setMessages(messages: Message[]) { + set({ ...get(), messages }) + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + function setLoaded(loaded: boolean) { + set({ ...get(), loaded }) + } + + return { + messages: [], + loading: false, + loaded: false, + setLoaded, + setMessages, + setLoading, + } + }) @@ -3,3 +3,6 @@ export * from './chat-bot-messages.store' export * from './use-user-message-actions' export * from './use-user-message-image' export * from './use-chat-messages-events' +export * from './image-bot-messages.store' +export * from './use-image-messages-events' +export * from './use-image-icons' @@ -0,0 +1,60 @@ +import { useCurrentChat } from '#/features/chats' +import { AsyncQueue, EventBus, eventBus } from '#/shared/classes' +import { useEventSource } from '#/shared/lib/event-source' +import { useCallback, useEffect, useRef, useState } from 'react' +import { MessageEventStreamResponse } from '../types' +import { useChatBotMessages } from './chat-bot-messages.store' +import { useImageObjectId } from '#/features/image-object-id' +import { useImageBotMessages } from './image-bot-messages.store' +import { addBase64Padding, base64Decode, validateBase64 } from '#/shared' + +export function useImageMessagesEvents() { + const chunks = useRef([]) + + const { event, addOpenCallback } = useEventSource() + + const { imageObjectId } = useImageObjectId() + + const { messages, setMessages } = useImageBotMessages() + + const callback = useCallback( + ({ id, content }: MessageEventStreamResponse) => { + const joined = chunks.current.join('') + content + + const mimeTypeRegex = /^data:(image\/(jpeg|png|gif|bmp|webp));base64,(.+)$/ + + const matches = chunks.current.at(0) + ? chunks.current[0].match(mimeTypeRegex) + : content.match(mimeTypeRegex) + + const type = matches && matches.at(1) ? matches[1] : 'image/webp' + + const blob = new Blob( + [Uint8Array.from(base64Decode(joined.split(',')[1]), (c) => c.charCodeAt(0))], + { type } + ) + + const file = URL.createObjectURL(blob) + + const { messages } = useImageBotMessages.getState() + + setMessages([...messages.map((m) => (m.uid !== id ? m : { ...m, file }))]) + + chunks.current = [...chunks.current, content] + }, + [chunks] + ) + + const makeEvent = useCallback( + (name: string, onOpened?: (value: Event) => void) => { + chunks.current = [] + event(name, `api/media/images/${imageObjectId}/messages/stream`, callback) + onOpened && addOpenCallback(name, onOpened) + }, + [imageObjectId] + ) + + return { + makeEvent, + } +} @@ -39,8 +39,8 @@ export function useUserMessageActions(message: Message) { if (file && typeof file === 'string') { const newFile = await getBlobFromUrl(file) setFile(newFile ? newFile : null) - } else if (file && file instanceof File) { - setFile(file) + } else if (file && (file as any) instanceof File) { + setFile(file as any) } else { setFile(null) } @@ -4,7 +4,7 @@ export interface MessageSend { info: Record } -export interface Message { +export interface Message { content: string created_at: string elapsed_time: string @@ -19,7 +19,13 @@ export interface Message { export interface OptimisticMessage extends Omit {} + export interface MessageEventStreamResponse { - id: string - content: string + id: string + content: string +} + +export interface MediaMessageListResponse { + id: string + messages: Message[] } @@ -0,0 +1,26 @@ + + +.archive{ + display: flex; + align-items: center; + justify-content: center; + + border: 1px solid var(--air-color); + border-radius: 20px; + width: 250px; + height: 284px; + + @media screen and (max-width: 1000px) { + width: 291px; + height: 291px; + } + + &__text{ + + } + + &__download{ + color: var(--air-color); + margin-top: 8px; + } +} \ No newline at end of file @@ -0,0 +1,23 @@ +import React, { useMemo } from 'react' + +import styles from './archive-image-message.module.scss' +import Link from 'next/link' +import { Message } from '../types' +import { fileIsLink } from '#/shared' + +interface ArchiveImageMessageProps extends Message {} + +export const ArchiveImageMessage = ({ file }: ArchiveImageMessageProps) => { + const link = useMemo(() => (file && fileIsLink(file) ? file : ''), [file]) + + return ( +
+

+ Эта генерация является архивом + + Скачать + +

+
+ ) +} @@ -0,0 +1,53 @@ +@keyframes fadein { + 0% { + background-color: #dcdcdc; + } + 50% { + background-color: #c0c0c0; + } + 100% { + background-color: #dcdcdc; + } +} + +.wrap { + width: 250px; + height: 250px; + position: relative; + border-radius: 15px; + + @media screen and (max-width: 1000px) { + width: 291px; + height: 291px; + } + + &_skeleton { + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + z-index: 100; + width: 100%; + height: 100%; + background-color: #dcdcdc; + animation: fadein 2s infinite; + border-radius: 15px; + } + } + + &__content { + padding-top: 12px; + color: var(--new-ui-gray-color); + } + + &__image { + position: relative; + z-index: 2; + border-radius: 15px; + width: 250px; + height: 250px; + object-fit: cover; + user-select: none; + } +} @@ -0,0 +1,34 @@ +import React, { useMemo, useState } from 'react' + +import styles from './file-image-message.module.scss' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { Message } from '../types' +import { c, cutString } from '#/shared' +import { ImageIcons } from './image-icons' + +interface FileImageMessageProps extends Message {} + +export const FileImageMessage = ({ file, content, uid, ...message }: FileImageMessageProps) => { + const fileContent = useMemo(() => file ?? '', [file]) + + const [loaded, setLoaded] = useState(false) + + return ( + + ) +} @@ -0,0 +1,14 @@ +.wrap { + padding: 12px 9px; + background-color: var(--new-ui-bg-color); + min-width: unset !important; + border-radius: 100px; + + margin-top: 10px; + + &__content{ + display: flex; + flex-direction: column; + gap: 20px; + } +} @@ -0,0 +1,39 @@ +import { PopupTemplate, getPopupById } from '#/shared/ui/popup' +import React from 'react' +import styles from './image-icons-popup.module.scss' +import { Message } from '../types' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { useImageIcons } from '../model' +import BlankLinkSvg from '#/assets/svg/blank-link.svg?react' +import DownloadSvg from '#/assets/svg/download.svg?react' + +interface ImageIconsPopupProps extends Message {} + +export const ImageIconsPopup = ({ uid, file, content }: ImageIconsPopupProps) => { + const popup = getPopupById(uid) + + const { downloadFile } = useImageIcons() + + return ( + +
+ + + + + + +
+
+ ) +} @@ -0,0 +1,16 @@ +.gamburger { + color: white; + position: absolute; + top: 10px; + right: 10px; + z-index: 5; + display: flex; + align-items: center; + flex-direction: column; +} + +:root[data-theme='dark'] { + .gamburger { + color: #303035; + } +} @@ -0,0 +1,29 @@ +import { useAppSelector } from '#/app/store/store' +import { TooltipCustom, c } from '#/shared' +import { Grow, Box } from '@mui/material' +import { Message } from '../types' +import { useImageIcons } from '../model' +import GamburgerSvg from '#/assets/svg/gamburger.svg?react' + +import styles from './image-icons.module.scss' +import { getPopupById } from '#/shared/ui/popup' +import { ImageIconsPopup } from './image-icons-popup' + +export interface ImageIconsProps extends Message {} + +export const ImageIcons = ({ uid, file, content, ...message }: ImageIconsProps) => { + const popup = getPopupById(uid) + + return ( + + ) +} @@ -0,0 +1,3 @@ +.message{ + position: relative; +} \ No newline at end of file @@ -0,0 +1,34 @@ + +import React, { CSSProperties, MouseEventHandler, useEffect, useMemo } from 'react' + +import styles from './image-message.module.scss' +import { c } from '#/shared' +import { Message } from '../types' +import { ArchiveImageMessage } from './archive-image-message' +import { FileImageMessage } from './file-image-message' + +interface ImageMessageProps extends Message { + onClick?: MouseEventHandler + className?: string + style?: CSSProperties +} + +export const ImageOldMessage = ({ file, onClick, className, style, ...message }: ImageMessageProps) => { + const extention = useMemo(() => { + if (!file) return 'unknown' + + const matches = file.match(/\.(\w+)\?/) + + return matches && matches[1] + }, [file]) + + return ( +
+ {extention === 'zip' ? ( + + ) : ( + + )} +
+ ) +} \ No newline at end of file @@ -1,3 +1,6 @@ export * from './image-message' export * from './image-settings.popup' export * from './user-message' +export * from './user-message' +export * from './image-message' +export * from './image-old-message' @@ -1,11 +1,6 @@ -import { API_URL } from '#/shared/lib/constants' -import axios from 'axios' import { ShortModel } from '../types' +import { api } from '#/shared/api' -export async function getModelsImages(token?: string) { - return await axios.get(API_URL + '/ml_models/?category=images', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) +export async function getImageBots() { + return await api.get(`api/media/images/models/`) } @@ -1,3 +1,4 @@ export * from './use-images-bots' export * from './use-chat-bot' export * from './use-chat-bot-params.store' +export * from './use-image-bot-params.store' @@ -0,0 +1,64 @@ +import { create } from 'zustand' +import { Inference, Model, InferenceParams, InferenceInput } from '../types' + +export interface ImageBotParamsStore { + 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 useImageBotParams = 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,66 @@ +import { version } from 'react' +import { getBotParams, getInference } from '../api' +import { makePrivateRequest } from '#/shared/api' +import { useRouter } from 'next/router' +import { useChatBotParams } from './use-chat-bot-params.store' +import { useImageBotParams } from './use-image-bot-params.store' + +export function useImageBot() { + const { + botParams, + setBotParams, + inference, + setInference, + setInferenceParams, + setInferenceValues, + inferenceParams, + inferenceInputs, + setInferenceInputs, + } = useImageBotParams() + + const { query, push } = useRouter() + + 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 } = useImageBotParams.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) + + 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,20 +1,17 @@ import { useState } from 'react' -import { getModelsImages } from '../api' import { useSession } from 'next-auth/react' import { ShortModel } from '../types' +import { getImageBots } from '../api' +import { makePrivateRequest } from '#/shared/api' export function useImagesBots() { const [bots, setBots] = useState([]) - const { data } = useSession() - - async function fetchBots() { - if (!data) return - - const response = await getModelsImages(data.access) + const fetchBots = makePrivateRequest(async () => { + const response = await getImageBots() if (response.status < 400) setBots(response.data) - } + }) return { bots, @@ -18,7 +18,7 @@ export interface ModelTag { } export interface Model { - uid: string + id: string title: string enabled: boolean description: string @@ -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,16 @@ +import { POPUP_IMAGE_BOT_PARAMS, PopupTemplate } from '#/shared/ui/popup' +import React from 'react' +import styles from './image-bot-options-popup.module.scss' +import { ImageModelOptions } from '#/widgets/image-model-options' + +export const ImageBotOptionsPopup = () => { + return ( + +
+
+ +
+
+
+ ) +} @@ -22,21 +22,20 @@ export function ImageModelCard({ title, slug, accessed_models, + enabled, tags, }: ImageModelCardProps) { const link = useMemo(() => { return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' - : `images/${slug}` + : `deprecated/${slug}` }, [accessed_models]) - const { theme } = useThemeAndDevice() - return ( - {/*
{description}

- {blocked && ( + {!enabled && (
- - Модель недоступна - + Модель недоступна
)} {accessed_models && !accessed_models.includes(slug) && ( @@ -88,7 +85,7 @@ export function ImageModelCard({
))} - */} + ) } @@ -1,2 +1,3 @@ export * from './chat-model-card' -export * from './image-model-card' \ No newline at end of file +export * from './image-model-card' +export * from './image-bot-options-popup' \ No newline at end of file @@ -4,7 +4,7 @@ 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' +import { useMediaQuery } from 'usehooks-ts' export interface ChatItemActionsPopupProps extends PopupTemplateProps { id: string @@ -14,7 +14,7 @@ export interface ChatItemActionsPopupProps extends PopupTemplateProps { export const ChatItemActionsPopup = ({ id, setRename, ...props }: ChatItemActionsPopupProps) => { const { onRemoveChat } = useChatActions() - const isMobile = useMediaQuery({ query: '(max-width: 1000px)' }) + const isMobile = useMediaQuery('(max-width: 1000px)') const popup = getPopupById(id) @@ -1,62 +1,71 @@ import { getUserBalance } from '#/entities/balance' -import { Message, MessageSend, sendImage } from '#/entities/message' +import { + Message, + MessageSend, + postImageMessage, + useImageBotMessages, + useImageMessagesEvents, +} from '#/entities/message' import { useAppDispatch } from '#/app/store/store' import { Device } from '#/shared/lib/types/entities' -import { formDataHelper } from '#/widgets/messages' +import { MutableRefObject, RefObject, useState } from 'react' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' import { useSession } from 'next-auth/react' -import { Dispatch, RefObject, SetStateAction, useEffect, useState } from 'react' +import { makePrivateRequest } from '#/shared/api' +import { useEventSource } from '#/shared/lib/event-source' export function useImageBotCreateImage( - showError: (message: string) => void, - type: string, device: Device, - setMessages: Dispatch>, - mobileScrollContainer: RefObject + mobileScrollContainer: RefObject, + offset: MutableRefObject ) { - const [isComplete, setIsComplete] = useState(false) + const { botParams } = useImageBot() const [createLoading, setCreateLoading] = useState(false) - const { data } = useSession() + const { makeEvent } = useImageMessagesEvents() + + const { showMessage } = useShowDataStore() + + const { setMessages, messages } = useImageBotMessages() const dispatch = useAppDispatch() - const createImage = async (dataForSend: MessageSend) => { - const { content, file } = dataForSend + const { data: session } = useSession() + + const { event } = useEventSource() + + const createImage = makePrivateRequest(async (dto: MessageSend) => { + if (!botParams) return - setIsComplete(false) setCreateLoading(true) - const dataSending = file ? formDataHelper(file, dataForSend) : dataForSend + if (!dto.file) delete dto.file - const { data: messages } = await sendImage(type, dataSending, data?.access) + const { data, status } = await postImageMessage(botParams.slug, dto) setCreateLoading(false) - if (typeof messages === 'string') { - showError(messages) - return - } + if (status !== 200) + return showMessage((data as { detail: string }).detail ?? 'Ошибка при получении сообщений') + + dispatch(getUserBalance(session?.access)) - dispatch(getUserBalance(data?.access)) + const messages = useImageBotMessages.getState().messages - setMessages((prev: Message[]) => { - if (!prev || !prev.length) return messages + offset.current++ - if (device === 'desktop') { - return [...messages, ...prev] - } - return [...prev, ...messages] - }) + if (device === 'desktop') setMessages([(data as Message[])[1], ...messages]) + else setMessages([...messages, (data as Message[])[1]]) - setIsComplete(true) + makeEvent('images') - if (device === 'desktop') { + if (device === 'desktop') return window.scrollTo({ top: 0, behavior: 'smooth', }) - } setTimeout(() => { if (!mobileScrollContainer.current) return @@ -66,11 +75,10 @@ export function useImageBotCreateImage( behavior: 'smooth', }) }, 500) - } + }) return { createImage, - isComplete, createLoading, } } @@ -1 +1 @@ -export * from './use-images-uniq-input' \ No newline at end of file +export * from './use-images-model-input' \ No newline at end of file @@ -0,0 +1,45 @@ +import { MessageSend } from '#/entities/message' +import { Inference, useImageBotParams } from '#/entities/model-entity' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useMemo, useState } from 'react' + +export function useImageBotInput(sendMessage: (data: MessageSend) => Promise) { + const { showMessage } = useShowDataStore() + const [file, setFile] = useState(null) + const { infrerenceValues, inference, inferenceInputs: inputs } = useImageBotParams() + + const [valueInput, setValueInput] = useState('') + + const inputTypes = useMemo(() => inputs.map((p) => p.type), [inputs]) + + const requiredTypes = useMemo(() => inputs.filter((p) => p.required).map((p) => p.type), [inputs]) + + const onSendMessage = async (content: string, required: (string | null)[]) => { + if (required.includes('text') && content === '') return showMessage('Введите сообщение!') + + if (required.includes('zip') && file === null) 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, + valueInput, + setValueInput, + inputTypes, + requiredTypes, + } +} @@ -1,64 +0,0 @@ -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -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 { showMessage } = useShowDataStore() - - 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)) { - showMessage('Введите сообщение!') - return false - } - if (required.includes('image') && image === null) { - showMessage('Прикрепите изображение!') - return false - } - if (required.includes('zip') && image === null) { - showMessage('Прикрепите архив!') - 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, - } -} @@ -1 +1,2 @@ +export * from './use-images-bot-pagination-old' export * from './use-images-bot-pagination' \ No newline at end of file @@ -0,0 +1,150 @@ +import { useAppSelector } from '#/app/store/store' +import { getImagesBySlug, Message, useImageBotMessages } 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 { 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 { useImageObjectId } from '#/features/image-object-id' + +export function useImageBotPaginationOld(deviceType: Device) { + const refScrollMobile = useRef(null) + const refScrollDesktop = useRef(null) + const mobileScrollContainer = useRef(null) + const offset = useRef(0) + const observer = useRef(null) + + const [isHidden, setIsHidden] = useState(false) + + const { setImageObjectId } = useImageObjectId() + + const { query } = useRouter() + + const { messages, setLoading, setMessages, loading } = useImageBotMessages() + + const { showMessage } = useShowDataStore() + + const { data } = useSession() + + const limits: Record = { + small: { + active: useMediaQuery('(max-height: 600px)'), + limit: 20, + firstLimit: 30, + }, + 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 = async (count?: number) => { + if (!data) return + + setLoading(true) + + const { data: answer, ...response } = await getImagesBySlug( + query.slug as string, + data.access, + offset.current, + count || 10 + ) + + if (response.status >= 400 || !Array.isArray(answer.messages)) + return showMessage('Ошибка загрузки чата') + + const messages = useImageBotMessages.getState().messages + + if (deviceType === 'desktop') setMessages([...messages, ...answer.messages]) + else setMessages([...answer.messages.reverse(), ...messages]) + + offset.current += answer.messages.length + + setImageObjectId(answer.id) + } + + const callback = async function (entries: IntersectionObserverEntry[]) { + if (!entries[0].isIntersecting) return + + if (isHidden) return setIsHidden(false) + + if (deviceType === 'desktop') { + const active = Object.values(limits).find((item) => item.active) + + if (!active) return + + if (offset.current > 0) { + fetchMessages(active.limit) + } else { + fetchMessages(active?.firstLimit) + } + } + + const { current } = mobileScrollContainer + + if (!current) return + + const scrollBottom = current.scrollHeight - current.scrollTop + + await fetchMessages() + + setTimeout(() => { + const { current } = mobileScrollContainer + + if (!current) return + + if (offset.current === 0) current.scrollTop = current.scrollHeight - scrollBottom + else { + current.scroll({ + top: current.scrollHeight - scrollBottom, + behavior: 'smooth', + }) + } + + setLoading(false) + }, 500) + } + + function onObserverMounted() { + const currentObserver = + deviceType === 'desktop' ? refScrollDesktop.current : refScrollMobile.current + + if (!currentObserver) return + + observer.current = new IntersectionObserver(callback, { rootMargin: '400px' }) + + observer.current.observe(currentObserver!) + } + + function isHiddenHandler() { + if (document.visibilityState === 'hidden') return setIsHidden(true) + setIsHidden(false) + } + + useEffect(() => { + document.addEventListener('visibilitychange', isHiddenHandler) + return () => document.removeEventListener('visibilitychange', isHiddenHandler) + }, []) + + return { + refScrollMobile, + refScrollDesktop, + onObserverMounted, + messages, + loading, + setLoading, + offset, + setMessages, + fetchMessages, + mobileScrollContainer, + } +} @@ -4,11 +4,12 @@ import { useSession } from 'next-auth/react' import { useMemo, useRef } from 'react' import { useAppSelector } from '#/app/store/store' -import { getImagesBySlug, Message } from '#/entities/message' +import { getImagesBySlug, Message, useImageBotMessages } from '#/entities/message' import { Device } from '#/shared/lib/types/entities' import { LimitSize, Limit } from '../types' import { useRouter } from 'next/router' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useImageObjectId } from '#/features/image-object-id' export function useImageBotPagination(container: React.RefObject) { const refScroll = useRef(null) @@ -44,6 +45,7 @@ export function useImageBotPagination(container: React.RefObject if (!data) return setLoading(true) + const { data: answer, ...response } = await getImagesGalery( data.access, offset.current, @@ -51,11 +53,12 @@ export function useImageBotPagination(container: React.RefObject ) setLoading(false) - if (response.status >= 400 || !Array.isArray(answer)) return showMessage('Ошибка загрузки чата') + if (response.status >= 400 || !Array.isArray(answer.messages)) + return showMessage('Ошибка загрузки чата') - addMessages(answer) + addMessages(answer.messages) - offset.current = offset.current + answer.length + offset.current = offset.current + answer.messages.length } const callback = async function (entries: IntersectionObserverEntry[]) { @@ -0,0 +1,6 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; + padding-right: 8px; +} @@ -0,0 +1,85 @@ +import React from 'react' +import styles from './image-bot-params-map.module.scss' + +import { SwitchFilter } from '#/features/switch-filter/' +import { InputFilter } from '#/features/input-filter/ui/input-filter' +import { useChatBotParams, useImageBotParams } 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 ImageBotParamsMap {} + +export function ImageBotParamsMap({}: ImageBotParamsMap) { + const { infrerenceValues, setInferenceValueByKey, inferenceParams } = useImageBotParams() + + 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)} + withoutTooltip={!description || description.length == 0} + onlyNumber={type == 'int'} + /> + ) + } + })} +
+ ) +} @@ -0,0 +1 @@ +export * from './image-bot-params-map' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -9,10 +9,9 @@ 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 { ModalImage } from './modal-image' -import { Message } from '#/entities/message' +import { Message, useImageIcons } from '#/entities/message' import { GALLERY_IMAGES, getModalById, PlateTemplate } from '#/features/modals' import { CSSTransition } from 'react-transition-group' @@ -125,6 +124,7 @@ export default function FullScreenModal({ images, onSlideFalse, reverse = false, { + const { botParams, inference, onSetInferenceParams } = useImageBot() + + 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} + isDescriptionRequired={true} + /> + ) +} @@ -0,0 +1 @@ +export * from './image-model-params-select' @@ -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,71 @@ +import { useAppSelector } from '#/app/store/store' +import { Message, MessageSend, postImageMessage } from '#/entities/message' +import { Model, useImageBotParams } from '#/entities/model-entity' +import { useMessagesStore } from '#/widgets/messages' +import { useSession } from 'next-auth/react' +import { useState } from 'react' + +export function useImageInput(model: Model | null, container: React.RefObject) { + const [text, setText] = useState('') + const [image, setImage] = useState(null) + const [loading, setLoading] = useState(false) + + const { inference } = useImageBotParams() + + const { setMessages, messages, addOptimistic } = useMessagesStore() + + const includeParams = useAppSelector((state) => state.params.params) + + const { data: session } = useSession() + + function createMessage() { + if (!inference) return + const data = { + file: image, + content: text, + info: { + ...includeParams, + inference: inference.id, + }, + } + + 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 postImageMessage(model.slug, param) + + setLoading(false) + + if (typeof data === 'string') return + + setMessages([...(data as Message[]), ...messages]) + } + + return { + onSend, + text, + setText, + image, + setImage, + 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,112 @@ +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 { useImageInput, useImageInputStyles } from '../model' +import { CommonButton } from '#/shared/ui/button' +import { ImageSettingsPopup, useImageBotMessages } from '#/entities/message' +import { getPopupById, POPUP_IMAGE_SETTINGS } from '#/shared/ui/popup' +import { Model } from '#/entities/model-entity' +import { useImageBotParams } from '#/entities/model-entity' + +export interface ImageInputProps { + model: Model | null + container: React.RefObject +} + +export const ImageInput = ({ model, container }: ImageInputProps) => { + const { createMessage, text, setText, loading } = useImageInput(model, container) + + const { inferenceParams, inference } = useImageBotParams() + + const { onStylesButtonClick } = useImageInputStyles() + + const imageSettingsPopup = getPopupById(POPUP_IMAGE_SETTINGS) + + return ( +
+
+
+ + +
+ +
+ + +
+
+
+
+ + +
+ +
+
+ +
+ + Сгенерировать + + { + if (!model) return + + imageSettingsPopup.toogleState({ + botParams: inferenceParams, + currentVersion: inference?.id, + }) + }} + 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 './use-image-object-id' \ No newline at end of file @@ -0,0 +1,17 @@ +import { create } from 'zustand' + +export interface ImageObjectIdStore { + imageObjectId: string | null + setImageObjectId: (imageObjectId: string | null) => void +} + +export const useImageObjectId = create((set, get) => { + function setImageObjectId(imageObjectId: string | null) { + set({ imageObjectId }) + } + + return { + imageObjectId: null, + setImageObjectId, + } +}) @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './reset-image-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,32 @@ +import React from 'react' +import styles from './reset-image-bot-filters.module.scss' +import { c } from '#/shared' +import { POPUP_CHAT_BOT_PARAMS, POPUP_IMAGE_BOT_PARAMS, getPopupById } from '#/shared/ui/popup' +import { useChatBot } from '#/entities/model-entity' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' + +interface ResetImageBotFilters {} + +export const ResetImageBotFilters = ({}: ResetImageBotFilters) => { + const popup = getPopupById(POPUP_IMAGE_BOT_PARAMS) + + const { onSetInferenceParams, inference } = useImageBot() + + return ( +
+

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

+ +

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

+
+ ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -103,7 +103,7 @@ export function useChatMessageActions() { setMessages([...initialMessages, ...(result as Message[])]) - makeEvent('chat', () => { + makeEvent('chat-bot', () => { ;(result as Message[])[1].content = '' }) @@ -0,0 +1,6 @@ +import { ImageModelPage } from '#/views/old-images' +import { getDefaultLayout } from '#/widgets/layouts' + +ImageModelPage.getLayout = getDefaultLayout() + +export default ImageModelPage @@ -0,0 +1,6 @@ +import { ImageModelsPage } from '#/views/old-images' +import { getDefaultLayout } from '#/widgets/layouts' + +ImageModelsPage.getLayout = getDefaultLayout({ title: 'Изображения' }) + +export default ImageModelsPage @@ -0,0 +1,6 @@ +import { ImageModelPage } from '#/views/images' +import { getDefaultLayout } from '#/widgets/layouts' + +ImageModelPage.getLayout = getDefaultLayout() + +export default ImageModelPage @@ -2,9 +2,10 @@ const API_URL = process.env.NEXT_PUBLIC_API_HOST import { EventSourcePolyfill } from 'event-source-polyfill' import { useSession } from 'next-auth/react' +import { useEventStore } from './events-store' export function useEventSource() { - let events: { name: string; obj: EventSource }[] = [] + const { events, setEvents } = useEventStore() const { data } = useSession() @@ -13,19 +14,39 @@ export function useEventSource() { 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}` }, - }), - }) + const { events } = useEventStore.getState() + + setEvents([ + ...events, + { + name, + obj: new EventSourcePolyfill(`${url}/${src}`, { + headers: { Authorization: `Bearer ${data?.access}` }, + }), + }, + ]) if (callback) { addEventCallback(name, callback) } } + function closeEvent(name: string): boolean { + const { events } = useEventStore.getState() + + const index = events.findIndex((e) => e.name === name) + + if (index === -1) return false + + events[index].obj.close() + + setEvents([...events.filter((x) => x.name !== name)]) + + return true + } + function addEventCallback(name: string, callback: (value: T) => void) { + const { events } = useEventStore.getState() const event = events.find((e) => e.name === name) if (!event) { @@ -34,7 +55,8 @@ export function useEventSource() { event.obj.addEventListener('error', (error) => { ;(error.target as any).close() - events = events.filter((x) => x.name !== event.name) + const { events } = useEventStore.getState() + setEvents(events.filter((x) => x.name !== event.name)) }) event.obj.addEventListener('message', (event) => { @@ -43,6 +65,8 @@ export function useEventSource() { } function addOpenCallback(name: string, callback: (value: Event) => void) { + const { events } = useEventStore.getState() + const event = events.find((e) => e.name === name) if (!event) { @@ -58,5 +82,7 @@ export function useEventSource() { event, addEventCallback, addOpenCallback, + closeEvent, + events, } } @@ -0,0 +1,18 @@ +import { create } from 'zustand' +import { Event } from '../types' + +export interface EventsStore { + events: Event[] + setEvents: (events: Event[]) => void +} + +export const useEventStore = create((set, get) => { + function setEvents(events: Event[]) { + set({ ...get, events }) + } + + return { + events: [], + setEvents, + } +}) @@ -1 +1,2 @@ -export * from './event-source' \ No newline at end of file +export * from './event-source' +export * from './events-store' \ No newline at end of file @@ -0,0 +1,4 @@ +export interface Event { + name: string + obj: EventSource +} @@ -0,0 +1 @@ +export * from './event' \ No newline at end of file @@ -1 +1,2 @@ -export * from './model' \ No newline at end of file +export * from './model' +export * from './types' \ No newline at end of file @@ -1,11 +1,63 @@ -import axios from 'axios' - export async function getBlobFromUrl(url: string) { const extension = url.match(/\.(\w+)\?/) - if(!extension) return + if (!extension) return const buffer = await fetch(url).then((res) => res.arrayBuffer()) return new File([buffer], `file.${extension[1]}`) } + +export function fileIsLink(file: string | null) { + if (!file) return false + + return file.includes('http') +} + +export function fileIsBase64(file: string | null) { + if (!file) return false + + return file.includes('base64') +} + +export function addBase64Padding(str: string) { + // debugger + const paddingNeeded = str.length % 3 + + if (paddingNeeded > 0) { + return str + '='.repeat(3 - paddingNeeded) + } + + return str +} + +export function validateBase64(base64String: string) { + // Удаляем пробелы, проверяем на наличие недопустимых символов + const cleanedString = base64String.replace(/\s/g, '').replace(/=+$/, '') + const validBase64Regex = /^[A-Za-z0-9+/]*[=]{0,2}$/ + + return validBase64Regex.test(cleanedString) +} + +export function base64Decode(str: string) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + let output = '' + str = str.replace(/[^A-Za-z0-9+/=]/g, '') // Удаляем недопустимые символы + + for (let i = 0; i < str.length; i += 4) { + const encoded1 = chars.indexOf(str.charAt(i)) + const encoded2 = chars.indexOf(str.charAt(i + 1)) + const encoded3 = chars.indexOf(str.charAt(i + 2)) + const encoded4 = chars.indexOf(str.charAt(i + 3)) + + const byte1 = (encoded1 << 2) | (encoded2 >> 4) + const byte2 = ((encoded2 & 15) << 4) | (encoded3 >> 2) + const byte3 = ((encoded3 & 3) << 6) | encoded4 + + output += String.fromCharCode(byte1) + if (encoded3 !== 64) output += String.fromCharCode(byte2) + if (encoded4 !== 64) output += String.fromCharCode(byte3) + } + + return output +} @@ -0,0 +1,32 @@ +import { FieldValues, UseFormSetValue, UseFormTrigger } from "react-hook-form" + +export function makeShouldDirtySetValue( + setValue: UseFormSetValue +) { + return (...params: Parameters) => { + params[2] = { shouldDirty: true } + return setValue(...params) + } +} + +export function makeTriggeredSetValue( + setValue: UseFormSetValue, + trigger: UseFormTrigger +) { + return (...params: Parameters) => { + setValue(...params) + trigger(params[0]) + return + } +} + +export function objectToFormdata(object: Record) { + const formData = new FormData() + + Object.entries(object).forEach(([k, v]) => { + if (typeof v !== "object" || v instanceof File) return formData.append(k, v) + return formData.append(k, JSON.stringify(v)) + }) + + return formData +} @@ -22,3 +22,9 @@ export function stringIsImage(str: string | null | undefined) { if (!str) return false return /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg|\.wav/.test(str) } + +export function cutString(str: string | null | undefined, length: number, postfix = '...') { + if (!str) return null + + return str.length > length ? str.slice(0, length) + postfix : str +} @@ -3,3 +3,4 @@ export const POPUP_IMAGE_SETTINGS = 'popup-image-settings' export const POPUP_IMAGE_MODEL = 'popup-image-model' export const POPUP_USER_MESSAGE_ACTIONS = 'popup-user-message-actions' export const POPUP_CHAT_BOT_PARAMS = 'popup-chat-bot-params' +export const POPUP_IMAGE_BOT_PARAMS = 'popup-image-bot-params' @@ -11,13 +11,14 @@ export const PopupBackdrop = () => { useEffect(() => { if (someIsOpened) document.body.style.overflow = 'hidden' + else document.body.style.overflow = 'auto' }, [someIsOpened]) return (
e.stopPropagation()} - onMouseEnter={(e) => e.stopPropagation()} - onMouseLeave={(e) => e.stopPropagation()} + onMouseEnter={(e) => e.stopPropagation()} + onMouseLeave={(e) => e.stopPropagation()} onClick={() => hideAllPopups()} className={c(styles.backdrop, someIsOpened && styles.backdrop_open)} >
@@ -72,6 +72,7 @@ const Template = memo( style={{ left: cordinates ? cordinates.left : 0, top: cordinates ? cordinates.top : 0, + transform: horizontal === 'center' && !isMobile ? 'translateX(-50%)' : 'auto', }} className={c( styles.popup, @@ -12,9 +12,10 @@ } .slider { - margin: 13px 0px; + margin: 0 auto; + margin: 13px 13px auto auto; position: relative; - width: 100%; + width: 90%; height: 4px; background-color: #eff0f294; border-radius: 3px; @@ -26,7 +27,7 @@ position: absolute; width: 100%; top: 50%; - transform: translateY(-50%); + transform: translateY(-50%); height: 15px; } @@ -38,7 +38,7 @@ export const CommonTooltip = ({ > {children} - + {(state) => (
= () => { const { currentChat } = useCurrentChat() + const { closeEvent, events } = useEventSource() + + useEffect(() => { + console.log('events::', events) + }, [events]) + useEffect(() => { if (!currentChat || !loaded) return + console.log('change') + closeEvent('chat-bot') makeEvent('chat-bot') }, [currentChat, loaded]) @@ -0,0 +1,73 @@ +.model { + padding: 10px 12px; + border-radius: 15px; + display: flex; + align-items: center; + gap: 10px; + background-color: var(--new-ui-main-color); + width: max-content; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + z-index: 3; + + @media screen and (max-width: 1200px) { + top: 30px; + } +} + +.popup { + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); + z-index: 2; +} + +.messages { + z-index: 2; + height: calc(100dvh - 20px - 20px - 40px - 25px); + overflow-y: scroll; +} + +.input { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 2; + + @media screen and (max-width: 800px) { + bottom: -10px; + } +} + +.shape { + height: 100px; + position: absolute; + filter: blur(10px); + width: 100%; + left: 0; + right: 0; + background: linear-gradient( + 180deg, + var(--new-ui-bg-app-color) 0%, + var(--new-ui-bg-app-color) 30%, + rgba(255, 255, 255, 0) 100% + ); + z-index: 1; + &_top { + top: 0px; + } + + &_bottom { + bottom: -20px; + transform: rotate(180deg); + } +} + +.container { + padding-top: 20px; + position: relative; +} @@ -0,0 +1,78 @@ +import * as React from 'react' +import { useRouter } from 'next/router' +import { useSession } from 'next-auth/react' +import { Error } from '#/shared' +import { ImageMessagesList } from '#/widgets/messages/ui/image-messages-list' +import { c, getDeviceType } from '#/shared/lib/helpers' +import Head from 'next/head' + +import styles from './image-model.module.scss' +import { NextPageWithLayout } from '#/pages/_app' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' +import { ImageModelsSelect } from '#/widgets/image-models-popup' + +import { ImageInput } from '#/features/image-models-input' +import { useEffect, useRef } from 'react' +import { useMessagesStore } from '#/widgets/messages' +import { ImagesEmpty } from '#/widgets/image-messages' +import { MessageFastChoiceMenu } from '#/features/message-fast-choice-menu' +import { useShowDataStore } from '#/shared/lib/hooks' + +const ImageModelPage: NextPageWithLayout = () => { + const { query } = useRouter() + + const { botParams, inference, fetchBotParams } = useImageBot() + + const container = useRef(null) + + const deviceType = getDeviceType() + + const { data: session } = useSession() + + const { messages, loading } = useMessagesStore() + + async function onFetch() { + await Promise.all([fetchBotParams()]) + } + + useEffect(() => { + onFetch() + }, [session, query]) + + return ( +
+ + Модель {botParams ? botParams.title : 'Загрузка...'} + +
+

Модель

+ +
+
+ +
+ {!loading && messages.length === 0 ? ( + + ) : ( + + )} +
+ +
+ +
+ +
+ +
+ +
+
+ ) +} + +export default ImageModelPage @@ -0,0 +1 @@ +export { default as ImageModelPage } from './image-model' @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-image-bots' \ No newline at end of file @@ -0,0 +1,27 @@ +import { ShortModel, getImageBots } 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 useImageBots() { + const [bots, setBots] = useState([]) + + const { showMessage } = useShowDataStore() + + const [fetchImageBots, pending] = withPending( + makePrivateRequest(async () => { + const { data, status } = await getImageBots() + + if (status !== 200) return showMessage('Ошибка при получении чат ботов') + + setBots(data) + }) + ) + + return { + pending, + fetchImageBots, + bots, + } +} \ No newline at end of file @@ -0,0 +1,151 @@ +.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; + } +} @@ -0,0 +1,250 @@ +import { useEffect } from 'react' +import { Box, Stack } from '@mui/material' +import Head from 'next/head' +import { useRouter } from 'next/router' + +import styles from './image-model.module.scss' + +import BlockedSvg from '#/assets/svg/blocked.svg?react' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' +import { useImageBotCreateImage } from '#/features/image-bot-create-image' +import { useImagesBotFilters } from '#/features/image-bot-filters' +import { useImageBotPagination, useImageBotPaginationOld } from '#/features/image-bot-pagination' +import { NextPageWithLayout } from '#/pages/_app' +import { Loader } from '#/shared' +import { c, getDeviceType, getOs } from '#/shared/lib/helpers' +import { useThemeAndDevice } from '#/shared/lib/hooks' +import { SvgIcon } from '#/shared/ui/svg' +import { ImageMessagesList, ImageMessagesListOld } from '#/widgets/messages' +import { ModelInput } from '#/features/model-input' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { CommonTitle } from '#/shared/ui/title' +import { ImageModelOptions } from '#/widgets/image-model-options' +import { useImageBotInput } from '#/features/image-bot-input' +import { POPUP_IMAGE_BOT_PARAMS, getPopupById } from '#/shared/ui/popup' + +const ImageModelPage: NextPageWithLayout = () => { + const { botParams, fetchBotParams } = useImageBot() + + const deviceType = getDeviceType() + + const deviceOs = getOs() + + const { desktop } = useThemeAndDevice(deviceType, deviceOs) + + const { + refScrollMobile, + refScrollDesktop, + mobileScrollContainer, + onObserverMounted, + fetchMessages, + loading, + offset, + messages, + } = useImageBotPaginationOld(deviceType) + + const { createImage } = useImageBotCreateImage(deviceType, mobileScrollContainer, offset) + + const { onSendMessage, file, setFile, valueInput, setValueInput, inputTypes, requiredTypes } = + useImageBotInput(createImage) + + async function onFetch() { + await Promise.all([fetchBotParams()]) + } + + const popup = getPopupById(POPUP_IMAGE_BOT_PARAMS) + + useEffect(() => { + onFetch() + onObserverMounted() + }, []) + + return ( + <> + + {botParams ? botParams?.title : 'Загрузка...'} + +
+ + {botParams && + botParams.tags.map((tag, index) => ( +
+ + + {tag.title} +
+ ))} +
+ } + /> +
+ + + {desktop ? ( + <> + + popup.toogleState()} + inputTypes={inputTypes} + blocked={botParams ? !botParams.enabled : false} + sendMessage={(message) => onSendMessage(message, requiredTypes)} + /> + + {botParams && !botParams.enabled && ( +
+ + + Модель недоступна + +
+ )} +
+ + + + + + ) : ( + +
+
+ +
+
+
+ +
+ +
+ + + {botParams && ( + popup.toogleState()} + inputTypes={inputTypes} + blocked={botParams ? !botParams.enabled : false} + sendMessage={(message) => + onSendMessage(message, requiredTypes) + } + /> + )} + {botParams && !botParams.enabled && ( +
+ + + Модель недоступна + +
+ )} +
+
+
+ )} +
+ {desktop && } +
+ + ) +} + +export default ImageModelPage @@ -0,0 +1,25 @@ +.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; + } +} @@ -0,0 +1,60 @@ +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, ShortModel } from '#/entities/model-entity' + +import styles from './image-models.module.scss' +import { useImageBots } from '../model' +import { useUserSelector } from '#/entities/user-account' + +export const ImageModelsPage: NextPageWithLayout = () => { + const { payment_plan } = useUserSelector() + + const { bots, fetchImageBots } = useImageBots() + + useEffect(() => { + fetchImageBots() + }, []) + + return ( + <> + + Изображения + +
+ {bots ? ( + bots.map((item) => { + return ( + + ) + }) + ) : ( + + )} +
+ + ) +} + +export default ImageModelsPage @@ -0,0 +1,2 @@ +export { default as ImageModelsPage } from './image-models' +export { default as ImageModelPage } from './image-model' @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,62 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; + padding: 30px; + background-color: var(--new-ui-main-color); + border-radius: 15px; + width: 30%; + margin-left: 20px; + + @media screen and (max-width: 1000px) { + width: 100%; + margin: unset; + } + + &__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 { + overflow-y: auto; + 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,60 @@ +import { c } from '#/shared' +import styles from './image-model-options.module.scss' +import ArrowDownSvg from '#/assets/svg/arrow-down.svg?react' +import { HtmlHTMLAttributes, useState } from 'react' +import { ResetBotFilters } from '#/features/reset-bot-filters' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' +import { ImageModelParamsSelect } from '#/features/image-model-params-select' +import { ImageBotParamsMap } from '#/features/image-bot-params' +import { ResetImageBotFilters } from '#/features/reset-image-bot-filters' + +interface ImageModelOptionsProps extends HtmlHTMLAttributes { + isMobile?: boolean +} + +export const ImageModelOptions = ({ className, isMobile = false }: ImageModelOptionsProps) => { + const [opened, setOpened] = useState(isMobile ? true : false) + + const { inference, botParams } = useImageBot() + + return ( +
+ {botParams && botParams.inferences && ( + <> +

версии

+ + + )} + {inference && inference.parameters.length > 0 ? ( + <> + + +
+ + +
+ + ) : ( +

Параметры отсутствуют

+ )} +
+ ) +} @@ -0,0 +1,2 @@ + +export * from './image-model-options' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-image-models-popup' \ No newline at end of file @@ -0,0 +1,13 @@ +import { useImagesBots } from '#/entities/model-entity' +import { useMemo, useState } from 'react' + +export function useImageModelsPopup() { + const { bots, fetchBots, setBots } = useImagesBots() + + + return { + bots, + setBots, + fetchBots, + } +} @@ -0,0 +1 @@ +export type CostZones = 30 | 60 | 90 @@ -0,0 +1 @@ +export * from './cost' \ No newline at end of file @@ -0,0 +1,74 @@ +.container { + position: relative; + + @media (max-width: 768px) { + position: static; + } +} +.wrapper { + background: rgba($color: #000000, $alpha: 0.4); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 104; + visibility: hidden; + opacity: 0; + transition: all 0.3s ease-in-out; + + &_open { + visibility: visible; + opacity: 1; + } +} +.select { + padding: 5px 12px; + background: rgba(#8280ff, 0.1); + border-radius: 10px; + display: flex; + align-items: center; + width: max-content; + gap: 10px; + cursor: pointer; + + &__arrow { + padding-top: 5px; + @media (max-width: 768px) { + width: 15px; + height: 15px; + } + } + + &__title { + color: var(--air-color); + } +} + +.popup { + background: var(--new-ui-main-color); + border-radius: 15px; + width: max-content; + position: absolute; + z-index: 105; + transform: translateY(20px); + visibility: hidden; + opacity: 0; + transition: all 0.3s ease-in-out; + border: 1px solid #40404e4b; + max-height: 70vh; + overflow-y: scroll; + top: calc(100% + 10px); + + @media (max-width: 768px) { + left: 0; + right: 0; + width: fit-content; + } + + &_open { + visibility: visible; + opacity: 1; + transform: translateY(0); + } +} @@ -0,0 +1,41 @@ +import { Typography } from '@mui/material' +import React, { useEffect } from 'react' +import styles from './image-models-select.module.scss' +import { c } from '#/shared/lib/helpers' +import { useImageModelsPopup } from '../model' +import { ImagePopupModel } from './image-popup-model' +import { useRouter } from 'next/router' +import ArrowDownSvg from '#/assets/svg/arrow-down.svg?react' +import { getPopupById, POPUP_IMAGE_MODEL } from '#/shared/ui/popup' +import { ImageModelsPopup } from './image-models.popup' +import { Model } from '#/entities/model-entity' + +export interface ImageModelsPopupProps { + model: Model | null + className?: string +} + +export const ImageModelsSelect = ({ model, className }: ImageModelsPopupProps) => { + const popup = getPopupById(POPUP_IMAGE_MODEL) + + return ( + <> +
+
popup.toogleState()} className={c(styles.select)}> +

+ {model ? model.title : 'Загрузка...'} +

+ +
+ + +
+ + ) +} @@ -0,0 +1,25 @@ + +.popup { + display: flex; + flex-direction: column; + width: max-content; + + max-height: 60dvh; + overflow-y: scroll; + overflow-x: hidden; + border-radius: 15px; + + @media screen and (max-width: 600px) { + width: 100vw; + max-height: 70dvh; + } + + > div:first-child { + border-radius: 15px 15px 0px 0px !important; + } +} + +.template{ + padding: 0px; + border-radius: 15px; +} \ No newline at end of file @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react' +import { useImageModelsPopup } from '../model' +import styles from './image-models.popup.module.scss' +import { c } from '#/shared' +import { ImagePopupModel } from './image-popup-model' +import { getPopupById, POPUP_IMAGE_MODEL, PopupTemplate } from '#/shared/ui/popup' +import { useRouter } from 'next/router' +import { Model } from '#/entities/model-entity' + +interface ImageModelsPopupProps { + model: Model | null +} + +export const ImageModelsPopup = ({ model, ...props }: ImageModelsPopupProps) => { + const { fetchBots, bots } = useImageModelsPopup() + + const popup = getPopupById(POPUP_IMAGE_MODEL) + + const { push } = useRouter() + + async function onFetch() { + await fetchBots() + } + + useEffect(() => { + onFetch() + }, []) + + return ( + +
+ {bots.map((item, index) => ( + { + popup.setState(false) + push(`/images/${slug}`) + }} + /> + ))} +
+
+ ) +} @@ -0,0 +1,99 @@ +.model { + // font-family: Inter; + padding: 15px 20px; + cursor: pointer; + display: flex; + align-items: center; + gap: 22px; + justify-content: space-between; + max-width: 500px; + transition: background-color 0.3s ease; + position: relative; + + &:hover { + background-color: rgba($color: #a4aab5, $alpha: 0.1); + } + + @media (max-width: 768px) { + // max-width: calc(100% - 100px) !important; + width: 100%; + // max-width: unset; + } + + &_blocked { + &:hover { + background-color: transparent; + } + } + + &__blocked { + background-color: rgba($color: #000000, $alpha: 0.7); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-right: 10px; + span { + color: white; + font-weight: 600; + padding-left: 8px; + } + } + + &__locked { + background-color: rgba($color: #000000, $alpha: 0.7); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-right: 10px; + span { + color: white; + font-weight: 600; + padding-left: 8px; + } + } + &__title { + font-weight: 600; + font-size: 18px; + margin-bottom: 4px; + } + + &__description { + font-size: 14px; + color: #a4aab5; + margin-bottom: 7px; + } + + &__badges { + display: flex; + gap: 10px; + } + + &__badge { + padding: 4px 6px; + font-size: 13px; + display: flex; + align-items: center; + gap: 2px; + background-color: rgba($color: #a4aab5, $alpha: 0.1); + color: white; + font-weight: 500; + border-radius: 5px; + } + + &__time { + gap: 6px; + color: var(--new-ui-gray-color); + } + &__cost { + } +} @@ -0,0 +1,79 @@ +import React, { useMemo } from 'react' + +import styles from './image-popup-model.module.scss' +import { c, formatDate } from '#/shared/lib/helpers' +import SuccessRoundedSvg from '#/assets/svg/success-rounded.svg?react' +import ClockSvg from '#/assets/svg/clock.svg?react' +import LightningSvg from '#/assets/svg/lightning.svg?react' +import LockSvg from '#/assets/svg/lock.svg?react' +import BlockedSvg from '#/assets/svg/blocked.svg?react' +import { useAppSelector } from '#/app/store/store' +import { ShortModel } from '#/entities/model-entity' +import { useUserSelector } from '#/entities/user-account' + +interface ImagePopupModelProps extends ShortModel { + active: boolean + setActive: (slug: string) => void + averageTokenCost: number +} + +export interface CostZone { + remainder: number + color: string +} + +export const costZoneColors: CostZone[] = [ + { remainder: -20, color: '#1EB034' }, + { remainder: 20, color: '#B0891E' }, + { remainder: 1000, color: '#B01E1E' }, +] + +export const ImagePopupModel = ({ + title, + description, + slug, + enabled, + active, + averageTokenCost, + setActive, +}: ImagePopupModelProps) => { + const user = useUserSelector() + + const accessed_models = useMemo( + () => (user.payment_plan ? user.payment_plan.plan.accessed_models : []), + [user] + ) + + const isAccess = useMemo(() => accessed_models && accessed_models.includes(slug), [accessed_models]) + + return ( +
{ + if (!enabled || !isAccess) return + setActive(slug) + }} + className={c(styles.model, !enabled && styles.model_blocked)} + > + {!enabled ? ( +
+ + Модель недоступна +
+ ) : ( + !isAccess && ( +
+ + Недоступно в текущем тарифе +
+ ) + )} + +
+

{title}

+

{description}

+
+ + {active && } +
+ ) +} @@ -0,0 +1,3 @@ +export * from './image-models-select' +export * from './image-popup-model' +export * from './image-models.popup' \ No newline at end of file @@ -0,0 +1,4 @@ +export * from './types' +export * from './ui' +export * from './model' +// export * from './api' \ No newline at end of file @@ -24,6 +24,7 @@ import styles from './default.module.scss' import { PlateImageStyles } from '#/widgets/images-style' import { SlowLoading } from '#/features/slow-loading' import { ChatBotOptionsPopup } from '#/entities/chat/ui/chat-bot-options-popup' +import { ImageBotOptionsPopup } from '#/entities/model-entity' export const Layout: React.FC = ({ children, @@ -156,6 +157,7 @@ export const Layout: React.FC = ({ + ) @@ -1,10 +1,10 @@ -import { Message, MessageSend } from '#/entities/message' +import { MediaMessageListResponse, Message, MessageSend } from '#/entities/message' import { API_URL } from '#/shared/lib/constants' import { IMessageRequest } from '#/shared/lib/types/types-gpt' import axios, { AxiosError, AxiosResponse } from 'axios' export async function getImagesGalery(token?: string, offset?: number, limit = 10) { - return await axios.get(API_URL + `/media/gallery/images?limit=${limit}&offset=${offset}`, { + return await axios.get(API_URL + `/media/gallery/images?limit=${limit}&offset=${offset}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -1,3 +1,3 @@ -export * from './use-image-icons' export * from './use-images-pagination' export * from './messages.store' +export * from './use-images-pagination' @@ -1,5 +1,5 @@ import { getUserBalance } from '#/entities/balance' -import { Message, MessageSend, sendImage } from '#/entities/message' +import { Message } from '#/entities/message' import { useAppDispatch } from '#/app/store/store' import { useSession } from 'next-auth/react' import { useState, useRef, useEffect, useCallback } from 'react' @@ -6,15 +6,10 @@ export const useMessages = (messages: Message[]) => { const [loaded, setLoaded] = useState(false) - // типизация ну супер кривая))) const computedLibraryImages = useMemo[]>(() => { return messages - .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 - }) - .filter((el) => el !== null) as Message[] + .map((e) => (!e.file || e.file.includes('.svg') ? null : e)) + .filter((e) => e !== null) as Message[] }, [messages]) return { @@ -1,105 +0,0 @@ -import { useAppSelector } from '#/app/store/store' -import { TooltipCustom } from '#/shared' -import { Grow, Box } from '@mui/material' -import { useImageIcons } from '../model' - -export interface ImageIconsProps { - uid: string - url: string | null - content?: string -} - -export const ImageIcons = ({ uid, url, content }: ImageIconsProps) => { - const theme = useAppSelector((state) => state.theme.theme) - - const { toggleMenu, downloadFile, iconsMenu } = useImageIcons() - - return ( - <> - { - e.stopPropagation() - toggleMenu(uid) - }} - width='35' - height='35' - viewBox='0 0 35 35' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - - - - { - window.open(`${url ? url : ''}`, '_blank') - }} - width='15' - height='15' - viewBox='0 0 15 15' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - { - downloadFile(url, content) - }} - width='15' - height='15' - viewBox='0 0 15 15' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - - - ) -} @@ -0,0 +1,70 @@ +.model { + position: absolute; + top: 12px; + left: 12px; + z-index: 100; + background-color: rgba($color: #151518, $alpha: 0.5); + font-size: 13px; + padding: 2px 8px; + border-radius: 10px; + font-weight: 500; + color: #fff; +} +.wrap { + display: flex; + overflow: hidden; + width: 100%; + flex-wrap: wrap; + gap: 20px; + @media (max-width: 766px) { + height: 70vh; + width: 100%; + overflow-y: scroll; + text-align: center; + justify-content: center; + } + div { + margin-right: 10px; + @media (max-width: 766px) { + width: 100%; + } + + .image { + margin: 10px auto; + width: 250px; + height: 250px; + border-radius: 15px !important; + } + + @media (max-width: 1700px) { + } + + @media (max-width: 766px) { + text-align: center; + width: 80%; + } + } +} + +.list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.container { + padding-top: 70px; + padding-bottom: 150px; + position: relative; +} + +.scroll { + position: absolute; + top: 0; + left: 0; +} +.loader { + margin: 0 auto; + width: fit-content; + margin-bottom: 20px; +} @@ -0,0 +1,60 @@ +import React, { createRef, memo, useEffect, useMemo, useState, version } from 'react' + +import { useAppSelector } from '#/app/store/store' +import { ClientOnly, TooltipCustom, c, getDeviceType } from '#/shared' + +import styles from './image-messages-list.module.scss' +import { createPortal } from 'react-dom' +import { useMessages } from '../model/use-messages' +import { ImageIcons } from '../../../entities/message/ui/image-icons' +import { ImageModal } from '#/features/image-modal' +import { ImageMessage, ImageOldMessage, Message } from '#/entities/message' +import { GALLERY_IMAGES, getModalById } from '#/features/modals' + +interface ImageMessagesListProps { + device: 'mobile' | 'desktop' + images: Message[] + getMessagesPagination?: () => Promise + isComplete: boolean +} + +export const ImageMessagesListOld = memo(({ device, images, getMessagesPagination }: ImageMessagesListProps) => { + const { chosenImage, setChosenImage, computedLibraryImages } = useMessages(images) + + const modal = getModalById(GALLERY_IMAGES) + + const deviceType = getDeviceType() + + return ( + <> + + {createPortal( + , + document.getElementById('modal-container')! + )} + + +
+

ГЕНЕРАЦИИ

+ +
+ {images.map((message, index) => ( + { + setChosenImage(message.file) + modal.setState(true) + }} + key={message.uid} + {...message} + /> + ))} +
+
+ + ) +}) @@ -1,3 +1,6 @@ + + + .model { position: absolute; top: 12px; @@ -7,7 +10,7 @@ font-size: 13px; padding: 2px 8px; border-radius: 10px; - font-weight: 500; + font-weight: 500; color: #fff; } .wrap { @@ -46,25 +49,24 @@ } } -.list { - display: flex; - flex-direction: column; - gap: 10px; -} +.images { + margin-top: 15px; + &__title { + padding-bottom: 30px; + font-size: 16px; + color: var(--new-ui-gray-color); + font-weight: 600; + letter-spacing: 0.03rem; + } -.container { - padding-top: 70px; - padding-bottom: 150px; - position: relative; -} + &__container { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 20px; -.scroll { - position: absolute; - top: 0; - left: 0; -} -.loader{ - margin: 0 auto; - width: fit-content; - margin-bottom: 20px; + @media screen and (max-width: 1000px) { + justify-content: center; + } + } } @@ -5,9 +5,9 @@ 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' export * from './chat-messages-list' export * from './chat-messages-list' export * from './image-messages-list' export * from './answer-wrap' export * from './is-next-day' +export * from './image-messages-list-old' @@ -58,6 +58,12 @@ export const menuListMiddle = [ link: '/upscale', icon: '/svg/side-menu/image', activeList: ['upscale'], + }, + { + title: 'Олд мод изображений', + link: '/deprecated', + icon: '/svg/side-menu/image', + activeList: ['deprecated'], }, // { title: 'Копирайтинг', link: '/copywriting/my', icon: '/svg/side-menu/copyrating', activeList: ['copywriting'] }, // { title: 'Видео', link: '/video', icon: '/svg/side-menu/video', activeList: [] }, @@ -14,7 +14,7 @@ "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^4.5.8", - "@lottiefiles/dotlottie-react": "^0.13.2", + "@lottiefiles/dotlottie-react": "^0.13.5", "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", "@mui/styled-engine-sc": "^5.11.11", @@ -37,7 +37,7 @@ "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^4.5.8", - "@lottiefiles/dotlottie-react": "^0.13.2", + "@lottiefiles/dotlottie-react": "^0.13.5", "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", "@mui/styled-engine-sc": "^5.11.11", @@ -106,10 +106,10 @@ }, "devDependencies": { "@svgr/webpack": "^8.1.0", - "@types/draftjs-to-html": "^0.8.4", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", + "@types/draftjs-to-html": "^0.8.4", "@types/intro.js": "^5.1.1", "@types/jest": "^29.5.14", "@types/lodash": "^4.14.195",