@@ -1,3 +1,11 @@ +## Precondition + + + +## Steps to reproduce + + + ## Expected Result @@ -10,10 +18,9 @@ Прикрепляйте видео/скриншоты тут --> +## Notes -## Steps to reproduce - - + *** @@ -0,0 +1,40 @@ +#!/usr/bin/env sh +# +# If you create/switch to a branch without a prefix (no '/'), +# automatically rename it to "feature/". +# +# Allowed prefixes (when '/' is present): +# fix/, feature/, release/, chore/ +# + +set -eu + +IS_BRANCH_CHECKOUT="${3:-0}" +if [ "$IS_BRANCH_CHECKOUT" != "1" ]; then + exit 0 +fi + +BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" +if [ -z "$BRANCH" ]; then + exit 0 +fi + +case "$BRANCH" in + fix/*|feature/*|release/*|chore/*) + exit 0 + ;; +esac + +# No prefix -> default to feature/ +if ! printf "%s" "$BRANCH" | grep -q '/'; then + NEW_BRANCH="feature/$BRANCH" + if git show-ref --verify --quiet "refs/heads/$NEW_BRANCH"; then + exit 0 + fi + git branch -m "$BRANCH" "$NEW_BRANCH" 2>/dev/null || true + exit 0 +fi + +# Has a prefix but it's not allowed: cannot reliably fix here (checkout already happened). +exit 0 + @@ -0,0 +1,35 @@ +#!/usr/bin/env sh +# +# Enforce allowed branch prefixes: +# fix/, feature/, release/, chore/ +# +# If branch has no prefix (no '/'), automatically rename to feature/. +# + +set -eu + +BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" +if [ -z "$BRANCH" ]; then + exit 0 +fi + +case "$BRANCH" in + fix/*|feature/*|release/*|chore/*) + exit 0 + ;; +esac + +# No slash => default to feature/ +if ! printf "%s" "$BRANCH" | grep -q '/'; then + NEW_BRANCH="feature/$BRANCH" + if ! git show-ref --verify --quiet "refs/heads/$NEW_BRANCH"; then + git branch -m "$BRANCH" "$NEW_BRANCH" + fi + exit 0 +fi + +echo >&2 "Ошибка: недопустимый префикс ветки '$BRANCH'." +echo >&2 "Разрешены только: fix/, feature/, release/, chore/." +echo >&2 "Либо используйте ветку без префикса — она будет автоматически переименована в feature/<имя>." +exit 1 + @@ -0,0 +1,65 @@ +#!/usr/bin/env sh +# +# Auto-prefix commit messages based on branch name: +# fix/..., feature/..., release/..., chore/... +# -> "refs #: " +# + +set -eu + +COMMIT_MSG_FILE="${1:-}" +COMMIT_SOURCE="${2:-}" + +if [ -z "$COMMIT_MSG_FILE" ] || [ ! -f "$COMMIT_MSG_FILE" ]; then + exit 0 +fi + +# Skip for merges/squashes/amends/templates (avoid fighting Git-generated messages). +case "$COMMIT_SOURCE" in + merge|squash|commit|template) + exit 0 + ;; +esac + +BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" +TASK_NUMBER="" +if printf "%s" "$BRANCH" | grep -Eq '^(fix|feature|release|chore)/[0-9]+'; then + TASK_NUMBER="$(printf "%s" "$BRANCH" | sed -E 's#^(fix|feature|release|chore)/([0-9]+).*#\2#')" +else + exit 0 +fi + +FIRST_LINE="$(LC_ALL=C sed -n '1p' "$COMMIT_MSG_FILE" || true)" + +# Do not touch special commit subjects. +case "$FIRST_LINE" in + "Merge "*|"fixup! "*|"squash! "*|"revert! "*) + exit 0 + ;; +esac + +PREFIX="refs #${TASK_NUMBER}: " + +# Already has the right prefix (or already references the number at start) -> no-op. +case "$FIRST_LINE" in + "$PREFIX"*|"refs #${TASK_NUMBER} "*|"refs #${TASK_NUMBER}:"*) + exit 0 + ;; +esac + +# If message already starts with some refs prefix, don't double-prefix. +case "$FIRST_LINE" in + "refs #"*|"refs: #"* ) + exit 0 + ;; +esac + +TMP_FILE="${COMMIT_MSG_FILE}.tmp.$$" +{ + printf "%s%s\n" "$PREFIX" "$FIRST_LINE" + LC_ALL=C sed -n '2,$p' "$COMMIT_MSG_FILE" +} > "$TMP_FILE" + +mv "$TMP_FILE" "$COMMIT_MSG_FILE" +exit 0 + Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Black.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-BlackItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Bold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-BoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraLight.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ExtraLightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Italic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Light.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-LightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Medium.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-MediumItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Regular.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-SemiBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-SemiBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-Thin.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_18pt-ThinItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Black.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-BlackItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Bold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-BoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraLight.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ExtraLightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Italic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Light.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-LightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Medium.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-MediumItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Regular.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-SemiBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-SemiBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-Thin.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_24pt-ThinItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Black.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-BlackItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Bold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-BoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraLight.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ExtraLightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Italic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Light.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-LightItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Medium.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-MediumItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Regular.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-SemiBold.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-SemiBoldItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-Thin.ttf differ Binary files /dev/null and b/public/fonts/Inter/static/Inter_28pt-ThinItalic.ttf differ Binary files /dev/null and b/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf differ Binary files /dev/null and b/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf differ Binary files a/public/fonts/Raleway/static/Raleway-Black.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-BlackItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Bold.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-BoldItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraBold.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraBoldItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraLight.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ExtraLightItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Italic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Light.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-LightItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Medium.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-MediumItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Regular.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-SemiBold.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-SemiBoldItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-Thin.ttf and /dev/null differ Binary files a/public/fonts/Raleway/static/Raleway-ThinItalic.ttf and /dev/null differ Binary files a/public/fonts/Raleway/Raleway-Italic-VariableFont_wght.ttf and /dev/null differ Binary files a/public/fonts/Raleway/Raleway-VariableFont_wght.ttf and /dev/null differ Binary files a/public/voice-clone/add-voice.png and b/public/voice-clone/add-voice.png differ Binary files a/public/voice-clone/my-generations.png and b/public/voice-clone/my-generations.png differ Binary files a/public/voice-clone/step-1.png and b/public/voice-clone/step-1.png differ @@ -19,10 +19,8 @@ export const CheckboxFilter = ({ name, value, filters, item_key, description, se const [check, setCheck] = React.useState(value || false) React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== check) { - setCheck(filters[item_key]) - } - }, [filters]) + setCheck(filters[item_key] ?? value) + }, [filters, item_key, value]) return ( @@ -20,10 +20,8 @@ export const SelectFilter = ({ selects, filters, item_key, name, description, se const [value, setValue] = React.useState(selects.default) React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== value) { - setValue(filters[item_key]) - } - }, [filters]) + setValue(filters[item_key] ?? selects.default) + }, [filters, item_key, selects.default]) const onChange = (e: SelectChangeEvent) => { setNewParam({ [item_key]: e.target.value }) @@ -38,14 +36,13 @@ export const SelectFilter = ({ selects, filters, item_key, name, description, se IconComponent={KeyboardArrowDownIcon} value={value} onChange={onChange} - inputProps={{ - MenuProps: { - MenuListProps: { - sx: { - color: '#A6A5A5', - backgroundColor: '#151518', - }, - }, + MenuProps={{ + transitionDuration: 0, + PaperProps: { + sx: { backgroundColor: '#151518', borderRadius: '8px', marginTop: '8px' }, + }, + MenuListProps: { + sx: { backgroundColor: '#151518', color: '#A6A5A5' }, }, }} style={{ @@ -27,10 +27,8 @@ export const Slide = ({ title, values, filters, item_key, description, setNewPar } React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== range) { - setRange(filters[item_key]) - } - }, [filters]) + setRange(filters[item_key] ?? values.default) + }, [filters, item_key, values.default]) return ( <> @@ -28,14 +28,13 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul IconComponent={KeyboardArrowDownIcon} value={value} onChange={onChange} - inputProps={{ - MenuProps: { - MenuListProps: { - sx: { - color: '#A6A5A5', - backgroundColor: '#151518', - }, - }, + MenuProps={{ + transitionDuration: 0, + PaperProps: { + sx: { backgroundColor: '#151518', borderRadius: '8px', marginTop: '8px' }, + }, + MenuListProps: { + sx: { backgroundColor: '#151518', color: '#A6A5A5' }, }, }} style={{ @@ -4,7 +4,6 @@ import { configureStore } from '@reduxjs/toolkit' import { paramsStore } from '#/app/store/model-parametres-store' import { balanceSlice } from '#/entities/balance' import { userSlice } from '#/entities/user-account' -import { settingsSlice } from '#/entities/user-account/model/settings' import { chatsReducer } from '#/features/chats/chats-slice' import { pendingSlice } from '#/features/pending' import { stepperSlice } from '#/features/register-business' @@ -19,7 +18,6 @@ export const store = configureStore({ user: userSlice.reducer, notification: notificationSlice.reducer, params: paramsStore.reducer, - settings: settingsSlice.reducer, copy: copySlice.reducer, loading: pendingSlice.reducer, chats: chatsReducer, @@ -27,19 +27,19 @@ } @font-face { - font-family: 'Raleway'; + font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; - src: url('/fonts/Raleway/Raleway-VariableFont_wght.ttf') format('truetype'); + src: url('/fonts/Inter/Inter-VariableFont_opsz,wght.ttf') format('truetype'); } @font-face { - font-family: 'Raleway'; + font-family: 'Inter'; font-style: italic; font-weight: 100 900; font-display: swap; - src: url('/fonts/Raleway/Raleway-Italic-VariableFont_wght.ttf') format('truetype'); + src: url('/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf') format('truetype'); } * { @@ -49,7 +49,7 @@ } html { - font-family: 'Raleway', sans-serif; + font-family: 'Inter', sans-serif; } body { @@ -449,17 +449,6 @@ button { font: inherit; color: inherit; background-color: transparent; - text-align: left; - cursor: pointer; -} - -button { - padding: 0; - border: none; - font: inherit; - color: inherit; - background-color: transparent; - text-align: left; cursor: pointer; } @@ -1,17 +0,0 @@ -import { api } from '#/shared/api' - -import { IUserSetting } from '../model/types' - -export const addUserSettings = async ( - token: string, - setting: Omit -): Promise => { - try { - const { data } = await api.post('/api/users/settings/', setting, { - headers: { Authorization: `Bearer ${token}` }, - }) - return data - } catch (err) { - return null - } -} @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios' import { getApiUrl } from '#/shared/lib/constants' -import { AccountType } from '../model/types' +import { AccountType } from '../model/account.types' export const getAccountType = async (token: string | null | undefined): Promise => { if (!token) { return 'regular' @@ -1,14 +0,0 @@ -import { api } from '#/shared/api' - -import { IUserSetting } from '../model/types' - -export const getUserSettings = async (token: string): Promise => { - try { - const { data } = await api.get('/api/users/settings/', { - headers: { Authorization: `Bearer ${token}` }, - }) - return data - } catch (err) { - return [] - } -} @@ -1 +0,0 @@ -export * from './settings.routes' \ No newline at end of file @@ -1,15 +0,0 @@ -import { api } from '#/shared/api' -import { Agent } from 'https' -import { IUserSetting } from '../model/types' - -export function getUserSettings() { - return api.get('/api/users/settings/') -} - -export function postUserSettings(option: Omit) { - return api.post('/api/users/settings/', option) -} - -export function updateUserSettings(id: string, value: any) { - return api.put(`/api/users/settings/${id}`, { value }) -} @@ -1,20 +0,0 @@ -import { api } from '#/shared/api' - -import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' -import { IUserSetting, SettingValueType } from '../model/types' - -export const updateUserSettings = async ( - token: string, - id: string, - value: SettingValueType, - settings: IUserSetting[] -) => { - try { - await api.put(`/api/users/settings/${id}`, { value }, { - headers: { Authorization: `Bearer ${token}` }, - }) - return getUpdatedSettingsLocal({ settings, id, value }) - } catch (err) { - return null - } -} @@ -1,23 +0,0 @@ -import { Device } from '#/shared/lib/types/entities' - -import { IUserSetting, SettingType } from '../../model/types' - -interface IProps { - settings: Omit | null - targetType: SettingType - targetDevice: Device -} - -export function isSettingExist({ settings, targetDevice, targetType }: IProps): IUserSetting | null { - if (!settings || settings.length === 0) { - return null - } - - for (let i = 0; i < settings.length; i++) { - if (settings[i].type === targetType && settings[i].device === targetDevice) { - return settings[i] - } - } - - return null -} @@ -1,22 +0,0 @@ -import { IUserSetting, SettingValueType } from '../../model/types' - -interface IProps { - settings: IUserSetting[] - id: string - value: SettingValueType -} - -export function getUpdatedSettingsLocal({ settings, id, value }: IProps): IUserSetting[] { - if (settings.length === 0) { - return settings - } - - return settings.map((setting) => - setting.id === id - ? { - ...setting, - value, - } - : setting - ) -} @@ -0,0 +1,2 @@ +export type AccountType = 'regular' | 'business_host' | 'business_account' + @@ -1,2 +1 @@ -export * from './settings-context' -export * from './use-global-settings' \ No newline at end of file +export {} \ No newline at end of file @@ -1,9 +0,0 @@ -import { createUseContext } from '#/shared' -import { createContext } from 'react' -import { useGlobalSettings } from './use-global-settings' - -export const UserSettingsContext = createContext | null>(null) - -export const useUserSettingsContext = createUseContext(UserSettingsContext) - -export const UserSettingsContextProvider = UserSettingsContext.Provider @@ -1,93 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' - -import { addUserSettings } from '../api/add-user-settings' -import { getUserSettings } from '../api/get-user-settings' -import { updateUserSettings } from '../api/update-user-settings' -import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' - -import { IUserSetting, SettingValueType } from './types' -import { useLocalStorageSave } from '#/shared/lib/helpers/local-storage-helper' - -interface IAddSettings { - token: string | undefined | null - setting: Omit -} - -export const getUserAccountSettings = createAsyncThunk( - 'users/getSettings', - async (token: string | undefined | null) => { - if (!token) { - return initialState.state - } - return await getUserSettings(token) - } -) - -export const addUserAccountSettings = createAsyncThunk( - 'users/addSettings', - async ({ token, setting }: IAddSettings) => { - if (!token) { - return - } - return await addUserSettings(token, setting) - } -) - -export const updateUserAccountSettings = createAsyncThunk( - 'users/updateSettings', - async ({ - token, - id, - value, - settings, - }: { - token: string | undefined | null - id: string - value: SettingValueType - settings: IUserSetting[] | null - }) => { - if (!token) { - return initialState.state - } - if (!settings) { - return initialState.state - } - return await updateUserSettings(token, id, value, settings) - } -) - -const initialState: { state: IUserSetting[] | null } = { - state: null, -} - -export const settingsSlice = createSlice({ - name: 'balance', - initialState, - reducers: { - setSettings: (state, action) => { - state.state = action.payload - }, - }, - extraReducers: (builder) => { - builder.addCase(getUserAccountSettings.fulfilled, (state, action) => { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - }) - builder.addCase(addUserAccountSettings.fulfilled, (state, action) => { - if (state.state === null) state.state = [] - if (action.payload) { - state.state = [...state.state, action.payload] - localStorage.setItem('global_settings', JSON.stringify([...state.state, action.payload])) - } - }) - - builder.addCase(updateUserAccountSettings.fulfilled, (state, action) => { - if (action.payload) { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - } - }) - }, -}) - -export const { setSettings } = settingsSlice.actions @@ -1,16 +0,0 @@ -import { Device } from '#/shared/lib/types/entities' - -export type AccountType = 'regular' | 'business_host' | 'business_account' - -export type SettingType = 'sidemenu' | string - -export type SettingValueType = { - sidemenu_state?: 'opened' | 'closed' -} - -export interface IUserSetting { - id: string - device: Device - type: SettingType - value: SettingValueType | any -} @@ -1,66 +0,0 @@ -import { useLocalStorage } from 'usehooks-ts' -import { IUserSetting } from './types' -import { makePrivateRequest } from '#/shared/api' -import { getUserSettings, postUserSettings, updateUserSettings } from '../api' - -import { getDeviceType } from '#/shared' -import { useCallback, useEffect, useRef, useState } from 'react' -import { useSession } from 'next-auth/react' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' - -export function useGlobalSettings() { - const [settings, setSettings] = useLocalStorage('global_settings', []) - - const { showMessage } = useShowDataStore() - - const device = getDeviceType() - - const { data } = useSession() - - const addSettings = makePrivateRequest(async (type: string, value: any) => { - const { status, data } = await postUserSettings({ device, type, value }) - - if (status !== 200) return showMessage('Ошибка создания настроек') - - setSettings((s) => [...s, data]) - }) - - const updateSettings = useCallback( - makePrivateRequest(async (type: string, value: any) => { - const option = settings.find((x) => x.type === type) - - if (!option) return showMessage('Ошибка присвоения настроек') - - const { status } = await updateUserSettings(option.id, value) - - if (status !== 204) return showMessage('Ошибка создания настроек') - - setSettings((s) => [...s.filter((x) => x.type !== type), { ...option, value }]) - }), - [settings, data] - ) - - const fetchUserSettings = makePrivateRequest(async () => { - const { status, data } = await getUserSettings() - - const device = getDeviceType() - - if (status !== 200) return showMessage('Ошибка загрузки данных пользователя') - - setSettings(data.filter((s) => s.device === device)) - }) - - const getOptionValue = (type: string, initial: any) => { - const option = settings.find((o) => o.type === type) - return option ? option.value : initial - } - - return { - settings, - setSettings, - addSettings, - fetchUserSettings, - updateSettings, - getOptionValue, - } -} @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { Box, TextField, Typography } from '@mui/material' import axios from 'axios' -import { Dayjs } from 'dayjs' +import dayjs, { Dayjs } from 'dayjs' import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' @@ -38,6 +38,11 @@ export const ApiKeyModal = ({ const keyName = `Ключ ${Math.floor(Math.random() * 100000) + 1}` + if (endDate && dayjs(endDate).isBefore(dayjs(), 'day')) { + showMessage('Указана неверная дата') + return + } + setIsLoading(true) const response = endDate && endDate !== '' @@ -0,0 +1,55 @@ +import { IModelParams } from '#/shared/api/models/models' + +export type ModelParamValues = Record + +export function getAllowedParamKeys(params: IModelParams[], currentVersion: string): Set { + return new Set( + params + .filter((el) => { + if (el.versions.length === 0) { + return true + } + + if (!currentVersion) { + return true + } + + return el.versions.includes(currentVersion) + }) + .map((el) => el.key) + ) +} + +export function pruneModelParams( + includeParams: ModelParamValues, + params: IModelParams[], + currentVersion: string +): ModelParamValues | null { + const allowedKeys = getAllowedParamKeys(params, currentVersion) + + if (allowedKeys.size === 0) { + return Object.keys(includeParams).length !== 0 ? {} : null + } + + const filteredEntries = Object.entries(includeParams).filter(([key]) => allowedKeys.has(key)) + + if (filteredEntries.length === Object.keys(includeParams).length) { + return null + } + + return filteredEntries.length ? Object.fromEntries(filteredEntries) : {} +} + +export function buildDefaultParamsForVersion(parameters: IModelParams[], version: string): ModelParamValues { + if (!version) { + return parameters.reduce((acc, param) => ({ ...acc, [param.key]: param.values.default }), {}) + } + + return parameters.reduce((acc, param) => { + if (param.versions.length === 0 || param.versions.includes(version)) { + return { ...acc, [param.key]: param.values.default } + } + + return acc + }, {} as ModelParamValues) +} @@ -0,0 +1,27 @@ +import React from 'react' + +import { replaceParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { IModelParams } from '#/shared/api/models/models' + +import { pruneModelParams } from '../lib/prune-model-params' + +export function usePruneModelParams(params: IModelParams[] | undefined, currentVersion: string) { + const includeParams = useAppSelector((state) => state.params.params) as Record< + string, + string | number | number[] | boolean + > + const dispatch = useAppDispatch() + + React.useEffect(() => { + if (!params?.length) { + return + } + + const nextPayload = pruneModelParams(includeParams, params, currentVersion) + + if (nextPayload !== null) { + dispatch(replaceParams(nextPayload)) + } + }, [currentVersion, dispatch, includeParams, params]) +} @@ -1,126 +1,94 @@ -import React from 'react' -import { Stack } from '@mui/material' - -import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' -import { InputFilter } from '#/app/components/filters/input_filter' -import { SelectFilter } from '#/app/components/filters/select_filter' -import { Slide } from '#/app/components/filters/slide_filter' -import { replaceParams, setParams } from '#/app/store/model-parametres-store' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { IModelParams } from '#/shared/api/models/models' - -interface IProps { - params: IModelParams[] - currentVersion: string -} -export default function BotParamsMap({ params, currentVersion }: IProps) { - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useAppDispatch() - - const setNewParam = React.useCallback( - (payload: { [key: string]: string | number | number[] | boolean }) => { - dispatch(setParams(payload)) - }, - [dispatch] - ) - - React.useEffect(() => { - if (!params) { - return - } - - const allowedKeys = new Set( - params - .filter((el) => { - if (el.versions.length === 0) { - return true - } - - // Согласовано с chat-bot filteredParams: без выбранной версии считаем все параметры допустимыми - if (!currentVersion) { - return true - } - - return el.versions.includes(currentVersion) - }) - .map((el) => el.key) - ) - - if (allowedKeys.size === 0) { - if (Object.keys(includeParams).length !== 0) { - dispatch(replaceParams({})) - } - return - } - - const filteredEntries = Object.entries(includeParams).filter(([key]) => allowedKeys.has(key)) - - if (filteredEntries.length !== Object.keys(includeParams).length) { - const nextPayload = filteredEntries.length ? Object.fromEntries(filteredEntries) : {} - dispatch(replaceParams(nextPayload)) - } - }, [currentVersion, dispatch, includeParams, params, setNewParam]) - - return ( - - {params.map((item, idx) => { - if (item.versions.length === 0 || item.versions.includes(currentVersion)) { - if (item.type == 'floatrange' || item.type == 'intrange') { - return ( - - ) - } - if (item.type == 'bool') { - return ( - - ) - } - - if (item.type == 'list') { - return ( - - ) - } - - if (item.type == 'int' || item.type == 'str') { - return ( - - ) - } - } - })} - - ) -} +import React from 'react' +import { Stack } from '@mui/material' + +import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' +import { InputFilter } from '#/app/components/filters/input_filter' +import { SelectFilter } from '#/app/components/filters/select_filter' +import { Slide } from '#/app/components/filters/slide_filter' +import { setParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { IModelParams } from '#/shared/api/models/models' + +import { usePruneModelParams } from './model/use-prune-model-params' + +interface IProps { + params: IModelParams[] + currentVersion: string +} +export default function BotParamsMap({ params, currentVersion }: IProps) { + const includeParams = useAppSelector((state) => state.params.params) + const dispatch = useAppDispatch() + + usePruneModelParams(params, currentVersion) + + const setNewParam = React.useCallback( + (payload: { [key: string]: string | number | number[] | boolean }) => { + dispatch(setParams(payload)) + }, + [dispatch] + ) + + return ( + + {params.map((item, idx) => { + if (item.versions.length === 0 || item.versions.includes(currentVersion)) { + if (item.type == 'floatrange' || item.type == 'intrange') { + return ( + + ) + } + if (item.type == 'bool') { + return ( + + ) + } + + if (item.type == 'list') { + return ( + + ) + } + + if (item.type == 'int' || item.type == 'str') { + return ( + + ) + } + } + })} + + ) +} + \ No newline at end of file @@ -4,8 +4,7 @@ interface SendReport { errorMessage: string email: string file?: File - company?:string -} +} export async function sendReport({errorMessage, email, file}: SendReport) { let formData = new FormData() @@ -16,15 +15,3 @@ export async function sendReport({errorMessage, email, file}: SendReport) { } return await api.post('/reports/', formData) } - - -export async function sendBusinessReport({errorMessage, email, company, file}: SendReport) { - let formData = new FormData() - - formData.append('report_text', `${email}: ${errorMessage}`) - formData.append('company_name', `${company}`) - if (file) { - formData.append('images', file) - } - return await api.post('/reports/business-support/', formData) -} @@ -5,11 +5,11 @@ import { useAppSelector } from '#/app/store/store' import { makePrivateRequest } from '#/shared/api' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -import { sendBusinessReport, sendReport } from '../api' +import { sendReport } from '../api' const MAX_FILE_SIZE = 4.5 * 1024 * 1024 // 4.5 MB -export const useErrorReport = (reportType:'business' | 'default' = 'default') => { +export const useErrorReport = () => { const email = useAppSelector((state) => state.user.email) const [file, setFile] = React.useState() @@ -34,7 +34,7 @@ export const useErrorReport = (reportType:'business' | 'default' = 'default') => } } - const sendError = makePrivateRequest(async (company) => { + const sendError = makePrivateRequest(async () => { if (errorMessage.trim() === '') { showMessage('Поле не должно быть пустым.') setError(true) @@ -42,10 +42,7 @@ export const useErrorReport = (reportType:'business' | 'default' = 'default') => return } - const response = await (reportType === 'business' - ? sendBusinessReport({ errorMessage, email, company, file }) - : sendReport({ errorMessage, email, file }) - ) + const response = await sendReport({ errorMessage, email, file }) if (response.status === 201) { setErrorMessage('') @@ -1,120 +0,0 @@ -import * as React from 'react' -import { useForm } from 'react-hook-form' -import { Typography } from '@mui/material' - -import { useAppSelector } from '#/app/store/store' -import AttachSvg from '#/assets/svg/attach.svg?react' -import PlusSvg from '#/assets/svg/plus.svg?react' -import { BUSINESS_ERROR_REPORT, getModalById, PlateTemplate } from '#/features/modals' -import { CompanyAutocomplete } from '#/features/register-business/api/company-autocomplete' -import { companyNameOptions } from '#/features/register-business/lib/constatnts-step-legal-informative' -import { c } from '#/shared' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -import { CommonButton } from '#/shared/ui/button' -import { CommonTextArea } from '#/shared/ui/common-textarea' - -import { useErrorReport } from '../model/use-error-report' - -import styles from './error-report.module.scss' - -export const BusinessErrorReportPlate = () => { - const modal = getModalById(BUSINESS_ERROR_REPORT) - const { error, isSend, setErrorMessage, handleFileChange, file, setFile, sendError } = useErrorReport('business') - const { showMessage } = useShowDataStore() - - const { companyName } = useAppSelector((state) => state.stepper.dataForCreate) - - const methods = useForm({ - defaultValues: { - companyName - }, - }) - - const handleSubmit = () => { - const formValues = methods.getValues() - const company = formValues.companyName?.trim() - - if (!company) { - showMessage('Пожалуйста, укажите наименование организации') - return - } - - sendError(company) - } - - return ( - - {isSend ? ( -
-

