@@ -1,3 +0,0 @@
-{
- "typescript.tsdk": "node_modules/typescript/lib"
-}
Binary files a/public/images/dashboard.png and /dev/null differ
@@ -1,3 +1,3 @@
{
- "main-page": "AIR"
+ "main-page": "AIR"
}
@@ -1,3 +1,3 @@
{
- "main-page": "AIR"
+ "main-page": "AIR"
}
@@ -1,3 +1,5 @@
+
+
\ No newline at end of file
@@ -1,5 +1,5 @@
@@ -1,18 +1,19 @@
{
- "name": "AIR",
- "short_name": "AIR",
- "icons": [
- {
- "src": "/logo192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/logo512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ],
- "start_url": "/",
- "display": "standalone"
-}
+ "name": "AIR",
+ "short_name": "AIR",
+ "icons": [
+ {
+ "src": "/logo192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/logo512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "/",
+ "display": "standalone"
+ }
+
\ No newline at end of file
@@ -17,6 +17,8 @@ const acceptTypes: any = {
image: 'image/png,image/jpeg',
zip: 'application/zip',
pdf: 'application/pdf',
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ doc: 'application/msword',
audio: 'audio/*',
txt: '.txt',
text: '',
@@ -59,14 +61,7 @@ export const LoadImage = ({ loading, unpinImage, image, imageLoad, types }: IPro
}
return (
<>
-
+
setModal(false)}>
-
+
{
+ console.log(swiper?.activeIndex)
+ }}
simulateTouch={false}
- initialSlide={currentImageIndex}
+ initialSlide={currentIndex}
onSwiper={(swiper) => 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}
- />
- )}
-
+ {images.map((image, index) => (
+
+
))}
@@ -1 +1,2 @@
export { default as ImageModal } from './full-screen-modal'
+export * from './modal-image'
@@ -0,0 +1,23 @@
+.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;
+ }
+}
@@ -0,0 +1,41 @@
+import { Message } from '#/entities/message'
+import { Loader } from '#/shared'
+import Image from 'next/image'
+import React, { useEffect } from 'react'
+
+import styles from './modal-image.module.scss'
+
+interface ModalImageProps extends Message {}
+
+export const ModalImage = ({ file, ...image }: ModalImageProps) => {
+ const [state, updateImageState] = React.useState(false)
+
+ return (
+ <>
+ {!state && }
+
+ {!file.includes('.svg') ? (
+
e.stopPropagation()}
+ onLoadingComplete={() => {
+ updateImageState(true)
+ }}
+ loading='lazy'
+ src={file}
+ width={'1500'}
+ height={'1500'}
+ alt='К сожалению, изображение не загрузилось'
+ />
+ ) : (
+
e.stopPropagation()}
+ onLoad={() => updateImageState(true)}
+ alt='К сожалению, изображение не загрузилось'
+ className={styles.image_style}
+ />
+ )}
+
+ >
+ )
+}
@@ -63,29 +63,7 @@
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;
@@ -1,4 +1,4 @@
-import React, { Dispatch, SetStateAction } from 'react'
+import React, { Dispatch, SetStateAction, useMemo } from 'react'
import Image from 'next/image'
import styles from './modal-styles.module.scss'
@@ -6,10 +6,13 @@ import styles from './modal-styles.module.scss'
interface IProps {
modal: boolean
setModal: Dispatch>
- image: string
+ image: string | null
}
export default function FullScreenModal({ modal, setModal, image }: IProps) {
- const isSvg = image.includes('.svg')
+ const isSvg = useMemo(() => {
+ if (!image) return false
+ return image.includes('.svg')
+ }, [])
return (
setModal(false)}>
-
+
) : (
@@ -1,30 +1,30 @@
-.close_block {
- position: absolute;
- z-index: 105;
- cursor: pointer;
- right: 25px;
- top: 25px;
+.close_block{
+ position: absolute;
+ z-index: 105;
+ cursor: pointer;
+ right: 25px;
+ top: 25px;
}
-.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;
+.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;
}
-.image_style {
- position: relative;
- width: 100%;
- height: 100%;
- max-width: 960px;
- max-height: 700px;
- border-radius: 15px;
- object-fit: contain;
-}
+.image_style{
+ position: relative;
+ width: 100%;
+ height: 100%;
+ max-width: 960px;
+ max-height: 700px;
+ border-radius: 15px;
+ object-fit: contain;
+}
\ No newline at end of file
@@ -1,41 +1,14 @@
-.main {
- width: 100%;
- display: flex;
- justify-content: flex-start;
- gap: 20px;
- margin-bottom: 24px;
-
- .chatWindow {
- width: 70%;
- }
-
- .settings {
- width: 25%;
- }
-
- @media (max-width: 768px) {
- width: 100%;
- display: flex;
-
- flex-direction: column-reverse;
-
- .chatWindow {
- width: 100%;
- }
-
- .settings {
- width: 100%;
- }
- }
-}
.wrapModelTitle {
margin-bottom: 20px;
margin-top: 15px;
+ // width: max-content;
+ width: 100% !important;
@media (max-width: 768px) {
margin: 0;
margin-top: 15px;
}
.bigTitle {
+ min-width: max-content !important;
@media (max-width: 768px) {
display: none;
overflow: hidden;
@@ -2,9 +2,17 @@ import React from 'react'
import { Box, Typography } from '@mui/material'
import Link from 'next/link'
-import styles from '#/shared/styles/chats-bot-pages.module.scss'
+import styles from './title.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 +20,21 @@ export default function Title(props: any) {
{' '}
{props.type} •{' '}
- {props.title}
+
+ {props.title}
+
-
- {props.title}
-
+
+
+ {props.title}
+
+
+ {props.rightSlot}
+
+
)
}
@@ -0,0 +1,258 @@
+import React from 'react'
+import { Box, Card, CardMedia, Stack, TextField, Typography } from '@mui/material'
+import Button from '@mui/material/Button'
+import axios from 'axios'
+import Head from 'next/head'
+import Image from 'next/image'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+import { Layout } from '#/app/layout'
+import { useAppSelector } from '#/app/store/store'
+import { API_URL } from '#/shared/lib/constants/constants'
+import { getRandomImage, getTypeDevice } from '#/shared/lib/helpers'
+import { TDeviceProp } from '#/shared/lib/types/entities'
+
+export async function getServerSideProps(context: any): Promise<{ props: TDeviceProp }> {
+ const device = getTypeDevice(context)
+
+ return {
+ props: {
+ device,
+ },
+ }
+}
+
+const ChangePassword: React.FC = ({ device }) => {
+ const [password1, setPassword1] = React.useState('')
+
+ const [password2, setPassword2] = React.useState('')
+
+ const [isChange, setIsChange] = React.useState(false)
+
+ const { query, push } = useRouter()
+
+ const changePassword = async () => {
+ let formData: any = new FormData()
+
+ formData.append('password_1', password1)
+ formData.append('password_2', password2)
+
+ if (password1.trim() && password2.trim()) {
+ try {
+ await axios.post(API_URL + `/auth/change-pass?token=${query.token}`, formData, {
+ headers: {
+ 'content-type': 'multipart/form-data ',
+ },
+ })
+ setIsChange(true)
+ setTimeout(() => push('/login'), 5000)
+ } catch (err) {}
+ }
+ }
+
+ const desktop = device === 'desktop'
+
+ const refImage = React.useRef(getRandomImage())
+
+ const theme = useAppSelector((state) => state.theme.theme)
+
+ return (
+
+
+ Восстановление аккаунта
+
+
+
+
+
+
+
+
+ {desktop && (
+
+
+
+
+ Midjourney
+
+
+ by honeynek
+
+
+
+
+ )}
+ {isChange ? (
+
+
+ Ваш пароль успешно изменён!
+
+
+ Войти в аккаунт
+
+
+ ) : (
+
+
+ Восстановление пароля
+
+
+ Новый пароль
+
+ setPassword1(e.target.value)}
+ >
+
+ Подтверждение пароля
+
+ setPassword2(e.target.value)}
+ >
+
+
+
+
+ )}
+
+
+ )
+}
+
+export default ChangePassword
@@ -1,8 +1,7 @@
import axios from 'axios'
-import { IModel } from '#/shared/api/models/models'
import { API_URL } from '#/shared/lib/constants'
-import { IShortModel } from '#/entities/model-entity'
+import { IShortModel, IModel } from '#/entities/model-entity'
const model_api = {
async getBots(token?: string): Promise {
@@ -8,8 +8,8 @@ import { getUserBalance } from '#/entities/balance'
import { useAppDispatch } from '#/app/store/store'
import { API_URL } from '#/shared/lib/constants'
import { Device } from '#/shared/lib/types/entities'
-import { Message, MessageSend } from '#/shared/lib/types/model'
import { IMessageRequest } from '#/shared/lib/types/types-gpt'
+import { Message, MessageSend } from '#/entities/message'
const formDataHelper = (file: File, dataForSend: MessageSend): FormData => {
const FD = new FormData()
@@ -120,23 +120,31 @@ export function useModel(
return
}
+ const date = new Date()
+
const userMessage: Message = {
content: dataForSend.content.trim(),
info: dataForSend.info as any,
is_sent: true,
- file: dataForSend.file ? ((await base64(dataForSend.file)) as string) : null,
+ model: '',
+ file: dataForSend.file
+ ? ((URL.createObjectURL(dataForSend.file) +
+ '?type=.' +
+ dataForSend.file.name.split('.')[1]) as string)
+ : null,
from_model: false,
uid: 'new-send',
elapsed_time: '',
is_favourite: false,
- created_at: new Date().toISOString(),
+ created_at: date.toISOString(),
}
const modelMessageAboutStartGeneration: Message = {
...userMessage,
content: `Ваш вопрос получен. Ожидание ответа от ${modelType}...`,
from_model: true,
- created_at: new Date().toISOString(),
+ file: null,
+ created_at: date.toISOString(),
}
setLoading(true)
@@ -172,7 +180,7 @@ export function useModel(
setMessages((prev) => [...prev!, userMessage])
try {
//@ts-ignore
- const message = (result.details as AxiosError).response!.data.trim() ?? 'Ошибка отправки сообщения'
+ const message = (result.details as AxiosError).response.data.trim() ?? 'Ошибка отправки сообщения'
showError(message)
return
} catch (e) {
@@ -186,7 +194,7 @@ export function useModel(
}
const deleteMessage = (message_uid: string) => {
- axios.delete(process.env.NEXT_PUBLIC_MAIN_URL + `/chats/${currentChat}/messages/${message_uid}`, {
+ axios.delete(process.env.NEXT_PUBLIC_API_HOST + `/chats/${currentChat}/messages/${message_uid}`, {
headers: {
Authorization: `Bearer ${data?.access}`,
},
@@ -449,6 +457,7 @@ export function useMedia(showError: (message: string) => void, modelType: string
content: input,
info: null,
is_sent: true,
+ model: '',
file: null,
from_model: false,
uid: 'new-send',
@@ -1,70 +0,0 @@
-.card {
- width: 381px;
- overflow: hidden;
- position: relative;
- height: 290px;
- border-radius: 15px;
- background-color: var(--new-ui-main-color);
- margin-top: 15px;
- margin-right: 20px;
-
- @media (max-width: 420px) {
- width: 92vw;
- height: 300px;
- }
-
- .description {
- padding: 15px 20px;
-
- .title {
- color: var(--new-ui-text-color);
- font-weight: 600;
- font-size: 21px;
- }
- .text {
- margin-top: 15px;
- font-size: 15px;
- color: var(--new-ui-gray-color);
- }
- }
-}
-
-.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;
- }
-}
@@ -1,72 +0,0 @@
-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
- icon: string
- title: string
- slug: string
- uid: string
- blocked: boolean
- accessed_models?: string[] | null
-}
-
-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 (
-
-
-
-
- {title}
- {text}
-
-
- {blocked ? (
-
-
-
- Модель недоступна
-
-
- ) : (
- accessed_models &&
- !accessed_models.includes(slug) && (
-
-
- Недоступно в текущем тарифе
-
- )
- )}
-
-
- )
-}
@@ -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)
+}
@@ -29,7 +29,6 @@ export const getDeviceType = (): Device => {
return isMobile ? 'mobile' : 'desktop'
}
-
/**
* @deprecated
*/
@@ -4,3 +4,4 @@ export { useConcat } from './reactive-concat'
export * from './get-type-device'
export * from './get-random-image'
export * from './string'
+export * from './date-helper'
@@ -1,3 +1,13 @@
-export const c = (...classes: string[]) => {
- return classes.filter(Boolean).join(' ')
+export function c(...args: Array) {
+ return args.filter(Boolean).join(' ')
+}
+
+export function commaSeparated(input: number | string | null | undefined): string {
+ const str: string = typeof input === 'number' || typeof input === 'string' ? String(input) : ''
+
+ if (str) {
+ return str.replace(/\B(? {
+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 }
}
@@ -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,58 @@
+.button {
+ border: none;
+ outline: none;
+ background-color: transparent;
+ padding: 15px 25px;
+ cursor: pointer;
+ box-sizing: content-box;
+
+ &_outline {
+ border-radius: 15px;
+ border: 0.15rem solid var(--new-ui-text-color);
+ color: var(--new-ui-text-color);
+ 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: var(--new-ui-text-color);
+ 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: var(--new-ui-text-color);
+ }
+ }
+}
@@ -0,0 +1,26 @@
+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,35 @@
+import axios from 'axios'
+import { useState, useEffect, HtmlHTMLAttributes } from 'react'
+
+export interface SvgIconProps extends HtmlHTMLAttributes {
+ width?: string | number
+ height?: string | number
+ color?: string
+ url: string
+}
+
+export function SvgIcon({ url, color, width, height, ...props }: SvgIconProps) {
+ const [svgContent, setSvgContent] = useState('')
+
+ async function onFetch() {
+ const response = await axios.get(url)
+
+ if (response.status === 200) {
+ setSvgContent(response.data)
+ }
+ }
+
+ useEffect(() => {
+ onFetch()
+ }, [])
+
+ return (
+
+ )
+}
@@ -0,0 +1 @@
+export * from './dynamic-svg'
\ No newline at end of file
@@ -0,0 +1 @@
+export * from './ui'
\ No newline at end of file
@@ -10,20 +10,25 @@ import { useChats } from '#/features/chats'
import Title from '#/features/title/title'
import { TutorialContext } from '#/features/tutorial-context/tutorial-context'
import { ChatSelect } from '#/app/components/chat_select'
-import { AllChatWindow } from '#/app/components/chat_window'
import { ResetFilters } from '#/app/components/filters/reset_filters'
import { setParams as setParametres } from '#/app/store/model-parametres-store'
import { useAppSelector } from '#/app/store/store'
import { DrawerCustom, Error, useModel, useShowData } from '#/shared'
import model_api from '#/shared/api/models/api'
-import { IModel } from '#/shared/api/models/models'
-import styles from '#/shared/styles/chats-bot-pages.module.scss'
+import styles from './chats-bot.module.scss'
import { Chats } from '#/widgets/chats'
-import { getDeviceType, getOs } from '#/shared/lib/helpers'
+import { c, getDeviceType, getOs } from '#/shared/lib/helpers'
import { NextPageWithLayout } from '#/pages/_app'
import { getDefaultLayoutDynamic } from '#/widgets/layouts'
import Head from 'next/head'
+import LockSvg from '#/assets/svg/lock.svg?react'
+import BlockedSvg from '#/assets/svg/blocked.svg?react'
+import { IModelTag, IModel } from '#/entities/model-entity'
+import { useThemeAndDevice } from '#/shared/lib/hooks'
+import { SvgIcon } from '#/shared/ui/svg'
+import { AllChatWindow } from '#/widgets/chat-window'
+
export interface ChatBotPageProps {}
const Page: NextPageWithLayout = () => {
@@ -68,12 +73,12 @@ const Page: NextPageWithLayout = () => {
const deleteMessageMemo = useCallback(deleteMessage, [currentChat, messages])
- const { push } = useRouter()
+ const { push } = useRouter()
React.useEffect(() => {
if (data?.access) {
model_api.getBotParams(router.asPath.split('/')[2], data.access).then((res) => {
- if (!res.title) return push('/404')
+ if (!res.title) return push('/404')
setBotParams(res)
setModelType(res.slug)
if (res.versions.length !== 0) {
@@ -92,7 +97,12 @@ const Page: NextPageWithLayout = () => {
} else {
setVersion('')
dispatch(
- setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))
+ setParametres(
+ res.parameters.reduce(
+ (a, v) => ({ ...a, [v.key]: v.values.default }),
+ {}
+ )
+ )
)
}
})
@@ -118,7 +128,12 @@ const Page: NextPageWithLayout = () => {
} else {
setVersion(botParams.slug)
dispatch(
- setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))
+ setParametres(
+ botParams.parameters.reduce(
+ (a, v) => ({ ...a, [v.key]: v.values.default }),
+ {}
+ )
+ )
)
}
}
@@ -132,14 +147,21 @@ const Page: NextPageWithLayout = () => {
setParametres(
botParams.parameters.reduce(
(a, v) =>
- v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a },
+ v.versions.includes(version)
+ ? { ...a, [v.key]: v.values.default }
+ : { ...a },
{}
)
)
)
} else {
dispatch(
- setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))
+ setParametres(
+ botParams.parameters.reduce(
+ (a, v) => ({ ...a, [v.key]: v.values.default }),
+ {}
+ )
+ )
)
}
}
@@ -185,20 +207,48 @@ const Page: NextPageWithLayout = () => {
return true
}
+ const { theme } = useThemeAndDevice()
+
return (
<>
{botParams ? botParams.title : 'Загрузка...'}
-
-
+
+
+ {botParams &&
+ botParams.tags.map((tag, index) => (
+
+
+
+
+ {tag.title}
+
+
+ ))}
+
+ }
+ type={'Чат-боты'}
+ linkBack={'/chat-bot'}
+ />
+
+
= () => {
modelTitle={botParams?.title}
currentVersion={version}
/>
+ {/* {botParams?.blocked && (
+
+
+
+ Модель недоступна
+
+
+ )} */}
= () => {
handleClickChatSetting={handleClickChatSetting}
/>
{desktop && (
-
- {botParams?.versions && botParams.versions?.length !== 0 && (
- <>
-
- ВЕРСИИ
-
-
- >
- )}
+
+ {botParams?.versions &&
+ botParams.versions?.length !== 0 && (
+ <>
+
+ ВЕРСИИ
+
+
+ >
+ )}
{botParams && botParams.parameters?.length > 0 && (
= () => {
viewBox='0 0 21 13'
fill='none'
xmlns='http://www.w3.org/2000/svg'
- className={`${params ? 'rotate-180' : 'rotate-0'}`}
+ className={`${
+ params ? 'rotate-180' : 'rotate-0'
+ }`}
>
= () => {
)}
{botParams && botParams.parameters?.length > 0 ? (
-
+
= () => {
)}
{!desktop && (
-
+
- {botParams?.versions && botParams.versions?.length !== 0 && (
- <>
-
- ВЕРСИИ
-
-
- >
- )}
+ {botParams?.versions &&
+ botParams.versions?.length !== 0 && (
+ <>
+
+ ВЕРСИИ
+
+
+ >
+ )}
{botParams && botParams.parameters?.length > 0 ? (
<>
{
const [bots, setBots] = useState(null)
@@ -16,32 +16,25 @@ const Page: NextPageWithLayout = () => {
const user = useAppSelector((state) => state.user)
useEffect(() => {
- if (!data?.access) {
- return
- }
- model_api
- .getBots(data?.access)
- .then((res) => {
- setBots(res)
- })
- .catch((err) => {})
- }, [data?.access])
+ if (!data) return
+
+ model_api.getBots(data.access).then((res) => {
+ setBots(res)
+ })
+ }, [data])
return (
<>
- Чат-боты
-
+
+ Чат-боты
+
+
{bots ? (
- bots.map((item, idx) => (
- (
+
))
) : (
@@ -58,7 +51,7 @@ const Page: NextPageWithLayout = () => {
}}
/>
)}
-
+
>
)
}
@@ -0,0 +1,136 @@
+@keyframes fadein {
+ 0% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.header {
+ width: 70%;
+
+ @media screen and (max-width: 768px) {
+ width: 100%;
+ }
+}
+
+.tag {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--air-color);
+ 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;
+
+ &__icon {
+ color: white;
+ }
+
+ &: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: white;
+ font-weight: 500;
+ padding-left: 10px;
+ font-size: 14px;
+ opacity: 0;
+ display: none;
+ min-width: max-content !important;
+ }
+}
+
+.tags {
+ min-width: 100%;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ justify-content: flex-end;
+
+ @media screen and (max-width: 768px) {
+ display: none;
+ }
+}
+
+.main {
+ width: 100%;
+ display: flex;
+ justify-content: flex-start;
+ align-items: stretch;
+ gap: 20px;
+ // margin-bottom: 24px;
+
+ .chatWindow {
+ width: 70%;
+ position: relative;
+ }
+
+ .settings {
+ width: 25%;
+ }
+
+ @media (max-width: 768px) {
+ width: 100%;
+ display: flex;
+
+ flex-direction: column-reverse;
+
+ .chatWindow {
+ width: 100%;
+ }
+
+ .settings {
+ width: 100%;
+ }
+ }
+}
+
+.icon {
+}
+
+.blocked {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #c32528;
+ // padding-right: 20px;
+ padding: 8px 15px;
+ gap: 5px;
+ position: absolute;
+ left: 0;
+ bottom: -10px;
+ width: 100%;
+ border-radius: 0px 0px 15px 15px;
+
+
+ // @media screen and (max-width: 768px) {
+ // display: none;
+ // }
+
+ &__text {
+ color: white;
+ font-size: 14px;
+ font-weight: 600;
+ }
+}
@@ -0,0 +1,148 @@
+.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) {
+ display: none;
+ }
+}
+
+.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;
+
+ &__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 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #c32528;
+ // padding-right: 20px;
+ padding: 8px 15px;
+ gap: 5px;
+ position: absolute;
+ left: 0;
+
+ width: 100%;
+ border-radius: 15px 15px 0px 0px;
+ top: -23px;
+
+
+ @media screen and (max-width: 768px) {
+ top: unset;
+ bottom: -30px;
+ z-index: 1;
+ border-radius: 0px 0px 15px 15px;
+ }
+
+ &__text {
+ color: white !important;
+ font-size: 14px;
+ font-weight: 600;
+ }
+}
@@ -1,263 +1,128 @@
-import * as React from 'react'
-import { useRef } from 'react'
-import { useDispatch } from 'react-redux'
-import { Collapse, Typography } from '@mui/material'
-import Box from '@mui/material/Box'
-import Stack from '@mui/material/Stack'
-import { useRouter } from 'next/router'
-import { useSession } from 'next-auth/react'
-
-import { Setting } from '#/domains/images-bots/babes/types'
-import BotParamsMap from '#/features/bot-params/bot-params-map'
-import Title from '#/features/title/title'
import { ChatSelect } from '#/app/components/chat_select'
import { ResetFilters } from '#/app/components/filters/reset_filters'
import { UniqInput } from '#/app/components/uniq_input'
-import { setParams as setParametres } from '#/app/store/model-parametres-store'
-import { useAppSelector } from '#/app/store/store'
-import { DrawerCustom, Error } from '#/shared'
-import model_api from '#/shared/api/models/api'
-import { useModelImages } from '#/shared/api/models/endpoints'
-import { IModel } from '#/shared/api/models/models'
-import { useShowData } from '#/shared/lib/hooks'
+import { useImageBot } from '#/entities/model-entity/model/use-image-bot'
+import BotParamsMap from '#/features/bot-params/bot-params-map'
+import { useImageBotCreateImage } from '#/features/image-bot-create-image'
+import { useImagesBotFilters } from '#/features/image-bot-filters'
+import { useImagesUniqInput } from '#/features/image-bot-input'
+import { useImageBotPagination } from '#/features/image-bot-pagination'
+import Title from '#/features/title/title'
+import { useShowData, DrawerCustom, Error, Loader } from '#/shared'
+import { useThemeAndDevice } from '#/shared/lib/hooks'
import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow'
-import { ImageMessagesList } from '#/widgets/messages/ui/image-messages-list'
-import { getDeviceType, getOs } from '#/shared/lib/helpers'
+import { useImagesPagination, ImageMessagesList } from '#/widgets/messages'
+import { Layout } from '#/app/layout'
+import { Box, Typography, Collapse, Stack } from '@mui/material'
+import { useSession } from 'next-auth/react'
import Head from 'next/head'
+import { useRouter } from 'next/router'
+import { useEffect, useMemo } from 'react'
+import { c, getDeviceType, getOs } from '#/shared/lib/helpers'
+import { Device, DeviceOs } from '#/shared/lib/types/entities'
+import LockSvg from '#/assets/svg/lock.svg?react'
+import BlockedSvg from '#/assets/svg/blocked.svg?react'
+
+import styles from './image-model.module.scss'
import { NextPageWithLayout } from '#/pages/_app'
+import { SvgIcon } from '#/shared/ui/svg'
const ImageModelPage: NextPageWithLayout = () => {
- const [image, setImage] = React.useState(null)
+ const { query } = useRouter()
+
+ const {
+ botParams,
+ version,
+ modelType,
+ fetchBotParams,
+ resetParams,
+ setDefaultParams,
+ setVersion,
+ } = useImageBot(query.slug as string)
const deviceType = getDeviceType()
const deviceOs = getOs()
- const desktop = deviceType === 'desktop'
- const ios = deviceOs === 'ios'
+ const { ios, desktop } = useThemeAndDevice(deviceType, deviceOs)
const { error, showError } = useShowData()
- const [openFiltersMobile, setOpenFiltersMobile] = React.useState(false)
- const router = useRouter()
- const { data } = useSession()
- const [botParams, setBotParams] = React.useState(null)
- const [version, setVersion] = React.useState('')
- const [modelType, setModelType] = React.useState('')
- const [params, setParams] = React.useState(false)
- const includeParams = useAppSelector((state) => state.params.params)
- const dispatch = useDispatch()
-
- const { messages, loading, createImage, isComplete, getMessagesPagination } = useModelImages(
- showError,
- modelType,
- deviceType
- )
-
- const [chatScrollHeight, setChatScrollHeight] = React.useState(0)
- const [scrollBottom, setScrollBottom] = React.useState(0)
- const refScrollMobile = useRef()
- const [isPaginating, setIsPaginating] = React.useState(false)
-
- React.useEffect(() => {
- model_api
- .getBotParams(router.asPath.split('/')[2], data?.access)
- .then((res) => {
- if (!res.title) return router.push('/404')
- setBotParams(res)
- setModelType(res.slug)
- if (res.versions.length !== 0) {
- setVersion(res.versions[0].slug)
- dispatch(
- setParametres(
- res.parameters.reduce(
- (a, v) =>
- v.versions.includes(res.versions[0].slug)
- ? { ...a, [v.key]: v.values.default }
- : { ...a },
- {}
- )
- )
- )
- } else {
- setVersion('')
- dispatch(
- setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))
- )
- }
- })
- .catch((err) => {})
- }, [data?.access, router.query])
-
- const onLoadImage = (event: React.ChangeEvent) => {
- if (event.target.files) {
- setImage(event.target.files[0])
- // showError('Файл успешно загружен, можете отправлять его!')
- }
- }
-
- const viewMobileSettings = () => {
- setOpenFiltersMobile(true)
- }
-
- const hideMobileSettings = () => {
- setOpenFiltersMobile(false)
- }
-
- const resetParams = () => {
- if (botParams) {
- dispatch(setParametres({}))
- if (botParams.versions.length !== 0) {
- setVersion(botParams.versions[0].slug)
- dispatch(
- setParametres(
- botParams.parameters.reduce(
- (a, v) =>
- v.versions.includes(botParams.versions[0].slug)
- ? { ...a, [v.key]: v.values.default }
- : { ...a },
- {}
- )
- )
- )
- } else {
- setVersion(botParams.slug)
- dispatch(
- setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))
- )
- }
- }
- }
- const setDefaultParams = () => {
- if (botParams) {
- dispatch(setParametres({}))
- if (version !== '') {
- dispatch(
- setParametres(
- botParams.parameters.reduce(
- (a, v) =>
- v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a },
- {}
- )
- )
- )
- } else {
- dispatch(
- setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))
- )
- }
- }
- }
+ const router = useRouter()
- const onCreateImage = (input: string, required: (string | null)[]) => {
- 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,
- }
- }
+ const { data: session } = useSession()
- createImage({
- content: input,
- file: image,
- info: {
- ...data,
- },
- })
- return true
- }
+ const { openFiltersMobile, setOpenFiltersMobile, params, setParams, includeParams } =
+ useImagesBotFilters()
- React.useEffect(() => {
- if (isComplete) {
- if (!desktop) {
- const block = refScrollMobile.current
- if (block) {
- //@ts-ignore
- block.scrollTop = block.scrollHeight
- }
- } else {
- window.scroll(0, 0)
- }
- }
- }, [isComplete])
+ const {
+ refScrollMobile,
+ refScrollDesktop,
+ mobileScrollContainer,
+ onObserverMounted,
+ setMessages,
+ fetchMessages,
+ loading,
+ offset,
+ messages,
+ } = useImageBotPagination(deviceType)
- const handleMobileScroll = () => {
- setScrollBottom(
- refScrollMobile.current?.scrollHeight -
- refScrollMobile.current?.scrollTop -
- refScrollMobile.current?.clientHeight
- )
+ const { createImage, isComplete, createLoading } = useImageBotCreateImage(
+ showError,
+ modelType,
+ deviceType,
+ setMessages,
+ mobileScrollContainer
+ )
- if (refScrollMobile.current && messages?.length !== 0) {
- const { scrollTop, scrollHeight, clientHeight } = refScrollMobile.current
- if (scrollTop === 0) {
- if (getMessagesPagination) {
- setIsPaginating(true)
- getMessagesPagination(deviceType)
- }
- }
- }
- }
+ const { onCreateImage, onLoadImage, image, setImage } = useImagesUniqInput(
+ version,
+ includeParams,
+ createImage
+ )
- const handleScroll = () => {
- if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight) {
- setIsPaginating(true)
- getMessagesPagination(deviceType)
- }
+ async function onFetch() {
+ await Promise.all([fetchBotParams()])
}
- React.useEffect(() => {
- window.addEventListener('scroll', handleScroll)
- return () => {
- window.removeEventListener('scroll', handleScroll)
- }
- }, [])
-
- React.useEffect(() => {
- const block = deviceType === 'desktop' ? window : refScrollMobile.current
- if (block) {
- if (messages != undefined && !isPaginating) {
- setChatScrollHeight(block.scrollHeight)
- const time = setTimeout(() => {
- //@ts-ignore
- block.scrollTo({
- top: block.scrollHeight,
- behavior: 'smooth', // добавляем плавную прокрутку
- })
- }, 350)
- return () => clearTimeout(time)
- } else if (messages != undefined && isPaginating) {
- //@ts-ignore
- block.scrollTop = block.scrollHeight - chatScrollHeight
+ useEffect(() => {
+ if (!session) return
+ onFetch()
+ onObserverMounted()
+ }, [session?.access])
- setChatScrollHeight(block.scrollHeight)
- }
- }
- setIsPaginating(false)
- }, [messages])
+ const { theme } = useThemeAndDevice()
return (
<>
- {botParams ? botParams.title : 'Загрузка...'}
+ {botParams ? botParams?.title : 'Загрузка...'}
-
+
+
+ {botParams &&
+ botParams.tags.map((tag, index) => (
+
+
+
+
+ {tag.title}
+
+
+ ))}
+
+ }
+ />
+
{
>
{desktop ? (
<>
-
+
{botParams && (
{
image={image}
value={prompt}
desktop={desktop}
- loading={loading}
+ blocked={botParams.blocked}
+ loading={createLoading}
imageLoad={onLoadImage}
sendMessage={onCreateImage}
unpinImage={() => setImage(null)}
- viewMobileSettings={viewMobileSettings}
+ viewMobileSettings={() =>
+ setOpenFiltersMobile(true)
+ }
/>
)}
+ {botParams?.blocked && (
+
+
+
+ Модель недоступна
+
+
+ )}
-
+
getMessagesPagination(deviceType)}
- isComplete={isComplete}
+ isComplete={loading}
device={deviceType}
images={messages}
+ getMessagesPagination={fetchMessages}
/>
-
- >
- ) : (
-
- {scrollBottom > 500 && (
{
- const block = refScrollMobile.current
-
- block.scrollTo({
- top: block.scrollHeight,
- behavior: 'smooth', // добавляем плавную прокрутку
- })
+ ref={refScrollDesktop}
+ >
+
+ >
+ ) : (
+
+
+
+
getMessagesPagination(deviceType)}
- isComplete={isComplete}
+ isComplete={loading}
device={deviceType}
images={messages}
+ getMessagesPagination={fetchMessages}
/>
-
+
{botParams && (
setImage(null)}
- viewMobileSettings={viewMobileSettings}
+ viewMobileSettings={() =>
+ setOpenFiltersMobile(true)
+ }
/>
)}
+ {botParams?.blocked && (
+
+
+
+ Модель недоступна
+
+
+ )}
)}
@@ -438,11 +360,19 @@ const ImageModelPage: NextPageWithLayout = () => {
)}
{botParams && botParams.parameters?.length > 0 ? (
-
-
+
+
setOpenFiltersMobile(false)}
reset={resetParams}
/>
@@ -459,7 +389,10 @@ const ImageModelPage: NextPageWithLayout = () => {
)}
)}
-
+ setOpenFiltersMobile(false)}
+ >
{botParams?.versions && botParams.versions.length !== 0 ? (
<>
@@ -497,9 +430,12 @@ const ImageModelPage: NextPageWithLayout = () => {
>
ПАРАМЕТРЫ
-
+
setOpenFiltersMobile(false)}
desktop={desktop}
reset={resetParams}
/>
@@ -0,0 +1,7 @@
+.cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 20px;
+ padding-top: 40px;
+ margin-right: 60px;
+}
@@ -8,7 +8,9 @@ import { useAppSelector } from '#/app/store/store'
import model_api from '#/shared/api/models/api'
import { Card } from '#/shared/card/card'
import { NextPageWithLayout } from '#/pages/_app'
-import { IShortModel } from '#/entities/model-entity'
+import { ImageModelCard, IShortModel } from '#/entities/model-entity'
+
+import styles from './image-models.module.scss'
export const ImageModelsPage: NextPageWithLayout = () => {
const [bots, setBots] = useState(null)
@@ -23,20 +25,17 @@ export const ImageModelsPage: NextPageWithLayout = () => {
return (
<>
- Изображения
-
+
+ Изображения
+
+
{bots ? (
- bots.map((item, idx) => {
+ bots.map((item) => {
return (
-
)
})
@@ -54,7 +53,7 @@ export const ImageModelsPage: NextPageWithLayout = () => {
}}
/>
)}
-
+
>
)
}
@@ -0,0 +1,28 @@
+.blocked {
+ align-items: center;
+ justify-content: center;
+ background-color: #c32528;
+ padding: 8px 15px;
+ gap: 5px;
+ position: absolute;
+ left: 0;
+ // display: none;
+ display: flex;
+ bottom: -21px;
+ width: 100%;
+ border-radius: 0px 0px 15px 15px;
+
+ // @media screen and (max-width: 768px) {
+ // top: -30px;
+ // bottom: unset;
+ // z-index: 1;
+ // display: flex;
+ // border-radius: 15px 15px 0px 0px;
+ // }
+
+ &__text {
+ color: white !important;
+ font-size: 14px;
+ font-weight: 600;
+ }
+}
@@ -4,10 +4,45 @@ 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'
+import BlockedSvg from '#/assets/svg/blocked.svg?react'
+
+import styles from './chat-window.module.scss'
+
import 'intro.js/introjs.css'
+import { c } from '#/shared'
+import { Device, DeviceOs } from '#/shared/lib/types/entities'
+import { IModelInputs } from '#/entities/model-entity'
+import { Message } from '#/entities/message'
+
+interface Elements {
+ element: React.ReactElement
+ position: string
+ onClick?: () => void
+}
+
+export interface ChatProps {
+ device: Device
+ deviceOs?: DeviceOs
+ modelType: string
+ messages: Message[]
+ sendMessage: (message: string, required: (string | null)[]) => boolean
+ openMobileFilters: () => void
+ setting?: T
+ loading: boolean
+ file?: any
+ setFile: React.Dispatch>
+ isCalculating: boolean
+ elements?: Elements[]
+ model?: string
+ blocked?: boolean
+ getMessagesPagination?: () => Promise
+ input_types?: IModelInputs[]
+ deleteMessage: (message_uid: string) => void
+ modelTitle: string | undefined
+ currentVersion: string
+}
function Chat({
device,
@@ -22,6 +57,7 @@ function Chat({
input_types,
deleteMessage,
modelTitle,
+ blocked,
currentVersion,
deviceOs,
}: ChatProps) {
@@ -30,14 +66,17 @@ function Chat({
const theme = useAppSelector((state) => state.theme.theme)
const [resendValue, setResendValue] = React.useState('')
- const onLoadImage = useCallback((event: React.ChangeEvent | null, file?: File) => {
- if (event?.target.files) {
- setFile(event.target.files[0])
- }
- if (file) {
- setFile(file)
- }
- }, [])
+ const onLoadImage = useCallback(
+ (event: React.ChangeEvent | null, file?: File) => {
+ if (event?.target.files) {
+ setFile(event.target.files[0])
+ }
+ if (file) {
+ setFile(file)
+ }
+ },
+ []
+ )
const handleSetResetValue = useCallback(
(value: string) => {
@@ -71,15 +110,11 @@ function Chat({
: theme === 'light'
? 'white'
: '#151518',
- padding: desktop ? 4 : 1.5,
+ padding: desktop ? (blocked ? 2 : 2) : 1.5,
paddingTop: 0,
- paddingBottom: 2,
+ paddingBottom: blocked ? (desktop ? 3.8 : 5) : 1,
width: '100%',
- height: desktop
- ? '75vh'
- : deviceOs === 'ios'
- ? 'calc(200px + (400 - 200) * ((100vh - 400px) / (650 - 400)) - 100px)'
- : 'calc(200px + (400 - 200) * ((100vh - 400px) / (650 - 400)))',
+ height: desktop ? '75vh' : 'calc(100dvh - 245px)',
overflow: 'hidden',
}}
>
@@ -94,19 +129,32 @@ function Chat({
modelTitle={modelTitle}
loading={loading}
/>
-
+
+
+
+
+ {blocked && (
+
+
+
+ Модель недоступна
+
+
+ )}
+
>
)
@@ -0,0 +1 @@
+export * from './chat-window'
\ No newline at end of file
@@ -0,0 +1 @@
+export * from './ui'
\ No newline at end of file
@@ -1,4 +1,4 @@
-import React, { useState } from 'react'
+import React, { useEffect, useState } from 'react'
import { Box } from '@mui/material'
import { ThemeProvider } from '@mui/material/styles'
import Head from 'next/head'
@@ -34,6 +34,7 @@ export const Layout: React.FC = ({
const appState = useAppSelector((state) => state)
const dispatch = useAppDispatch()
+
const { userLoading } = useAppSelector((state) => state.loading)
const desktop = device === 'desktop'
@@ -93,7 +94,13 @@ export const Layout: React.FC = ({
if (userLoading || isLoader || status === 'loading') {
return (
-
+
)
@@ -102,7 +109,7 @@ export const Layout: React.FC = ({
return (
<>
- {titlePage}
+ {titlePage && {titlePage}}
@@ -114,7 +121,7 @@ export const Layout: React.FC = ({
id={'layout'}
sx={{
width: '100%',
- height: desktop ? '100%' : '100vh',
+ height: desktop ? '100%' : '100dvh',
padding: desktop ? '0px' : '10px',
}}
>
@@ -125,7 +132,10 @@ export const Layout: React.FC = ({
>
{desktop ? (
-
+
(API_URL + `/media/gallery/images?limit=${limit}&offset=${offset}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+}
@@ -0,0 +1 @@
+export * from './image-messages.routes'
\ No newline at end of file
@@ -0,0 +1,17 @@
+type DateResponse = {
+ month: string
+ year: string
+ day: string
+}
+
+export const getDateFromString = (inputDate: string): DateResponse | null => {
+ if (inputDate) {
+ const date = inputDate.split('T')[0].split('-')
+ return {
+ day: date[2],
+ month: date[1],
+ year: date[0],
+ }
+ }
+ return null
+}
@@ -0,0 +1,21 @@
+export const getDayMontsString = (inputDate: string): string => {
+ const monthNames = [
+ 'января',
+ 'февраля',
+ 'марта',
+ 'апреля',
+ 'мая',
+ 'июня',
+ 'июля',
+ 'августа',
+ 'сентября',
+ 'октября',
+ 'ноября',
+ 'декабря',
+ ]
+
+ const date = new Date(inputDate)
+ const day = date.getDate()
+ const month = monthNames[date.getMonth()]
+ return `${day} ${month}`
+}
@@ -0,0 +1,10 @@
+import { MessageSend } from '#/entities/message'
+
+export function formDataHelper(file: File, dataForSend: MessageSend): FormData {
+ const FD = new FormData()
+ FD.append('file', file)
+ FD.append('info', JSON.stringify(dataForSend.info))
+ FD.append('content', JSON.stringify(dataForSend.content))
+
+ return FD
+}
@@ -1,18 +1,5 @@
export const getDayMontsString = (inputDate: string): string => {
- const monthNames = [
- 'января',
- 'февраля',
- 'марта',
- 'апреля',
- 'мая',
- 'июня',
- 'июля',
- 'августа',
- 'сентября',
- 'октября',
- 'ноября',
- 'декабря',
- ]
+ const monthNames = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']
const date = new Date(inputDate)
const day = date.getDate()
@@ -0,0 +1,3 @@
+export * from './date-from-string'
+export * from './day-months-string'
+export * from './form-data'
\ No newline at end of file
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material'
import Stack from '@mui/material/Stack'
import Image from 'next/image'
@@ -7,7 +7,9 @@ import FullScreenModal from '#/features/image-modal/full-screen-modal'
import { useAppSelector } from '#/app/store/store'
import { TooltipCustom } from '#/shared'
import { Message } from '#/shared/lib/types/model'
-import { BotMessage } from '#/widgets/messages/message-components/bot-message'
+import { BotMessage } from '#/widgets/messages'
+import { Markdown } from '../../markdown/markdown'
+import { formatDate } from '#/shared/lib/helpers'
interface IMessagesList {
// messageResponse: Message[] | null
@@ -18,19 +20,18 @@ interface IMessagesList {
device: 'mobile' | 'desktop'
modelType: string
isNewMessage: boolean
- setCurrentSrc: (value: string) => void
message: Message
- setModal: (value: boolean) => void
deleteMessage?: (message_uid: string) => void
modelTitle: string | undefined
setResendValue: (value: string) => void
onLoadImage?: (event: React.ChangeEvent | null, file?: File) => void
}
-export const UserMessage = React.memo(function UserMessage({ setModal, setCurrentSrc, ...props }: IMessagesList) {
+export const UserMessage = React.memo(function UserMessage(props: IMessagesList) {
const [anchorEl, setAnchorEl] = React.useState(null)
const open = Boolean(anchorEl)
const desktop = props.device === 'desktop'
+ const [modal, setModal] = useState(false)
const [loaded, setLoaded] = useState(false)
const copy = (text: string) => {
@@ -43,6 +44,13 @@ export const UserMessage = React.memo(function UserMessage({ setModal, setCurren
setAnchorEl(null)
}
+ const fileName = useMemo(() => {
+ if (!props.message.file) return 'Подбираем имя файла...'
+ const matches = (props.message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/)
+ if (!matches) return 'Подбираем имя файла...'
+ return `${matches[1]}.${matches[2]}`
+ }, [])
+
const resend = async () => {
props.setResendValue(props.message.content)
if (props.message.file !== null) {
@@ -61,19 +69,37 @@ export const UserMessage = React.memo(function UserMessage({ setModal, setCurren
}
}
+ useEffect(() => {
+ console.log(props.message.file)
+ }, [props.message.file])
+
+ const flexStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'start',
+ gap: '12px',
+ color: '#7f7df3 !important',
+ fontWeight: '600 !important',
+ lineHeight: '140%',
+ letterSpacing: '0.2px',
+ cursor: 'pointer',
+ }
+
const theme = useAppSelector((state) => state.theme.theme)
return (
<>
+ {props.message.file && (
+
+ )}
{!props.message.from_model ? (
-
+ //
+
- {props.message.created_at.slice(10, 16).replace('T', ' ')}
+ {formatDate(props.message.created_at, {
+ hour: 'numeric',
+ minute: 'numeric',
+ })}
{props.message.file ? (
-
-
- {
- if (!props.message.file) return
- setCurrentSrc(props.message.file.toString())
- setModal(true)
- }}
- onLoadingComplete={() => setLoaded(true)}
- style={{
- objectFit: 'contain',
- height: '100%',
- width: '100%',
- borderRadius: '13px',
- cursor: 'pointer',
- opacity: loaded ? '100%' : '0%',
- minWidth: '100px',
- minHeight: '100px',
- }}
- width={500}
- height={500}
- src={props.message.file.toString()}
- alt={'К сожалению, изображение не загрузилось'}
- />
- {!loaded && (
-
+
+ {
+ if (!props.message.file)
+ return
+ setModal(true)
+ }}
+ onLoadingComplete={() =>
+ setLoaded(true)
+ }
+ style={{
+ objectFit: 'contain',
height: '100%',
+ width: '100%',
+ borderRadius: '13px',
+ cursor: 'pointer',
+ opacity: loaded
+ ? '100%'
+ : '0%',
+ minWidth: '100px',
+ minHeight: '100px',
}}
- variant='rectangular'
+ width={500}
+ height={500}
+ src={
+ props.message.file
+ .toString()
+ .split('?type')[0]
+ }
+ alt={
+ 'К сожалению, изображение не загрузилось'
+ }
/>
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
-
-
-
- Копировать
-
-
+
+
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30
+ ? 'pre-wrap'
+ : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
+
+ ) : (
+
+
+ {
+ if (props.message.file) {
+ window.open(
+ props.message
+ .file as string,
+ '_blank'
+ )
+ }
+ }}
+ className='smallScroll'
+ sx={{
+ overflowY: 'scroll',
+ position: 'relative',
+ padding: '15px 23px',
+
+ border: `1px solid ${
+ theme === 'dark'
+ ? '#303035'
+ : '#EFF0F2'
+ }`,
+
+ boxShadow: 'none',
+ fontSize: '15px',
+ marginTop: 0.5,
+ textAlign: 'left',
+ fontFamily:
+ 'Raleway,sans-serif',
+ borderRadius: '13px',
+ '& p': {
+ color: 'inherit',
+ fontStyle: 'inherit',
+ },
+ ...flexStyle,
+ }}
+ >
+
- Удалить
-
-
+
+
+
+
-
- {!props.message.is_sent && (
-
+
+
+
+
+
+
+
+
+
+
+
+
- )}
-
- 30
- ? 'pre-wrap'
- : 'pre',
- }}
- >
- {/**/}
- {props.message.content}
-
+ >
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30
+ ? 'pre-wrap'
+ : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
-
+ )
) : (
{
if (props.deleteMessage)
- props.deleteMessage(props.message.uid)
+ props.deleteMessage(
+ props.message.uid
+ )
}}
>
30 ? 'pre-wrap' : 'pre',
+ props.message.content.length > 30
+ ? 'pre-wrap'
+ : 'pre',
}}
>
{/**/}
@@ -533,7 +915,7 @@ export const UserMessage = React.memo(function UserMessage({ setModal, setCurren
)}
-
+
) : (
void, type: string, device: Device) {
+ const { data } = useSession()
+
+ const [messages, setMessages] = useState([])
+
+ const [loading, setLoading] = useState(false)
+
+ const [offset, setOffset] = useState(0)
+
+ const dispatch = useAppDispatch()
+
+ async function onFetch() {
+ if (!data) return
+
+ setLoading(true)
+ console.log(offset)
+ const { data: answer, ...response } = await getImagesGalery(data.access, offset)
+ setLoading(false)
+
+ if (response.status >= 400) return showError('Ошибка загрузки чата')
+
+ if (!Array.isArray(answer)) return showError('Ошибка загрузки чата')
+
+ if (device === 'desktop') {
+ setMessages(answer)
+ return setOffset(answer.length)
+ }
+
+ setMessages(answer.reverse())
+ setOffset(answer.length)
+ }
+
+ useEffect(() => {
+ onFetch()
+ }, [data, type])
+
+ const getMessagesPagination = async (device: 'mobile' | 'desktop') => {
+ if (!data) return
+
+ setLoading(true)
+ const { data: answer, ...response } = await getImagesGalery(data.access, offset)
+ setLoading(false)
+
+ if (response.status >= 400) return showError('Ошибка загрузки чата')
+
+ if (!Array.isArray(answer)) return showError('Ошибка загрузки сообщений')
+
+ if (device === 'mobile') {
+ const newMessages = answer.reverse()
+ setMessages([...newMessages, ...messages])
+ return setOffset((prev) => prev + answer.length)
+ }
+
+ setMessages([...messages, ...answer])
+ setOffset((prev) => prev + answer.length)
+ }
+
+ return { messages, loading, getMessagesPagination }
+}
@@ -1,4 +1,4 @@
-import { Message } from '#/shared/lib/types/model'
+import { Message } from '#/entities/message'
import { useMemo, useState } from 'react'
export const useMessages = (messages: Message[]) => {
@@ -7,13 +7,14 @@ export const useMessages = (messages: Message[]) => {
const [loaded, setLoaded] = useState(false)
// типизация ну супер кривая)))
- const computedLibraryImages = useMemo(() => {
+ const computedLibraryImages = useMemo[]>(() => {
return messages
.map((el) => {
if (el.file && (el.file as any).includes('.zip')) return null
- return el
+ if (/\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg/.test(el.file as string)) return el
+ return null
})
- .filter((el) => el !== null) as Message[]
+ .filter((el) => el !== null) as Message[]
}, [messages])
return {
@@ -0,0 +1,843 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material'
+import Stack from '@mui/material/Stack'
+import Image from 'next/image'
+
+import { useAppSelector } from '#/app/store/store'
+import { TooltipCustom } from '#/shared'
+import { Message } from '#/shared/lib/types/model'
+import { BotMessage } from '#/widgets/messages'
+import { Markdown } from '../../markdown/markdown'
+import { formatDate } from '#/shared/lib/helpers'
+
+interface IMessagesList {
+ // messageResponse: Message[] | null
+ // mode?: string
+ // sendMessage?: (title: string) => void
+ // getMessagesPagination?: () => Promise
+
+ device: 'mobile' | 'desktop'
+ modelType: string
+ isNewMessage: boolean
+ setCurrentSrc: (value: string) => void
+ message: Message
+ setModal: (value: boolean) => void
+ deleteMessage?: (message_uid: string) => void
+ modelTitle: string | undefined
+ setResendValue: (value: string) => void
+ onLoadImage?: (event: React.ChangeEvent | null, file?: File) => void
+}
+
+export const UserMessage = React.memo(function UserMessage({ setCurrentSrc, setModal, ...props }: IMessagesList) {
+ const [anchorEl, setAnchorEl] = React.useState(null)
+ const open = Boolean(anchorEl)
+ const desktop = props.device === 'desktop'
+ const [loaded, setLoaded] = useState(false)
+
+ const copy = (text: string) => {
+ navigator.clipboard.writeText(text)
+ }
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget)
+ }
+ const handleClose = () => {
+ setAnchorEl(null)
+ }
+
+ const fileName = useMemo(() => {
+ if (!props.message.file) return 'Подбираем имя файла...'
+ const matches = (props.message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/)
+ if (!matches) return 'Подбираем имя файла...'
+ return `${matches[1]}.${matches[2]}`
+ }, [])
+
+ const resend = async () => {
+ props.setResendValue(props.message.content)
+ if (props.message.file !== null) {
+ fetch(props.message.file as string, {
+ method: 'GET',
+ headers: {},
+ }).then((response) => {
+ response.arrayBuffer().then(function (buffer) {
+ const url = new Blob([buffer])
+ const binaryFile = new File([url], 'image.png')
+ if (props.onLoadImage) {
+ props.onLoadImage(null, binaryFile)
+ }
+ })
+ })
+ }
+ }
+
+ useEffect(() => {
+ console.log(props.message.file)
+ }, [props.message.file])
+
+ const flexStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'start',
+ gap: '12px',
+ color: '#7f7df3 !important',
+ fontWeight: '600 !important',
+ lineHeight: '140%',
+ letterSpacing: '0.2px',
+ cursor: 'pointer',
+ }
+
+ const theme = useAppSelector((state) => state.theme.theme)
+
+ return (
+ <>
+
+ {!props.message.from_model ? (
+ //
+
+
+
+
+ {formatDate(props.message.created_at, {
+ hour: 'numeric',
+ minute: 'numeric',
+ })}
+
+
+
+
+ {props.message.file ? (
+ /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg/.test(
+ props.message.file as string
+ ) ? (
+
+
+ {
+ if (!props.message.file) return
+ setCurrentSrc(props.message.file.toString())
+ setModal(true)
+ }}
+ onLoadingComplete={() => setLoaded(true)}
+ style={{
+ objectFit: 'contain',
+ height: '100%',
+ width: '100%',
+ borderRadius: '13px',
+ cursor: 'pointer',
+ opacity: loaded ? '100%' : '0%',
+ minWidth: '100px',
+ minHeight: '100px',
+ }}
+ width={500}
+ height={500}
+ src={props.message.file.toString().split('?type')[0]}
+ alt={'К сожалению, изображение не загрузилось'}
+ />
+ {!loaded && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30
+ ? 'pre-wrap'
+ : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
+
+ ) : (
+
+
+ {
+ if (props.message.file) {
+ window.open(
+ props.message.file as string,
+ '_blank'
+ )
+ }
+ }}
+ className='smallScroll'
+ sx={{
+ overflowY: 'scroll',
+ position: 'relative',
+ padding: '15px 23px',
+
+ border: `1px solid ${
+ theme === 'dark' ? '#303035' : '#EFF0F2'
+ }`,
+
+ boxShadow: 'none',
+ fontSize: '15px',
+ marginTop: 0.5,
+ textAlign: 'left',
+ fontFamily: 'Raleway,sans-serif',
+ borderRadius: '13px',
+ '& p': {
+ color: 'inherit',
+ fontStyle: 'inherit',
+ },
+ ...flexStyle,
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30
+ ? 'pre-wrap'
+ : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
+
+ )
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30 ? 'pre-wrap' : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
+ )}
+
+
+
+ ) : (
+
+ )}
+
+ >
+ )
+})
@@ -0,0 +1,23 @@
+import React from 'react'
+import { Box } from '@mui/material'
+import Link from 'next/link'
+
+import { Message } from '#/shared/lib/types/model'
+
+export function AnswerWrap(props: { message: Message }) {
+ if (props.message.file) {
+ const messageText = props.message.from_model
+ ? 'Ваша ссылка для скачивания \n (нажмите для просмотра)'
+ : 'Тут ссылка на ваш файл (нажмите для просмотра)'
+
+ return (
+
+ e.stopPropagation()} target='_blank' href={props.message.file.toString()}>
+ {messageText}
+
+
+ )
+ }
+
+ return <>{props.message.content}>
+}
@@ -6,6 +6,7 @@ import Image from 'next/image'
import { useAppSelector } from '#/app/store/store'
import { TooltipCustom } from '#/shared'
import { Markdown } from '#/widgets/markdown/markdown'
+import { formatDate } from '#/shared/lib/helpers'
export function BotMessage(props: any) {
// const LazyCode = dynamic(() => import('src/widgets/chat-gpt-field/ui/code'))
@@ -42,20 +43,19 @@ export function BotMessage(props: any) {
}
}, [props])
- const flexStyle =
- props.message.file && props.modelType !== 'chatgpt'
- ? {
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'start',
- gap: '12px',
- color: '#7f7df3 !important',
- fontWeight: '600 !important',
- lineHeight: '140%',
- letterSpacing: '0.2px',
- cursor: 'pointer',
- }
- : {}
+ const flexStyle = props.message.file
+ ? {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'start',
+ gap: '12px',
+ color: '#7f7df3 !important',
+ fontWeight: '600 !important',
+ lineHeight: '140%',
+ letterSpacing: '0.2px',
+ cursor: 'pointer',
+ }
+ : {}
return (
//
@@ -104,7 +104,10 @@ export function BotMessage(props: any) {
marginRight: 4,
}}
>
- {props.message.created_at.slice(10, 16).replace('T', ' ')}
+ {formatDate(props.message.created_at, {
+ hour: 'numeric',
+ minute: 'numeric',
+ })}
@@ -135,7 +138,7 @@ export function BotMessage(props: any) {
...flexStyle,
}}
>
- {props.message.file && props.modelType !== 'chatgpt' ? (
+ {props.message.file ? (
+
= memo(
const onlyImageMessage = useMemo(() => {
if (!messageResponse) return []
- console.log(messageResponse)
return messageResponse
- .map((item) => item)
- .filter((item) => item.file !== null && !(item.file as string).includes('.zip')) as Message[]
+ .map((el) => {
+ if (el.file && (el.file as any).includes('.zip')) return null
+ if (/\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg/.test(el.file as string)) {
+ return { ...el, file: el.file?.toString().split('?type')[0]! }
+ }
+ return null
+ })
+ .filter((el) => el !== null) as Message[]
}, [messageResponse])
React.useEffect(() => {
@@ -88,7 +93,7 @@ export const ChatMessagesList: React.FC = memo(
}, [messageResponse])
const handleScroll = () => {
- setScrollBottom(
+ setScrollBottom(
paginationScroll.current?.scrollHeight -
paginationScroll.current?.scrollTop -
paginationScroll.current?.clientHeight
@@ -144,7 +149,7 @@ export const ChatMessagesList: React.FC = memo(
width: 'fit-content',
cursor: 'pointer',
margin: '0 auto',
- bottom: '100px',
+ bottom: '150px',
zIndex: 10,
}}
onClick={() => {
@@ -1,3 +1,15 @@
+.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;
@@ -6,13 +6,13 @@ import Link from 'next/link'
import { useAppSelector } from '#/app/store/store'
import { ClientOnly, Success, TooltipCustom, useShowData } from '#/shared'
-import { Message } from '#/shared/lib/types/model'
import styles from './image-messages-list.module.scss'
import { createPortal } from 'react-dom'
import { useMessages } from '../model/use-messages'
import { ImageIcons } from './image-icons'
import { ImageModal } from '#/features/image-modal'
+import { Message } from '#/entities/message'
interface MessagesList {
device: 'mobile' | 'desktop'
@@ -1,2 +1,8 @@
+export * from './image-messages-list'
+export * from './chat-messages-list'
+export * from './answer-wrap'
+export * from './bot-message'
+export * from './preview-view'
+export * from './user-message'
+export * from './is-next-day'
export * from './image-icons'
-export * from './chat-messages-list'
\ No newline at end of file
@@ -2,9 +2,9 @@ import React, { memo, useMemo } from 'react'
import { Box, Typography } from '@mui/material'
import { Message } from '#/shared/lib/types/model'
-import { getDateFromString } from '#/widgets/messages/lib/getDateFromString'
-import { getDayMontsString } from '#/widgets/messages/lib/getDayMontsString'
-import { UserMessage } from '#/widgets/messages/message-components/user-message'
+import { getDateFromString } from '#/widgets/messages/lib/date-from-string'
+import { getDayMontsString } from '#/widgets/messages/lib/day-months-string'
+import { UserMessage } from '#/widgets/messages'
interface IProps {
message: Message
@@ -12,9 +12,9 @@ interface IProps {
messageResponse: Message[] | null
modelTitle: string | undefined
deleteMessage?: (message_uid: string) => void
- setCurrentSrc: (value: string) => void
+ setCurrentSrc: (value: string) => void
isNewMessage: boolean
- setModal: (value: boolean) => void
+ setModal: (value: boolean) => void
modelType: string
device: 'mobile' | 'desktop'
setResendValue: (value: string) => void
@@ -29,8 +29,8 @@ export const IsNextDay = memo(
messageResponse,
modelTitle,
deleteMessage,
- setCurrentSrc,
- setModal,
+ setCurrentSrc,
+ setModal,
isNewMessage,
modelType,
device,
@@ -65,9 +65,9 @@ export const IsNextDay = memo(
setResendValue={setResendValue}
modelTitle={modelTitle}
deleteMessage={deleteMessage}
- setCurrentSrc={setCurrentSrc}
+ setCurrentSrc={setCurrentSrc}
key={message.uid}
- setModal={setModal}
+ setModal={setModal}
message={message}
isNewMessage={isNewMessage}
modelType={modelType}
@@ -97,11 +97,11 @@ export const IsNextDay = memo(
+ Не знаете, с чего начать?
+
+ Попробуйте, например, вот так:
+
+
+ {previewMessages.map((el) => {
+ return (
+ props.setValue(el)}
+ sx={{
+ cursor: 'pointer',
+ padding: '15px',
+ color: '#8280FF',
+ width: 'fit-content',
+ marginTop: '15px',
+ backgroundColor: 'rgba(130, 128, 255, 0.10)',
+ borderRadius: '12px',
+ }}
+ >
+ {el}
+
+ )
+ })}
+
+
+ )
+}
@@ -0,0 +1,839 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material'
+import Stack from '@mui/material/Stack'
+import Image from 'next/image'
+
+import { useAppSelector } from '#/app/store/store'
+import { TooltipCustom } from '#/shared'
+import { Message } from '#/shared/lib/types/model'
+import { BotMessage } from '#/widgets/messages'
+import { Markdown } from '../../markdown/markdown'
+import { formatDate } from '#/shared/lib/helpers'
+
+interface IMessagesList {
+ // messageResponse: Message[] | null
+ // mode?: string
+ // sendMessage?: (title: string) => void
+ // getMessagesPagination?: () => Promise
+
+ device: 'mobile' | 'desktop'
+ modelType: string
+ isNewMessage: boolean
+ setCurrentSrc: (value: string) => void
+ message: Message
+ setModal: (value: boolean) => void
+ deleteMessage?: (message_uid: string) => void
+ modelTitle: string | undefined
+ setResendValue: (value: string) => void
+ onLoadImage?: (event: React.ChangeEvent | null, file?: File) => void
+}
+
+export const UserMessage = React.memo(function UserMessage({ setCurrentSrc, setModal, ...props }: IMessagesList) {
+ const [anchorEl, setAnchorEl] = React.useState(null)
+ const open = Boolean(anchorEl)
+ const desktop = props.device === 'desktop'
+ const [loaded, setLoaded] = useState(false)
+
+ const copy = (text: string) => {
+ navigator.clipboard.writeText(text)
+ }
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget)
+ }
+ const handleClose = () => {
+ setAnchorEl(null)
+ }
+
+ const fileName = useMemo(() => {
+ if (!props.message.file) return 'Подбираем имя файла...'
+ const matches = (props.message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/)
+ if (!matches) return 'Подбираем имя файла...'
+ return `${matches[1]}.${matches[2]}`
+ }, [])
+
+ const resend = async () => {
+ props.setResendValue(props.message.content)
+ if (props.message.file !== null) {
+ fetch(props.message.file as string, {
+ method: 'GET',
+ headers: {},
+ }).then((response) => {
+ response.arrayBuffer().then(function (buffer) {
+ const url = new Blob([buffer])
+ const binaryFile = new File([url], 'image.png')
+ if (props.onLoadImage) {
+ props.onLoadImage(null, binaryFile)
+ }
+ })
+ })
+ }
+ }
+
+ const flexStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'start',
+ gap: '12px',
+ color: '#7f7df3 !important',
+ fontWeight: '600 !important',
+ lineHeight: '140%',
+ letterSpacing: '0.2px',
+ cursor: 'pointer',
+ }
+
+ const theme = useAppSelector((state) => state.theme.theme)
+
+ return (
+ <>
+
+ {!props.message.from_model ? (
+ //
+
+
+
+
+ {formatDate(props.message.created_at, {
+ hour: 'numeric',
+ minute: 'numeric',
+ })}
+
+
+
+
+ {props.message.file ? (
+ /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg|\.wav/.test(
+ props.message.file as string
+ ) ? (
+
+
+ {
+ if (!props.message.file) return
+ setCurrentSrc(props.message.file.toString().split('?type')[0])
+ setModal(true)
+ }}
+ onLoadingComplete={() => setLoaded(true)}
+ style={{
+ objectFit: 'contain',
+ height: '100%',
+ width: '100%',
+ borderRadius: '13px',
+ cursor: 'pointer',
+ opacity: loaded ? '100%' : '0%',
+ minWidth: '100px',
+ minHeight: '100px',
+ }}
+ width={500}
+ height={500}
+ src={props.message.file.toString().split('?type')[0]}
+ alt={'К сожалению, изображение не загрузилось'}
+ />
+ {!loaded && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30
+ ? 'pre-wrap'
+ : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
+
+ ) : (
+
+
+ {
+ if (props.message.file) {
+ window.open(
+ props.message.file as string,
+ '_blank'
+ )
+ }
+ }}
+ className='smallScroll'
+ sx={{
+ overflowY: 'scroll',
+ position: 'relative',
+ padding: '15px 23px',
+
+ border: `1px solid ${
+ theme === 'dark' ? '#303035' : '#EFF0F2'
+ }`,
+
+ boxShadow: 'none',
+ fontSize: '15px',
+ marginTop: 0.5,
+ textAlign: 'left',
+ fontFamily: 'Raleway,sans-serif',
+ borderRadius: '13px',
+ '& p': {
+ color: 'inherit',
+ fontStyle: 'inherit',
+ },
+ ...flexStyle,
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30
+ ? 'pre-wrap'
+ : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
+
+ )
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!props.message.is_sent && (
+
+ )}
+
+ 30 ? 'pre-wrap' : 'pre',
+ }}
+ >
+ {/**/}
+ {props.message.content}
+
+
+ )}
+
+
+
+ ) : (
+
+ )}
+
+ >
+ )
+})
@@ -1,2 +1,4 @@
export * from './ui'
+export * from './lib'
+export * from './api'
export * from './model'
\ No newline at end of file
@@ -1,12 +1,14 @@
export interface NavigationSearchModelLink {
title: string
slug: string
+ alternative_titles: string[]
}
export interface NavigationSearchLink {
label: string
+ alternative_labels?: string[]
url: string
category: string
external?: boolean
- uid?: string
+ uid?: string
}
@@ -1,35 +1,35 @@
-import { NavigationSearchLink } from '../api/types'
+import { NavigationSearchLink } from "../api/types";
export const staticLinks: NavigationSearchLink[] = [
{
label: 'Дашборд',
url: '/',
- category: 'Навигация',
+ category: 'Навигация'
},
{
label: 'Оплата',
url: '/account?scope=subscribe',
- category: 'Навигация',
+ category: 'Навигация',
},
{
label: 'Настройки',
url: '/account?scope=setting',
- category: 'Навигация',
+ category: 'Навигация',
},
{
label: 'Настройки корп. аккаунта',
url: '/account?scope=business',
- category: 'Навигация',
+ category: 'Навигация',
},
{
label: 'API-ключи',
url: '/api-keys',
- category: 'Навигация',
+ category: 'Навигация',
},
{
label: 'Реквизиты',
url: 'https://air.fail/requisites',
- category: 'Навигация',
+ category: 'Навигация',
external: true,
},
]
@@ -1,2 +1,2 @@
export * from './use-model'
-export * from './use-chat-links'
+export * from './use-chat-links'
\ No newline at end of file
@@ -10,7 +10,7 @@ export const useNavigationSearchChatLinks = () => {
const { data } = useSession()
const fetchChatLinks = async () => {
- if (!data) return
+ if(!data) return
setChatLinks((await getModelChatLinks(data.access)).data)
}
@@ -21,6 +21,7 @@ export const useNavigationSearchChatLinks = () => {
label: el.title,
url: 'chat-bot/' + el.slug,
category: 'Чат-боты',
+ alternative_labels: el.alternative_titles
})),
[chatLinks]
)
@@ -21,6 +21,7 @@ export const useNavigationSearchMediaLinks = () => {
label: el.title,
url: 'images/' + el.slug,
category: 'Изображения',
+ alternative_labels: el.alternative_titles
})),
[mediaLinks]
)
@@ -14,9 +14,20 @@ export const useNavigationSearchModel = (links: NavigationSearchLink[]) => {
const filteredLinks = useMemo(() => {
if (!search) return links
- return links.filter((el) => el.label.toLowerCase().includes(search.toLowerCase()))
+ return links.filter(
+ (el) =>
+ el.label.toLowerCase().includes(search.toLowerCase()) ||
+ (el.alternative_labels &&
+ el.alternative_labels.some((el) =>
+ el.toLowerCase().includes(search.toLowerCase())
+ ))
+ )
}, [search, links])
+ useEffect(() => {
+ console.log(filteredLinks)
+ }, [filteredLinks])
+
// hooks
const ctrlF = (e: KeyboardEvent) => {
if (!searchRef.current) {
@@ -3,20 +3,26 @@ import React from 'react'
import styles from './key-for-search.module.scss'
-export const KeyForSearch = () => {
+interface KeyForSearchProps {
+ userAgent: string | null
+}
+
+export const KeyForSearch = ({ userAgent }: KeyForSearchProps) => {
+ if (userAgent === null) {
+ return null
+ }
+
const checkType = () => {
- if (navigator.userAgent && !navigator.userAgent.includes('Windows')) {
- return '⌘+F'
- } else {
+ if (userAgent.includes('Windows')) {
return 'Ctrl + F'
+ } else {
+ return '⌘+F'
}
}
return (
-
- {checkType()}
-
+ {checkType()}
)
}
@@ -27,10 +27,6 @@ export const Search = ({ device }: SearchProps) => {
const links = useConcat(visibleChatLinks, visibleMediaLinks, staticLinks)
- useEffect(() => {
- console.log(links)
- }, [links])
-
const { search, setSearch, searchOpen, setSearchOpen, searchRef, filteredLinks } = useNavigationSearchModel(links)
useEffect(() => {
@@ -47,7 +43,7 @@ export const Search = ({ device }: SearchProps) => {
sx={{ width: '457px' }}
noOptionsText={'Не найдено'}
options={filteredLinks}
- // filterOptions={filter}
+ filterOptions={(x) => x}
groupBy={(link) => link.category}
renderGroup={(params) => (
@@ -132,7 +128,7 @@ export const Search = ({ device }: SearchProps) => {
}}
/>
),
- endAdornment: ,
+ endAdornment: ,
}}
/>
)}
@@ -1,2 +1,2 @@
export * from './ui'
-export * from './config'
+export * from './config'
\ No newline at end of file
@@ -1,6 +1,15 @@
import React, { useEffect, useMemo, useState } from 'react'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
-import { Accordion, AccordionDetails, AccordionSummary, Box, Button, MenuItem, Stack, Typography } from '@mui/material'
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Button,
+ MenuItem,
+ Stack,
+ Typography,
+} from '@mui/material'
import Router, { useRouter } from 'next/router'
import { useSession } from 'next-auth/react'
@@ -18,6 +27,8 @@ export interface IOffer {
tokens_per_plan: string
title: string
duration: string
+ accessed_models: string[]
+ points: string[]
}
interface IPaymentsProps {
@@ -69,51 +80,49 @@ export const Payment: React.FC = ({ device, changeClose })
{offers?.map((offer) => {
return (
setSelectedOffer(uid)}
- uid={offer.uid}
- price={offer.price}
- tokens_per_plan={offer.tokens_per_plan}
key={offer.uid}
/>
)
})}
- {messages && (
-
-
- История
- {messages.map((el) => (
-
- ))}
-
-
- )}
+ {/* {messages && (
+
+
+ История
+ {messages.map((el) => (
+
+ ))}
+
+
+ )} */}
@@ -121,7 +130,10 @@ export const Payment: React.FC = ({ device, changeClose })
{info.map((el) => {
return (
-
+
p': {
@@ -0,0 +1,98 @@
+.tarif {
+ background-color: var(--new-ui-main-color);
+ padding: 30px;
+ border-radius: 15px;
+ width: 100%;
+ display: flex;
+ text-align: left;
+ flex-direction: column;
+ position: relative;
+ &__btn{
+ transition: all 0.8s ease-in-out !important;
+ }
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+
+ visibility: hidden;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 2px solid var(--air-color);
+ border-radius: 15px;
+ transition: all 0.6s ease-in-out;
+ opacity: 0;
+ }
+ &:hover {
+ &::before {
+ opacity: 1;
+ visibility: visible;
+ }
+ .tarif__btn {
+ background-color: var(--air-color);
+ color: white;
+ border: unset;
+ }
+ }
+ &__header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 45px;
+ position: relative;
+ z-index: 1;
+ }
+
+ &__badge {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ width: max-content;
+ span {
+ font-size: 36px;
+ font-weight: 700;
+ }
+ }
+
+ &__quote {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--new-ui-gray-color);
+ }
+
+ &__price {
+ font-weight: 700;
+ font-size: 25px;
+ letter-spacing: -2%;
+ margin-bottom: 8px;
+ position: relative;
+ z-index: 1;
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding-left: 20px;
+ gap: 4px;
+ margin-bottom: 45px;
+ position: relative;
+ z-index: 1;
+ }
+
+ &__btn {
+ border-color: var(--air-color);
+ color: var(--air-color);
+ position: relative;
+ z-index: 1;
+ }
+}
+
+.point {
+ color: var(--new-ui-text-color);
+ text-align: left;
+ &::marker {
+ color: var(--new-ui-gray-color) !important;
+ }
+}
@@ -1,11 +1,11 @@
import React from 'react'
-import { Box, Typography } from '@mui/material'
-import Button from '@mui/material/Button'
-import { Balance } from '#/shared'
-import { useThemeAndDevice } from '#/shared/lib/hooks'
import { IOffer } from '#/widgets/payment/model/payment'
-import styles from '#/widgets/payment/ui/payment.module.scss'
+import styles from './offer.module.scss'
+import { CommonBagde } from '#/shared/ui/badge'
+import LightningSvg from '#/assets/svg/lightning.svg?react'
+import { CommonButton } from '#/shared/ui/button'
+import { c, commaSeparated } from '#/shared/lib/helpers'
interface IOfferProps extends IOffer {
pickOffer: (uid: string) => void
@@ -14,42 +14,34 @@ interface IOfferProps extends IOffer {
pay: (uid: string) => void
}
-const duration = {
- month: 'мес',
- year: 'год',
-}
+const Offer: React.FC = ({ pickOffer, uid, tokens_per_plan, price, points, pay }) => {
-const Offer: React.FC = ({
- pickOffer,
- uid,
- tokens_per_plan,
- price,
- selectedOffer,
- title,
- duration: dur,
- pay,
-}) => {
return (
- pickOffer(uid)}>
- {title}
-
-
- {Math.floor(+price)} ₽
-
-
-
-
- -
- {Math.floor(+tokens_per_plan)} токенов
+
pickOffer(uid)}>
+
+
+
+
+ {commaSeparated(Math.round(Number(tokens_per_plan)))}
+
+
+
токенов
+
+
+
+ {points.map((el, idx) => (
+ -
+ {el}
-
-
-
-
-
-
+ ))}
+
+
+
{commaSeparated(parseInt(price) ?? 0)}₽
+
+
pay(uid)} variant={'outline'}>
+ Оплатить
+
+
)
}
@@ -43,30 +43,9 @@
}
}
.messages {
- margin-left: 10px;
- width: 18%;
- padding-top: 10px;
@media (max-width: 768px) {
margin-top: 15px;
margin-left: 0;
width: 100%;
}
}
-.tarif {
- position: relative;
- width: 326px;
- height: 380px;
- margin-right: 20px;
- background-color: var(--new-ui-main-color);
- border-radius: 15px;
- text-align: center;
- border: 2px solid rgba(130, 128, 255, 0.4);
- padding: 30px;
- @media (max-width: 768px) {
- width: 94vw;
-
- height: 300px;
- margin: 0;
- margin-top: 10px;
- }
-}
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/nextjs';
+
+export async function register() {
+
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
+ await import('../sentry.server.config');
+ }
+}
@@ -0,0 +1,68 @@
+import { NextResponse } from 'next/server'
+import { getToken } from 'next-auth/jwt'
+
+// export async function middleware(req) {
+// const res = NextResponse.next()
+
+// const token = await getToken({ req })
+
+// if (!token) {
+// const url = req.nextUrl.clone()
+// url.pathname = '/login'
+// const response = NextResponse.redirect(url)
+
+// req.url
+// .split('?')[1]
+// ?.split('&')
+// .forEach((el) => {
+// const [key, value] = el.split('=')
+// response.cookies.set(key, value)
+// })
+
+// return response
+// }
+
+// return res
+// }
+
+const SECRET = new TextEncoder().encode(process.env.NEXTAUTH_SECRET)
+
+export async function middleware(req) {
+ const token = await getToken({ req })
+ if (!token) {
+ return redirectToLogin(req)
+ }
+
+ return NextResponse.next()
+}
+
+function redirectToLogin(req) {
+ const url = req.nextUrl.clone()
+ url.pathname = '/login'
+ const response = NextResponse.redirect(url)
+
+ // Убедитесь, что query-параметры правильно передаются
+ const queryParams = req.nextUrl.searchParams
+ queryParams.forEach((value, key) => {
+ response.cookies.set(key, value)
+ })
+
+ return response
+}
+
+export const config = {
+ matcher: [
+ '/',
+ '/account',
+ '/admin',
+ '/service-keys',
+ '/chat-bot/:path*',
+ '/chat-bot/:path*',
+ '/dalle',
+ '/kandinsky',
+ '/stable-diffusion',
+ '/midjourney',
+ '/whisper',
+ '/business/',
+ ],
+}
@@ -6,6 +6,7 @@
"quotes": ["warn", "single"],
"indent": "off",
"jsx-quotes": ["warn", "prefer-single"],
+ "react/display-name": "off",
"simple-import-sort/imports": [
"warn",
{
@@ -25,7 +26,6 @@
]
}
],
- "simple-import-sort/exports": "warn",
- "react/display-name": "warn"
+ "simple-import-sort/exports": "warn"
}
}
@@ -38,3 +38,6 @@ next-env.d.ts
/.idea
+
+# Sentry Config File
+.env.sentry-build-plugin
@@ -1,84 +1,86 @@
stages:
- - Build
- - Deploy
+ - Build
+ - Deploy
default:
- image: docker:rc-cli
- before_script:
- - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
+ image: docker:latest
+ before_script:
+ - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
build_staging:
- stage: Build
- script:
- - cp $ENV .env.production
- - docker compose --env-file $ENV build
- - docker compose push
- environment:
- name: staging
- url: https://staging.air.fail
- services:
- - docker:dind
- only:
- - staging
+ stage: Build
+ image: docker:cli
+ script:
+ - cp $ENV .env.production
+ - docker compose --env-file $ENV build
+ - docker compose push
+ environment:
+ name: staging
+ url: https://staging.air.fail
+ services:
+ - docker:dind
+ only:
+ - staging
deploy_staging:
- stage: Deploy
- variables:
- DOCKER_HOST: tcp://$STAGING_CLUSTER_HOST:2376
- DOCKER_TLS_VERIFY: 1
- DOCKER_CERT_PATH: '/certs'
- services:
- - docker:dind
- environment:
- name: staging
- url: https://staging.air.fail
- only:
- - staging
- before_script:
- - cp $ENV .env.production
- - mkdir -p $DOCKER_CERT_PATH
- - echo "$STAGING_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem
- - echo "$STAGING_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem
- - echo "$STAGING_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem
- - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
- script:
- - docker compose pull
- - docker compose --env-file $ENV --project-name air-frontend up -d
+ stage: Deploy
+ image: docker:cli
+ variables:
+ DOCKER_HOST: tcp://$STAGING_CLUSTER_HOST:2376
+ DOCKER_TLS_VERIFY: 1
+ DOCKER_CERT_PATH: "/certs"
+ services:
+ - docker:dind
+ environment:
+ name: staging
+ url: https://staging.air.fail
+ only:
+ - staging
+ before_script:
+ - cp $ENV .env.production
+ - mkdir -p $DOCKER_CERT_PATH
+ - echo "$STAGING_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem
+ - echo "$STAGING_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem
+ - echo "$STAGING_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem
+ - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
+ script:
+ - docker compose pull
+ - docker compose --env-file $ENV --project-name air-ui-web up -d
build_production:
- stage: Build
- script:
- - cp $ENV .env.production
- - docker compose --env-file $ENV build
- - docker compose push
- environment:
- name: production
- url: https://app.air.fail
- services:
- - docker:dind
- only:
- - main
+ stage: Build
+ script:
+ - cp $ENV .env.production
+ - docker compose -f stack.yml --env-file $ENV build
+ - docker compose -f stack.yml push
+ environment:
+ name: production
+ url: https://app.air.fail
+ services:
+ - docker:dind
+ only:
+ - main
deploy_production:
- stage: Deploy
- variables:
- DOCKER_HOST: tcp://$PRODUCTION_CLUSTER_HOST:2376
- DOCKER_TLS_VERIFY: 1
- DOCKER_CERT_PATH: '/certs'
- services:
- - docker:dind
- environment:
- name: production
- url: https://app.air.fail
- only:
- - main
- before_script:
- - cp $ENV .env.production
- - mkdir -p $DOCKER_CERT_PATH
- - echo "$PRODUCTION_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem
- - echo "$PRODUCTION_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem
- - echo "$PRODUCTION_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem
- - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
- script:
- - docker compose pull
- - docker compose --env-file $ENV --project-name air-frontend up -d
+ stage: Deploy
+ image: docker:cli
+ variables:
+ DOCKER_HOST: tcp://$PRODUCTION_CLUSTER_HOST:2376
+ DOCKER_TLS_VERIFY: 1
+ DOCKER_CERT_PATH: "/certs"
+ services:
+ - docker:dind
+ environment:
+ name: production
+ url: https://app.air.fail
+ only:
+ - main
+ before_script:
+ - cp $ENV .env.production
+ - mkdir -p $DOCKER_CERT_PATH
+ - echo "$PRODUCTION_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem
+ - echo "$PRODUCTION_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem
+ - echo "$PRODUCTION_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem
+ - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
+ script:
+ - docker stack deploy --prune --with-registry-auth --resolve-image=always --compose-file stack.yml --detach frontend
\ No newline at end of file
@@ -1,10 +1,10 @@
{
- "useTabs": true,
- "tabWidth": 5,
- "singleQuote": true,
- "trailingComma": "es5",
- "jsxBracketSameLine": false,
- "semi": false,
- "printWidth": 120,
- "jsxSingleQuote": true
+ "useTabs": true,
+ "tabWidth": 5,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "jsxBracketSameLine": false,
+ "semi": false,
+ "printWidth": 100,
+ "jsxSingleQuote": true
}
@@ -2,7 +2,7 @@ FROM node:alpine as dependencies
WORKDIR /app
COPY package.json ./package.json
-RUN npm install
+RUN --mount=type=cache,target=/root/.npm npm install
FROM node:alpine as builder
@@ -1,17 +1,17 @@
services:
- app:
- build:
- context: .
- dockerfile: Dockerfile.dev
- container_name: frontend
- restart: unless-stopped
- volumes:
- - .:/app
- ports:
- - '3000:3000'
- env_file:
- - .env
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile.dev
+ container_name: frontend
+ restart: unless-stopped
+ volumes:
+ - .:/app
+ ports:
+ - '3000:3000'
+ env_file:
+ - .env
networks:
- default:
- name: 'air'
+ default:
+ name: 'air'
@@ -1,17 +1,27 @@
services:
- app:
- image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- build:
- context: .
- dockerfile: Dockerfile
- container_name: frontend
- restart: unless-stopped
- ports:
- - '3000:3000'
- env_file:
- - .env.production
+ app:
+ image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
+ build:
+ context: .
+ dockerfile: Dockerfile
+ restart: unless-stopped
+ networks:
+ - infrastructure
+ env_file:
+ - .env.production
+ labels:
+ - traefik.enable=true
+ - traefik.docker.network=infrastructure
+ - traefik.http.routers.frontend.rule=Host(`$DOMAIN`)
+ - traefik.http.routers.frontend.entrypoints=web,websecure
+ - traefik.http.routers.frontend.tls=true
+ - traefik.http.routers.frontend.tls.certresolver=defaultresolver
+ - traefik.http.routers.frontend.service=frontend
+ - traefik.http.services.frontend.loadbalancer.server.port=3000
+ - traefik.http.middlewares.frontend.redirectscheme.scheme=https
+ - traefik.http.middlewares.frontend.redirectscheme.permanent=true
networks:
- default:
- name: 'air'
- external: true
+ infrastructure:
+ name: infrastructure
+ external: true
@@ -32,13 +32,8 @@ const nextConfig = {
reactStrictMode: false,
images: {
domains: [
- '195.93.252.114',
'app.air.fail',
- 'air_django',
- '79.133.181.83',
- 'images.air.fail',
'storage.yandexcloud.net',
- 'dev.air.fail',
's3-alpha-sig.figma.com',
'localhost',
],
@@ -50,3 +45,21 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
})
module.exports = withBundleAnalyzer(nextConfig)
+
+
+// Injected content via Sentry wizard below
+
+const { withSentryConfig } = require("@sentry/nextjs");
+
+module.exports = withSentryConfig(
+ module.exports,
+ {
+ org: process.env.NEXT_PUBLIC_SENTRY_ORGANIZATION || '',
+ sentryUrl: `${process.env.NEXT_PUBLIC_SENTRY_DOMAIN ? process.env.NEXT_PUBLIC_SENTRY_DOMAIN + '/' : ''}`,
+ widenClientFileUpload: true,
+ tunnelRoute: '/monitoring',
+ disableLogger: true,
+ project: process.env.NEXT_PUBLIC_SENTRY_PROJECT || '',
+ authToken: process.env.NEXT_PUBLIC_SENTRY_AUTH_TOKEN || ''
+ }
+);
@@ -0,0 +1,15 @@
+// This file configures the initialization of Sentry on the client.
+// The config you add here will be used whenever a users loads a page in their browser.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '',
+
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+});
@@ -1,11 +0,0 @@
-import * as Sentry from '@sentry/nextjs'
-
-Sentry.init({
- dsn: 'https://977e4e013d7844c49c1c642cca0dff8f@o4505483299979264.ingest.sentry.io/4505550927691776',
- integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()],
- // Performance Monitoring
- tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
- // Session Replay
- replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
- replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
-})
@@ -0,0 +1,16 @@
+// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
+// The config you add here will be used whenever one of the edge features is loaded.
+// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '',
+
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+});
@@ -0,0 +1,15 @@
+// This file configures the initialization of Sentry on the server.
+// The config you add here will be used whenever the server handles a request.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '',
+
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+});
@@ -0,0 +1,47 @@
+services:
+ app:
+ image: $CI_REGISTRY_IMAGE:latest
+ build:
+ context: .
+ dockerfile: Dockerfile
+ tags:
+ - "$CI_REGISTRY_IMAGE:latest"
+ - "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
+ cache_from:
+ - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true
+ cache_to:
+ - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true
+ networks:
+ - infrastructure
+ deploy:
+ replicas: 2
+ update_config:
+ parallelism: 1
+ delay: 10s
+ order: start-first
+ restart_policy:
+ condition: on-failure
+ delay: 5s
+ max_attempts: 3
+ window: 30s
+ placement:
+ constraints:
+ - node.role == worker
+ labels:
+ - traefik.enable=true
+ - traefik.docker.network=infrastructure
+ - traefik.http.routers.frontend.rule=Host(`$DOMAIN`)
+ - traefik.http.routers.frontend.entrypoints=web,websecure
+ - traefik.http.routers.frontend.tls=true
+ - traefik.http.routers.frontend.tls.certresolver=defaultresolver
+ - traefik.http.routers.frontend.service=frontend
+ - traefik.http.services.frontend.loadbalancer.server.port=3000
+ - traefik.http.middlewares.frontend.redirectscheme.scheme=https
+ - traefik.http.middlewares.frontend.redirectscheme.permanent=true
+ env_file:
+ - .env.production
+
+networks:
+ infrastructure:
+ name: infrastructure
+ external: true
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "es5",
+ "target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -18,7 +18,7 @@
"noImplicitThis": false,
"paths": {
"#/*": ["./src/*"],
- "@/components/*": ["components/*"]
+ "#/components/*": ["components/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
@@ -1,9 +1,9 @@
{
- "static_path": ".next",
- "app_id": 51767823,
- "endpoints": {
- "mobile": "/",
- "mvk": "/",
- "web": "/"
- }
-}
+ "static_path": ".next",
+ "app_id": 51767823,
+ "endpoints": {
+ "mobile": "/",
+ "mvk": "/",
+ "web": "/"
+ }
+}
\ No newline at end of file