Спасибо!

-

Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации

-
- ) : ( -
e.stopPropagation()}> -

Запрос для компаний и партнеров

- - Опишите ваш сценарий — подберем решение и ответим за день. Работаем для корпораций, агентств и интеграторов - - - - - setErrorMessage(evt.target.value)} - buttomSlot={ - <> - - - } - /> - {file && ( -
-
- -
- {file.name} - {(file.size / 1024).toFixed(1)} KB -
-
- - -
- )} - -
- modal.setState(false)}> - Отмена - - - Отправить - -
-
- )} -
- ) -} \ No newline at end of file @@ -1,5 +1,4 @@ -import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' -import { ImageWithState } from './types' +import { Dispatch, SetStateAction, useEffect, useMemo } from 'react' import { Message } from '#/entities/message' export const useImagesLibrary = ( @@ -7,8 +6,6 @@ export const useImagesLibrary = ( current: string | null, images: Message[] ) => { - const [initialCount, setInitialCount] = useState(0) - const esc = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() @@ -18,7 +15,7 @@ export const useImagesLibrary = ( const currentIndex = useMemo( () => images.findIndex((item) => item.file === current), - [current] + [current, images] ) useEffect(() => { @@ -27,8 +24,6 @@ export const useImagesLibrary = ( }, []) return { - initialCount, - setInitialCount, - currentIndex, + currentIndex, } } @@ -1,7 +1,5 @@ -import { debounce } from 'lodash' -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { Swiper as SwiperCore } from 'swiper' -import { ImageWithState } from './types' export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefined, reverse: boolean) => { const [swiper, setSwiper] = useState(null) @@ -41,10 +39,6 @@ export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefin } }, [swiper]) - // useEffect(() => { - // swiper?.slideTo(currentImageIndex || 0) - // }, [currentImageIndex]) - return { swiper, setSwiper, @@ -0,0 +1,21 @@ +.overlay { + position: fixed; + z-index: 1201; + inset: 0; + width: 100%; + height: 100%; + display: none; +} + +.overlayVisible { + display: block; +} + +.backdrop { + position: absolute; + z-index: 101; + width: 100%; + height: 100%; + background-color: #000000; + opacity: 0.9; +} @@ -1,16 +1,15 @@ -import React, { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react' -import Image from 'next/image' +import { Dispatch, SetStateAction, useEffect, useRef } from 'react' +import overlayStyles from './full-screen-modal-overlay.module.scss' import styles from './modal-styles.module.scss' import { ArrowDropDown } from '@mui/icons-material' -import { c, Loader, TooltipCustom } from '#/shared' +import { c } from '#/shared' import { useImagesLibrary } from '../model' import { Swiper, SwiperSlide } from 'swiper/react' import 'swiper/css' import { useLibrarySwiper } from '../model/use-swiper' -import { ImageIcons, useImageIcons } from '#/widgets/messages' -import { Typography } from '@mui/material' +import { useImageIcons } from '#/widgets/messages' import { ModalImage } from './modal-image' import { Message } from '#/entities/message' @@ -32,55 +31,63 @@ export default function FullScreenModal({ onSlideFalse, reverse = false, }: IProps) { - const { initialCount, setInitialCount, currentIndex } = useImagesLibrary( + const { currentIndex } = useImagesLibrary( setModal, current, images ) - const count = useRef(0) + const prevImagesLengthRef = useRef(0) const { swiper, setSwiper, slideNext, slidePrev } = useLibrarySwiper(onSlideFalse, reverse) const { downloadFile } = useImageIcons() + useEffect(() => { - // debugger - if (reverse) - setTimeout(() => { - swiper?.slideNext() - count.current = images.length - }, 400) - else - setTimeout(() => { - swiper?.slideTo(images.length - count.current - 1) - count.current = images.length - }, 400) - }, [images]) + if (!modal) { + prevImagesLengthRef.current = 0 + return + } + if (!swiper) return + + const len = images.length + const prevLen = prevImagesLengthRef.current + + if (prevLen === 0) { + prevImagesLengthRef.current = len + return + } + + if (len === prevLen) return + + if (len < prevLen) { + prevImagesLengthRef.current = len + return + } + + const delta = len - prevLen + + const id = window.setTimeout(() => { + if (reverse) { + swiper.update() + } else { + const nextIndex = swiper.activeIndex + delta + const clamped = Math.max(0, Math.min(nextIndex, len - 1)) + swiper.slideTo(clamped, 0) + } + prevImagesLengthRef.current = len + }, 0) + + return () => clearTimeout(id) + }, [modal, images.length, reverse, swiper]) return (
setModal(false)} > -
+
= ({ successLo utm_source: cookie.utm_source, utm_medium: cookie.utm_medium, utm_campaign: cookie.utm_campaign, - utm_term: cookie.utm_campaign, - utm_content: cookie.utm_campaign, + utm_term: cookie.utm_term, + utm_content: cookie.utm_content, } if (localStorage.getItem('referral')) { @@ -3,9 +3,7 @@ import { signIn } from 'next-auth/react' import Image from 'next/image' import React, { useState } from 'react' -interface YandexAuthButtonProps {} - -export const YandexAuthButton = ({}: YandexAuthButtonProps) => { +export const YandexAuthButton = ({ children = 'Войти с Яндекс ID' }: { children?: React.ReactNode }) => { const [hovered, setHovered] = useState(false) return ( @@ -41,7 +39,7 @@ export const YandexAuthButton = ({}: YandexAuthButtonProps) => { }} > - Войти с Яндекс ID + {children} ) } @@ -64,6 +64,23 @@ export const authOptions: NextAuthOptions = { return (await data) as User }, }), + CredentialsProvider({ + id: 'tokens', + type: 'credentials', + credentials: {}, + async authorize(credentials) { + const { access, refresh } = credentials as { access: string; refresh: string } + + if (!access || !refresh) { + throw new Error('invalid_credentials') + } + + return { + id: 'tokens', + token: { access, refresh }, + } as User + }, + }), ], callbacks: { async jwt({ token, user, account, trigger }) { @@ -1,6 +0,0 @@ -import BusinessReportsMobile from '#/views/reports-mobile/ui/business-error-reports-mobile' -import { getDefaultLayout } from '#/widgets/layouts' - -BusinessReportsMobile.getLayout = getDefaultLayout({ titlePage: 'Компаниям', device: 'mobile' }) - -export default BusinessReportsMobile @@ -1,3 +1,26 @@ +export const getApiDetail = (error: unknown): string => { + const data = (error as { response?: { data?: { detail?: unknown; message?: string } } })?.response?.data + if (!data) return '' + + if (typeof data.message === 'string') return data.message + + const detail = data.detail + if (typeof detail === 'string') return detail + if (Array.isArray(detail)) { + return detail + .map((item) => + typeof item === 'string' + ? item + : typeof item === 'object' && item !== null && 'msg' in item + ? String((item as { msg: unknown }).msg) + : String(item) + ) + .join(', ') + } + + return '' +} + export const decodeError = (err: any, defaultErr: string) => { if (err.response?.data.detail?.includes('Token balance')) { return 'Не хватает ' + err.response.data.detail.split(' ').filter(Boolean).at(-2) + ' токена' @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers' +import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import dayjs, { Dayjs } from 'dayjs' @@ -28,7 +28,8 @@ export const DateInput = ({ return ( - - + = ({ className, maxWidth, stopClickPropagation = true, + wrapperWidth, fullWidthTrigger = false, tapToOpenOnMobile = false, }) => { @@ -43,9 +45,10 @@ export const TooltipCustom: React.FC = ({ const tapMode = Boolean(tapToOpenOnMobile && isTouchPrimary) const [tapOpen, setTapOpen] = useState(false) - const triggerSpanStyle: React.CSSProperties | undefined = fullWidthTrigger - ? { display: 'flex', width: '100%', minWidth: 0 } - : undefined + const triggerSpanStyle: React.CSSProperties | undefined = { + ...(fullWidthTrigger ? { display: 'flex', width: '100%', minWidth: 0 } : {}), + ...(wrapperWidth ? { display: 'block', width: wrapperWidth, maxWidth: '100%', minWidth: 0 } : {}), + } const tooltip = ( { alignItems='flex-start' spacing={2} sx={{ - width: desktop ? '90%' : '100%', + width: desktop ? (scope === 'referral' ? '97%' : '90%') : '100%', height: '100%', }} > @@ -5,9 +5,12 @@ import { useSession } from 'next-auth/react' import { TooltipCustom } from '#/shared' import { getApiUrl } from '#/shared/lib/constants' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { InputStyleSmallDark } from '#/shared/ui/input' import axios from 'axios' +const isValidLimit = (value: string) => value === '' || /^[1-9]\d*$/.test(value) + export type KeyItemBaseProps = { keyValue: string name: string @@ -32,6 +35,7 @@ export const KeyItem: React.FC = ({ children, limit: initialLimit, const [isCopy, setIsCopy] = useState(false) const [limit, setLimit] = useState(initialLimit) const { data: session } = useSession() + const { showMessage } = useShowDataStore() useEffect(() => { setLimit(initialLimit) @@ -72,6 +76,10 @@ export const KeyItem: React.FC = ({ children, limit: initialLimit, }, [initialLimit, limit, name, session?.access]) const handleLimitChange = (value: string) => { + if (!isValidLimit(value)) { + showMessage('Укажите целое число больше нуля') + return + } setLimit(value) } @@ -1,5 +1,4 @@ import React, { useCallback, useMemo } from 'react' -import { useDispatch } from 'react-redux' import { Box, Collapse, Stack, Typography } from '@mui/material' import Head from 'next/head' import { useRouter } from 'next/router' @@ -7,17 +6,19 @@ import { useSession } from 'next-auth/react' import { ChatSelect } from '#/app/components/chat_select' import { ResetFilters } from '#/app/components/filters/reset_filters' -import { setParams as setParametres } from '#/app/store/model-parametres-store' -import { useAppSelector } from '#/app/store/store' +import { replaceParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' import { IModel } from '#/entities/model-entity' import BotParamsMap from '#/features/bot-params/bot-params-map' +import { buildDefaultParamsForVersion } from '#/features/bot-params/lib/prune-model-params' +import { usePruneModelParams } from '#/features/bot-params/model/use-prune-model-params' import { selectCurrentChat } from '#/features/chats/chats-slice' +import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import Title from '#/features/title/title' import { TutorialContext } from '#/features/tutorial-context/tutorial-context' import { NextPageWithLayout } from '#/pages/_app' import { DrawerCustom, useModel } from '#/shared' import model_api from '#/shared/api/models/api' -import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import { getDeviceType, getOs } from '#/shared/lib/helpers' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { SvgIcon } from '#/shared/ui/svg' @@ -49,80 +50,71 @@ const Page: NextPageWithLayout = () => { const desktop = deviceType === 'desktop' const { data } = useSession() const router = useRouter() + const { push } = router + const slug = router.query.slug as string | undefined const currentChat = useAppSelector(selectCurrentChat) const { messages, sendMessage, loading, getMessagesPagination, deleteMessage } = useModel(currentChat, showMessage, modelType) const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useDispatch() + const dispatch = useAppDispatch() + + usePruneModelParams(botParams?.parameters, version) const deleteMessageMemo = useCallback(deleteMessage, [currentChat, messages]) - const { push } = useRouter() + React.useEffect(() => { + if (!slug) { + return + } + + dispatch(replaceParams({})) + }, [dispatch, slug]) React.useEffect(() => { - if (data?.access) { - model_api.getBotParams(router.asPath.split('/')[2], data.access).then((res) => { - if (!res.title) return push('/404') - setBotParams(res) - setModelType(res.slug) - if (res.versions.length !== 0) { - setVersion(res.versions[0].slug) - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => (v.versions.includes(res.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } else { - setVersion('') - dispatch(setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } - }) + if (!slug || !data?.access) { + return } - }, [data?.access, dispatch, push, router.asPath, router.query]) - const resetParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (botParams.versions?.length !== 0) { - setVersion(botParams.versions[0].slug) - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => (v.versions.includes(botParams.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) + model_api.getBotParams(slug, data.access).then((res) => { + if (!res.title) return push('/404') + setBotParams(res) + setModelType(res.slug) + + if (res.versions.length !== 0) { + const initialVersion = res.versions[0].slug + setVersion(initialVersion) + dispatch(replaceParams(buildDefaultParamsForVersion(res.parameters, initialVersion))) } else { - setVersion(botParams.slug) - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + setVersion('') + dispatch(replaceParams(buildDefaultParamsForVersion(res.parameters, ''))) } + }) + }, [data?.access, dispatch, push, slug]) + + const resetParams = () => { + if (!botParams) { + return + } + + if (botParams.versions?.length !== 0) { + const initialVersion = botParams.versions[0].slug + setVersion(initialVersion) + dispatch(replaceParams(buildDefaultParamsForVersion(botParams.parameters, initialVersion))) + } else { + setVersion(botParams.slug) + dispatch(replaceParams(buildDefaultParamsForVersion(botParams.parameters, ''))) } } const setDefaultParams = (newVersion?: string) => { - if (botParams) { - const targetVersion = newVersion !== undefined ? newVersion : version - - dispatch(setParametres({})) - if (targetVersion !== '') { - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => (v.versions.includes(targetVersion) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } else { - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } + if (!botParams) { + return } + + const targetVersion = newVersion !== undefined ? newVersion : version + dispatch(replaceParams(buildDefaultParamsForVersion(botParams.parameters, targetVersion))) } const viewMobileSettings = () => { @@ -133,32 +125,6 @@ const Page: NextPageWithLayout = () => { setOpenFiltersMobile(false) } - const onSendMessage = (input: string, required: (string | null)[]) => { - if (required.includes('text') && (input === '' || input === null)) { - showMessage('Введите сообщение!') - return false - } - if (required.includes('image') && file === null) { - showMessage('Прикрепите изображение!') - return false - } - - let data = { ...(includeParams || {}) } - if (version !== '') data = { ...data, ...{ version: version } } - - sendMessage({ - content: input, - file, - info: { - ...data, - }, - }) - return true - } - // theme removed: always dark - - // Подготавливаем данные для API вкладки - // Фильтруем параметры - оставляем только актуальные для текущей версии const filteredParams = useMemo(() => { if (!botParams?.parameters) { return {} @@ -187,6 +153,29 @@ const Page: NextPageWithLayout = () => { ) }, [botParams?.parameters, version, includeParams]) + const onSendMessage = useCallback( + (input: string, required: (string | null)[]) => { + if (required.includes('text') && (input === '' || input === null)) { + showMessage('Введите сообщение!') + return false + } + if (required.includes('image') && file === null) { + showMessage('Прикрепите изображение!') + return false + } + + const info = version !== '' ? { ...filteredParams, version } : { ...filteredParams } + + sendMessage({ + content: input, + file, + info, + }) + return true + }, + [file, filteredParams, sendMessage, showMessage, version] + ) + const showFileExample = useMemo(() => { if (!botParams?.inputs) return false @@ -206,7 +195,10 @@ const Page: NextPageWithLayout = () => { }) }, [botParams?.inputs, version]) - const predictPriceInfo = useMemo(() => ({ ...(includeParams || {}), ...(version ? { version } : {}) }), [includeParams, version]) + const predictPriceInfo = useMemo( + () => (version ? { ...filteredParams, version } : { ...filteredParams }), + [filteredParams, version] + ) const predictedPrice = usePredictPrice({ modelSlug: modelType, @@ -3,33 +3,66 @@ import { Box, Typography } from '@mui/material' import axios from 'axios' import Link from 'next/link' import { useRouter } from 'next/router' +import { signIn } from 'next-auth/react' import { getApiUrl } from '#/shared/lib/constants' +import { getApiDetail } from '#/shared/lib/helpers/decode-error' import { NextPageWithLayout } from '#/pages/_app' -import { signIn } from 'next-auth/react' + +const CONFIRM_FAILED_MESSAGE = + 'Не удалось подтвердить почту. Попробуйте позже или войдите вручную.' const Confirm: NextPageWithLayout = () => { const { push } = useRouter() const [success, setSuccess] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) + + const showConfirmError = (message?: string | null) => { + setSuccess(false) + setErrorMessage(message?.trim() || CONFIRM_FAILED_MESSAGE) + } useEffect(() => { - let formData: any = new FormData() - const tokenData = window.location.search.slice(1).split('=')[1] - formData.append("token", tokenData) + const params = new URLSearchParams(window.location.search) + const tokenData = params.get('token') ?? params.get('email_token') + + if (!tokenData) { + showConfirmError('Ссылка для подтверждения недействительна') + return + } + + const confirmEmail = async () => { + try { + const { data } = await axios.post<{ access?: string; refresh?: string }>( + getApiUrl() + '/v2/auth/confirm', + { token: tokenData }, + { headers: { 'Content-Type': 'application/json' } } + ) + + if (!data?.access || !data?.refresh) { + showConfirmError() + return + } + + const resp = await signIn('tokens', { + access: data.access, + refresh: data.refresh, + redirect: false, + }) + + if (resp?.ok) { + setSuccess(true) + push('/') + return + } + + showConfirmError() + } catch (err) { + showConfirmError(getApiDetail(err)) + } + } - axios.post(getApiUrl() + '/auth/confirm', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, validateStatus: (status) => status < 400 - }).then(() => { - setSuccess(true) - setTimeout(() => signIn('email_token', { token: tokenData, redirect: false }) - .then(() => push('/')) - .catch((err) => push({ pathname: '/login', query: { error: err ?? '' } })), 5000) - }).catch((err) => { - setSuccess(false) - setTimeout(() => push({ pathname: '/login', query: { error: err?.response.data.detail ?? '' } }), 5000) - }) + void confirmEmail() }, []) return ( @@ -56,9 +89,7 @@ const Confirm: NextPageWithLayout = () => { ) : success !== null && !success ? ( <> - Произошла проблема при потверждении почты -
- Уже получили сообщение об ошибке, пожалуйста - повторите попытку позже + {errorMessage ?? CONFIRM_FAILED_MESSAGE} Вернуться к авторизации @@ -351,7 +351,7 @@ const ImageModelPage: NextPageWithLayout = () => { ) : ( <> )} - {botParams && botParams.parameters?.length > 0 && ( + {botParams && botParams.parameters?.length > 0 && Object.keys(filteredParams).length > 0 && ( { )} - {botParams && botParams.parameters?.length > 0 ? ( + {botParams && botParams.parameters?.length > 0 && Object.keys(filteredParams).length > 0 ? ( setOpenFiltersMobile(false)} reset={resetParams} /> @@ -433,7 +433,7 @@ const ImageModelPage: NextPageWithLayout = () => { ) : ( <> )} - {botParams && botParams.parameters?.length > 0 ? ( + {botParams && botParams.parameters?.length > 0 && Object.keys(filteredParams).length > 0 ? ( <> { const { query, push } = useRouter() - const [, setCookie] = useCookies() + const [cookie, setCookie] = useCookies() React.useEffect(() => { Object.entries(query).forEach(([key, value]) => setCookie(key, value)) - }, []) + }, [query]) const { showMessage } = useShowDataStore() @@ -50,8 +50,21 @@ const Login: NextPageWithLayout = () => { if (!error || error === '') return - const expectedError = ERRROR_YANDEX_TRANSLATE_MAPPING[error] - showMessage(expectedError ?? 'Не удалось выполнить вход, попробуйте позже') + const yandexError = ERRROR_YANDEX_TRANSLATE_MAPPING[error] + if (yandexError) { + showMessage(yandexError) + return + } + + const authError = Object.fromEntries( + Object.entries(ERROR_MAPPING).map(([message, slug]) => [slug, message]) + )[error] + if (authError) { + showMessage(authError) + return + } + + showMessage(error) }, []) async function onSubmit(data: Record) { @@ -3,23 +3,38 @@ import { useDispatch } from 'react-redux' import { Box } from '@mui/material' import CircularProgress from '@mui/material/CircularProgress' import { useRouter } from 'next/router' +import { useSession } from 'next-auth/react' import { addReferral } from '#/entities/user-account/model/user-type-slice' import { NextPageWithLayout } from '#/pages/_app' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' export interface ReferralRegisterProps { email: string } +const REFERRAL_BLOCKED_MESSAGE = + 'Вы не можете повторно зарегистрироваться по реферальной ссылке или стать рефералом после регистрации.' + const ReferralRegister: NextPageWithLayout = ({ email }) => { const dispatch = useDispatch() - const { push } = useRouter() + const { push, replace } = useRouter() + const { status } = useSession() + const { showMessage } = useShowDataStore() useEffect(() => { + if (status === 'loading') return + + if (status === 'authenticated') { + showMessage(REFERRAL_BLOCKED_MESSAGE, 'error') + replace('/') + return + } + localStorage.setItem('referral', email) dispatch(addReferral(email)) push('/register') - }, []) + }, [status, email, dispatch, push, replace, showMessage]) return ( @@ -1,5 +1,6 @@ import * as React from 'react' import { useEffect, useState } from 'react' +import { useCookies } from 'react-cookie' import { useDispatch } from 'react-redux' import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' @@ -27,7 +28,12 @@ const Register: NextPageWithLayout = () => { const referral = useAppSelector((state) => state.user.referral) const dispatch = useDispatch() - const { push } = useRouter() + const { query, push } = useRouter() + const [, setCookie] = useCookies() + + React.useEffect(() => { + Object.entries(query).forEach(([key, value]) => setCookie(key, value)) + }, [query]) const onSuccessRegister = () => { setSuccess(true) @@ -114,7 +120,7 @@ const Register: NextPageWithLayout = () => { {referral === '' && isReferral && ( <> - + Зарегистрироваться с Яндекс ID или email @@ -1,112 +0,0 @@ -import React from 'react' -import { useForm } from 'react-hook-form' -import { Typography } from '@mui/material' - -import { useAppSelector } from '#/app/store/store' -import AttachSvg from '#/assets/svg/attach.svg?react' -import PlusSvg from '#/assets/svg/plus.svg?react' -import { useErrorReport } from '#/features/error-report/model/use-error-report' -import { CompanyAutocomplete } from '#/features/register-business/api/company-autocomplete' -import { companyNameOptions } from '#/features/register-business/lib/constatnts-step-legal-informative' -import { NextPageWithLayout } from '#/pages/_app' -import { c } from '#/shared' -import { useBodyScrollLock } from '#/shared/lib/hooks/use-body-scroll-lock' -import { CommonButton } from '#/shared/ui/button/ui' -import { CommonTextArea } from '#/shared/ui/common-textarea' - -import styles from './error-mobile.module.scss' - -const BusinessReportsMobile: NextPageWithLayout = () => { - const { error, isSend, setErrorMessage, handleFileChange, file, setFile, sendError, handleGoBackPage } = useErrorReport('business') - - const { companyName } = useAppSelector((state) => state.stepper.dataForCreate) - const [isAutocompleteOpen, setAutocompleteOpen] = React.useState(false) - - // хук для предотвращения скрола по странице - useBodyScrollLock(isAutocompleteOpen) - - const methods = useForm({ - defaultValues: { - companyName - }, - }) - - return ( - <> - {isSend ? ( -
-

Спасибо!

-

Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации

-
- ) : ( -
-

Запрос для компаний и партнеров

- - Опишите ваш сценарий — подберем решение и ответим за день. Работаем для корпораций, агентств и интеграторов - - - setAutocompleteOpen(true)} - onClose={() => setAutocompleteOpen(false)} - /> - - setErrorMessage(evt.target.value)} - buttomSlot={ - <> - - - } - /> - {file && ( -
-
- -
- {file.name} - {(file.size / 1024).toFixed(1)} KB -
-
- - -
- )} -
- - Отправить - - - Отмена - -
-
- )} - - ) -} - -export default BusinessReportsMobile @@ -11,7 +11,6 @@ import { getUserBalance } from '#/entities/balance' import { getAllInfo } from '#/entities/user-account' import { ChangePasswordPlate } from '#/features/change-password-corp' import { ErrorReportPlate } from '#/features/error-report' -import { BusinessErrorReportPlate } from '#/features/error-report/ui/business-error-report' import { change } from '#/features/pending' import { SlowLoading } from '#/features/slow-loading' import { Loader } from '#/shared' @@ -145,7 +144,6 @@ export const Layout: React.FC = ({ children, titlePage, isLoader, h {/* Дефолтные модалки */} -
) @@ -109,13 +109,6 @@ export const MainMenuMobile = memo(() => { link={'/reports-mobile'} icon={'/svg/side-menu/side-question'} /> - = ({ modelTitle, messageContent, }) => { - const [fontFamily, setFontFamily] = useState('Raleway,sans-serif') + const [fontFamily, setFontFamily] = useState('Inter,sans-serif') const [fontSize, setFontSize] = useState(16) const contentRef = useRef(null) const markdownRef = useRef(null) @@ -213,7 +213,7 @@ export const FullscreenMessageModal: React.FC = ({ } } - const fonts = ['Raleway,sans-serif', 'Arial', 'Times New Roman', 'Courier New', 'Georgia', 'Verdana'] + const fonts = ['Inter,sans-serif', 'Arial', 'Times New Roman', 'Courier New', 'Georgia', 'Verdana'] return ( = memo(({ device, images, (device === 'mobile' ? 35 : 30) ? message.content : ''} + wrapperWidth={device === 'desktop' ? '250px' : '291px'} + title={message.content} > message.content.length > 0 && onPromptClick?.(message.content)} sx={{ fontSize: '15px', - maxWidth: device === 'desktop' ? '250px' : '100%', + width: '100%', marginTop: '12px', color: '#A4AAB5', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', cursor: onPromptClick && message.content.length > 0 ? 'pointer' : 'default', }} > @@ -396,7 +396,7 @@ export const UserMessage = React.memo(function UserMessage({ setCurrentSrc, setM fontSize: '15px', marginTop: 0.5, textAlign: 'left', - fontFamily: 'Raleway,sans-serif', + fontFamily: 'Inter,sans-serif', borderRadius: '13px', '& p': { color: 'inherit', @@ -138,24 +138,27 @@ export const VideoMessagesList: React.FC = memo(({ device, videos, 30 ? message.content : ''} + wrapperWidth={device === 'desktop' ? '250px' : '291px'} + title={message.content} > message.content.length > 0 && onPromptClick?.(message.content)} sx={{ fontSize: '15px', - maxWidth: device === 'desktop' ? '250px' : '100%', + width: '100%', marginTop: '12px', color: '#A4AAB5', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', cursor: onPromptClick && message.content.length > 0 ? 'pointer' : 'default', }} > {!loadedVideos.has(message.uid) ? '' : message.content.length > 0 - ? message?.content.replaceAll('"', '').slice(0, 27) + ? message?.content.replaceAll('"', '') : 'описание отсутствует'} - {message?.content.length > 27 && loadedVideos.has(message.uid) && '...'} @@ -1,23 +1,7 @@ -import { useGlobalSettings, UserSettingsContextProvider } from '#/entities/user-account' -import { useSession } from 'next-auth/react' -import React, { PropsWithChildren, useEffect } from 'react' +import React, { PropsWithChildren } from 'react' interface ProvidersProps extends PropsWithChildren {} export const Providers = ({ children }: ProvidersProps) => { - const { settings, fetchUserSettings, ...rest } = useGlobalSettings() - - const { data } = useSession() - - useEffect(() => { - if(!data) return - if (settings.length > 0) return - fetchUserSettings() - }, [data]) - - return ( - - {children} - - ) + return <>{children} } @@ -55,8 +55,16 @@ export const Referral = ({ device }: IProps) => { }, [data?.access]) return ( - - + + { maxWidth: '350px', width: '100%', color: '#8280FF', + position: 'relative', }} gap={2} justifyContent={'space-between'} direction={'row'} alignItems={'center'} > - {device === 'mobile' ? url.slice(0, 35) + '...' : url.slice(0, 40) + '...'} + + {url} + {''} { width: '20px', height: '20px', cursor: 'pointer', + position: 'absolute', + right: '10px', }} width={100} height={100} @@ -242,7 +266,7 @@ export const Referral = ({ device }: IProps) => { { color: '#A4AAB5', fontWeight: '600', letterSpacing: '0.3px', - marginBottom: '15px', + marginBottom: '16px', }} > МОИ РЕФЕРАЛЫ @@ -1,6 +1,6 @@ import * as React from 'react' -import { useEffect, useMemo } from 'react' -import { Box, Tooltip, Typography, useMediaQuery } from '@mui/material' +import { useEffect, useMemo, useState } from 'react' +import { Box, Tooltip, Typography } from '@mui/material' import MuiDrawer from '@mui/material/Drawer' import List from '@mui/material/List' import ListItem from '@mui/material/ListItem' @@ -15,8 +15,7 @@ import { useSession } from 'next-auth/react' import { useNextStep } from 'nextstepjs' import { useAppSelector } from '#/app/store/store' -import { useUserSettingsContext } from '#/entities/user-account' -import { BUSINESS_ERROR_REPORT, ERROR_REPORT, getModalById } from '#/features/modals' +import { ERROR_REPORT, getModalById } from '#/features/modals' import '../styles/styles.module.css' import styles from '../styles/styles.module.css' @@ -121,19 +120,16 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' export const SideMenu = ({}) => { const { status } = useSession() const { pathname, asPath } = useRouter() - const { getOptionValue, settings, updateSettings } = useUserSettingsContext() const account_type = useAppSelector((state) => state.user.account_type) const { isNextStepVisible } = useNextStep() const error_report = getModalById(ERROR_REPORT) - const business_error_report = getModalById(BUSINESS_ERROR_REPORT) - const match = useMediaQuery('(min-height:800px)') - const open = useMemo(() => getOptionValue('sidemenu', { sidemenu_state: 'opened' }).sidemenu_state, [settings]) + const [open, setOpen] = useState<'opened' | 'closed'>('closed') useEffect(() => { - if (isNextStepVisible) updateSettings('sidemenu', { sidemenu_state: 'closed' }) - }, [isNextStepVisible, updateSettings]) + if (isNextStepVisible) setOpen('closed') + }, [isNextStepVisible, setOpen]) const filteredMenuListTop = useMemo(() => { return menuListTop.filter((item) => { @@ -146,12 +142,12 @@ export const SideMenu = ({}) => { }, [account_type]) const handleCloseDrawer = () => { - if (!isNextStepVisible) updateSettings('sidemenu', { sidemenu_state: 'closed' }) + if (!isNextStepVisible) setOpen('closed') } const handleToggleOpen = () => { if (!isNextStepVisible) { - updateSettings('sidemenu', { sidemenu_state: open === 'opened' ? 'closed' : 'opened' }) + setOpen(open === 'opened' ? 'closed' : 'opened') } } @@ -257,19 +253,6 @@ export const SideMenu = ({}) => { icon={'/svg/side-menu/side-question'} /> - { - business_error_report.setState(true) - }} - > - -