From 825be2d0a6916376922c1b094f4b3062888ed0f4 Mon Sep 17 00:00:00 2001 From: kazachilo Date: Fri, 11 Apr 2025 13:54:13 +0300 Subject: [PATCH] Fix image loading issues in buttons by enhancing ImageWithFallback component with button variant --- src/components/blocks/SquareButton.tsx | 10 +- .../generation/EmotionTypeSelector.tsx | 46 + src/components/generation/GenderSelector.tsx | 44 + .../generation/GenerationButton.tsx | 43 + src/components/generation/MainActions.tsx | 50 + src/components/generation/MemeSelector.tsx | 46 + src/components/generation/PhotoUpload.tsx | 44 + src/components/generation/PresetSelector.tsx | 87 ++ src/components/generation/StyleSelector.tsx | 47 + .../shared/ImageWithFallback.module.css | 17 + src/components/shared/ImageWithFallback.tsx | 16 +- .../tokens/TokenPacksModalContainer.tsx | 67 ++ src/hooks/useGenerationState.ts | 472 +++++++++ src/hooks/useImageCheck.ts | 121 +++ src/hooks/useNotifications.ts | 162 +++ src/screens/Home.tsx | 944 ++++-------------- src/types/generation.ts | 83 ++ src/utils/balanceUtils.ts | 76 ++ src/utils/generationUtils.ts | 143 +++ 19 files changed, 1755 insertions(+), 763 deletions(-) create mode 100644 src/components/generation/EmotionTypeSelector.tsx create mode 100644 src/components/generation/GenderSelector.tsx create mode 100644 src/components/generation/GenerationButton.tsx create mode 100644 src/components/generation/MainActions.tsx create mode 100644 src/components/generation/MemeSelector.tsx create mode 100644 src/components/generation/PhotoUpload.tsx create mode 100644 src/components/generation/PresetSelector.tsx create mode 100644 src/components/generation/StyleSelector.tsx create mode 100644 src/components/tokens/TokenPacksModalContainer.tsx create mode 100644 src/hooks/useGenerationState.ts create mode 100644 src/hooks/useImageCheck.ts create mode 100644 src/hooks/useNotifications.ts create mode 100644 src/types/generation.ts create mode 100644 src/utils/balanceUtils.ts create mode 100644 src/utils/generationUtils.ts diff --git a/src/components/blocks/SquareButton.tsx b/src/components/blocks/SquareButton.tsx index 91494ed..7f0a141 100644 --- a/src/components/blocks/SquareButton.tsx +++ b/src/components/blocks/SquareButton.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { BlockButton } from '../../types/blocks'; +import ImageWithFallback from '../shared/ImageWithFallback'; import styles from './SquareButton.module.css'; interface SquareButtonProps extends BlockButton { @@ -87,7 +88,14 @@ const SquareButton: React.FC = ({ data-selected={isSelected ? 'true' : 'false'} /* Добавляем data-атрибут для дополнительной стилизации */ > {imageUrl ? ( - {title} + ) : icon ? ( {icon} ) : null} diff --git a/src/components/generation/EmotionTypeSelector.tsx b/src/components/generation/EmotionTypeSelector.tsx new file mode 100644 index 0000000..5f718ac --- /dev/null +++ b/src/components/generation/EmotionTypeSelector.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; + +interface EmotionTypeSelectorProps { + selectedEmotionTypeButtonId?: string; + onEmotionTypeSelect: (emotionType: 'memes' | 'prompts', buttonId?: string) => void; + visible: boolean; +} + +/** + * Компонент для выбора типа эмоций (мемы или промпты) + */ +const EmotionTypeSelector: React.FC = ({ + selectedEmotionTypeButtonId, + onEmotionTypeSelect, + visible +}) => { + if (!visible) { + return null; + } + + // Находим блок выбора типа эмоций в конфигурации + const selectorBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'emotionTypeSelection'); + + if (!selectorBlock) { + return null; + } + + // Обработчик выбора типа эмоций + const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { + if (actionType === 'selectEmotionType') { + onEmotionTypeSelect(actionValue as 'memes' | 'prompts', buttonId); + } + }; + + return ( + + ); +}; + +export default EmotionTypeSelector; diff --git a/src/components/generation/GenderSelector.tsx b/src/components/generation/GenderSelector.tsx new file mode 100644 index 0000000..0da9bf9 --- /dev/null +++ b/src/components/generation/GenderSelector.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; + +interface GenderSelectorProps { + selectedGenderButtonId?: string; + onGenderDetectionSelect: (detection: 'auto' | 'manual', buttonId?: string) => void; + onGenderSelect: (gender: 'man' | 'woman', buttonId?: string) => void; +} + +/** + * Компонент для выбора пола + */ +const GenderSelector: React.FC = ({ + selectedGenderButtonId, + onGenderDetectionSelect, + onGenderSelect +}) => { + // Находим блок выбора пола в конфигурации + const genderBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'genderSelection'); + + if (!genderBlock) { + return null; + } + + // Обработчик выбора пола + const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { + if (actionType === 'selectGenderDetection') { + onGenderDetectionSelect('auto', buttonId); + } else if (actionType === 'selectGender') { + onGenderSelect(actionValue as 'man' | 'woman', buttonId); + } + }; + + return ( + + ); +}; + +export default GenderSelector; diff --git a/src/components/generation/GenerationButton.tsx b/src/components/generation/GenerationButton.tsx new file mode 100644 index 0000000..e2914c7 --- /dev/null +++ b/src/components/generation/GenerationButton.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; +import styles from '../../screens/Home.module.css'; + +interface GenerationButtonProps { + onStartGeneration: () => void; + isGenerating: boolean; +} + +/** + * Компонент кнопки генерации + */ +const GenerationButton: React.FC = ({ + onStartGeneration, + isGenerating +}) => { + // Находим блок кнопки генерации в конфигурации + const generateButton = homeScreenConfig.homeScreen.blocks.find(block => block.type === 'generateButton'); + + if (!generateButton) { + return null; + } + + // Обработчик нажатия на кнопку генерации + const handleAction = (actionType: string, actionValue: string) => { + if (actionType === 'function' && actionValue === 'startGeneration' && !isGenerating) { + onStartGeneration(); + } + }; + + return ( +
+ +
+ ); +}; + +export default GenerationButton; diff --git a/src/components/generation/MainActions.tsx b/src/components/generation/MainActions.tsx new file mode 100644 index 0000000..0a03290 --- /dev/null +++ b/src/components/generation/MainActions.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; + +interface MainActionsProps { + onSendFeedback: () => void; + onOpenTelegramBot: () => void; +} + +/** + * Компонент для верхнего блока с кнопками + */ +const MainActions: React.FC = ({ + onSendFeedback, + onOpenTelegramBot +}) => { + // Находим блок верхних кнопок и разделитель в конфигурации + const actionsBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'mainActions'); + const dividerBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'mainDivider'); + + if (!actionsBlock || !dividerBlock) { + return null; + } + + // Обработчик действий кнопок + const handleAction = (actionType: string, actionValue: string) => { + if (actionType === 'function') { + if (actionValue === 'sendFeedback') { + onSendFeedback(); + } else if (actionValue === 'openTelegramBot') { + onOpenTelegramBot(); + } + } + }; + + return ( + <> + + {}} + /> + + ); +}; + +export default MainActions; diff --git a/src/components/generation/MemeSelector.tsx b/src/components/generation/MemeSelector.tsx new file mode 100644 index 0000000..65d4ef5 --- /dev/null +++ b/src/components/generation/MemeSelector.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; + +interface MemeSelectorProps { + selectedMemeId?: string; + onMemeSelect: (meme: string, buttonId?: string) => void; + visible: boolean; +} + +/** + * Компонент для выбора мема + */ +const MemeSelector: React.FC = ({ + selectedMemeId, + onMemeSelect, + visible +}) => { + if (!visible) { + return null; + } + + // Находим блок выбора мема в конфигурации + const memeBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'memeSelection'); + + if (!memeBlock) { + return null; + } + + // Обработчик выбора мема + const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { + if (actionType === 'selectMeme') { + onMemeSelect(actionValue, buttonId); + } + }; + + return ( + + ); +}; + +export default MemeSelector; diff --git a/src/components/generation/PhotoUpload.tsx b/src/components/generation/PhotoUpload.tsx new file mode 100644 index 0000000..b1faeb2 --- /dev/null +++ b/src/components/generation/PhotoUpload.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; + +interface PhotoUploadProps { + onImageDataChange: (imageData: string) => void; +} + +/** + * Компонент для загрузки фото + */ +const PhotoUpload: React.FC = ({ + onImageDataChange +}) => { + // Находим заголовок и блок загрузки фото в конфигурации + const titleBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'step1'); + const uploadBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'photoUpload'); + + if (!titleBlock || !uploadBlock) { + return null; + } + + // Обработчик загрузки фото + const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string, extraData?: any) => { + if (actionType === 'uploadPhoto' && extraData?.imageData) { + onImageDataChange(extraData.imageData); + } + }; + + return ( + <> + {}} + /> + + + ); +}; + +export default PhotoUpload; diff --git a/src/components/generation/PresetSelector.tsx b/src/components/generation/PresetSelector.tsx new file mode 100644 index 0000000..bc6dc32 --- /dev/null +++ b/src/components/generation/PresetSelector.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; +import { stylePresets } from '../../config/stylePresets'; + +interface PresetSelectorProps { + selectedPresetId?: string; + onPresetSelect: (preset: string, buttonId?: string) => void; + selectedStyle: string; + selectedEmotionType?: 'memes' | 'prompts'; + isInputVisible: boolean; + onToggleInput: () => void; + onCustomPromptChange: (text: string) => void; +} + +/** + * Компонент для выбора пресета + */ +const PresetSelector: React.FC = ({ + selectedPresetId, + onPresetSelect, + selectedStyle, + selectedEmotionType, + isInputVisible, + onToggleInput, + onCustomPromptChange +}) => { + // Находим блоки выбора пресета в конфигурации + const presetBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'quickActions'); + const emotionPromptsBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'emotionPromptsSelection'); + const customPromptBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'customPrompt'); + + if (!presetBlock && !emotionPromptsBlock) { + return null; + } + + // Обработчик выбора пресета + const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { + if (actionType === 'selectPreset') { + onPresetSelect(actionValue, buttonId); + } else if (actionType === 'function' && actionValue === 'toggleInput') { + onToggleInput(); + } + }; + + // Определяем, какой блок показывать в зависимости от выбранного стиля и типа эмоций + let blockToRender = null; + + if (selectedStyle === 'emotions' && selectedEmotionType === 'prompts') { + blockToRender = emotionPromptsBlock; + } else if (selectedStyle !== 'emotions') { + blockToRender = presetBlock; + } + + // Модифицируем блок, чтобы показывать только кнопки из выбранного стиля + const modifiedBlock = blockToRender ? { + ...blockToRender, + buttons: selectedStyle === 'emotions' && selectedEmotionType === 'prompts' + ? stylePresets.emotions?.buttons || [] + : stylePresets[selectedStyle]?.buttons || [] + } : null; + + return ( + <> + {modifiedBlock && ( + + )} + + {customPromptBlock && isInputVisible && ( + {}} + extraProps={{ + visible: isInputVisible, + onTextChange: onCustomPromptChange + }} + /> + )} + + ); +}; + +export default PresetSelector; diff --git a/src/components/generation/StyleSelector.tsx b/src/components/generation/StyleSelector.tsx new file mode 100644 index 0000000..2264633 --- /dev/null +++ b/src/components/generation/StyleSelector.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import BlockRenderer from '../blocks/BlockRenderer'; +import { homeScreenConfig } from '../../config/homeScreen'; + +interface StyleSelectorProps { + selectedStyleButtonId?: string; + onStyleSelect: (style: string, buttonId?: string) => void; +} + +/** + * Компонент для выбора стиля генерации + */ +const StyleSelector: React.FC = ({ + selectedStyleButtonId, + onStyleSelect +}) => { + // Находим заголовок и блок выбора стиля в конфигурации + const titleBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'step2'); + const styleBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'styleActions'); + + if (!titleBlock || !styleBlock) { + return null; + } + + // Обработчик выбора стиля + const handleAction = (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { + if (actionType === 'selectStyle') { + onStyleSelect(actionValue, buttonId); + } + }; + + return ( + <> + {}} + /> + + + ); +}; + +export default StyleSelector; diff --git a/src/components/shared/ImageWithFallback.module.css b/src/components/shared/ImageWithFallback.module.css index f89eb6e..5edf2c4 100644 --- a/src/components/shared/ImageWithFallback.module.css +++ b/src/components/shared/ImageWithFallback.module.css @@ -12,6 +12,11 @@ user-select: none; } +/* Специальный контейнер для кнопок - без фона */ +.buttonContainer { + background-color: transparent; +} + .image { width: 100%; height: 100%; @@ -22,6 +27,18 @@ user-drag: none; } +/* Специальный стиль для изображений в кнопках */ +.buttonImage { + position: absolute; + top: 1%; + left: 0%; + width: 100%; + height: 100%; + object-fit: contain; + transform: scale(1); + transform-origin: center; +} + .hidden { opacity: 0; position: absolute; diff --git a/src/components/shared/ImageWithFallback.tsx b/src/components/shared/ImageWithFallback.tsx index 4ca3ae1..15deb3b 100644 --- a/src/components/shared/ImageWithFallback.tsx +++ b/src/components/shared/ImageWithFallback.tsx @@ -9,6 +9,8 @@ interface ImageWithFallbackProps { onContextMenu?: (e: React.MouseEvent) => void; maxRetries?: number; isDeleteMode?: boolean; + showErrorUI?: boolean; // Показывать ли UI с ошибкой + variant?: 'default' | 'button'; // Вариант отображения: обычный или для кнопок } const ImageWithFallback: React.FC = ({ @@ -18,7 +20,9 @@ const ImageWithFallback: React.FC = ({ onClick, onContextMenu, maxRetries = 2, // По умолчанию 2 попытки автоматической перезагрузки - isDeleteMode = false + isDeleteMode = false, + showErrorUI = true, // По умолчанию показываем UI с ошибкой + variant = 'default' // По умолчанию обычный вариант отображения }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -111,12 +115,12 @@ const ImageWithFallback: React.FC = ({ return (
{/* Показываем индикатор загрузки, если изображение загружается */} - {loading && ( + {loading && variant !== 'button' && (
@@ -128,13 +132,13 @@ const ImageWithFallback: React.FC = ({ src={imageSrc} alt={alt} draggable="false" - className={`${styles.image} ${error ? styles.hidden : ''}`} + className={`${styles.image} ${variant === 'button' ? styles.buttonImage : ''} ${error ? styles.hidden : ''}`} onLoad={handleLoad} onError={handleError} /> - {/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка */} - {error && ( + {/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка и showErrorUI=true */} + {error && showErrorUI && (
!
Ошибка загрузки
diff --git a/src/components/tokens/TokenPacksModalContainer.tsx b/src/components/tokens/TokenPacksModalContainer.tsx new file mode 100644 index 0000000..de020c8 --- /dev/null +++ b/src/components/tokens/TokenPacksModalContainer.tsx @@ -0,0 +1,67 @@ +import React, { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import TokenPacksModal from './TokenPacksModal'; +import { paymentService } from '../../services/paymentService'; +import { tokenPacks } from '../../constants/tokenPacks'; +import { useBalance } from '../../contexts/BalanceContext'; +import { updateBalanceWithRetries } from '../../utils/balanceUtils'; + +interface TokenPacksModalContainerProps { + isVisible: boolean; + onClose: () => void; + missingTokens: number; + onSuccess?: () => void; +} + +/** + * Контейнерный компонент для модального окна с пакетами токенов + */ +const TokenPacksModalContainer: React.FC = ({ + isVisible, + onClose, + missingTokens, + onSuccess +}) => { + const navigate = useNavigate(); + const { updateBalance } = useBalance(); + + /** + * Обработчик покупки пакета токенов + */ + const handleBuyPack = useCallback((packId: string) => { + const pack = tokenPacks.find(p => p.id === packId); + if (!pack) return; + + onClose(); + + paymentService.showBuyTokensPopup(pack, async (userData) => { + // Обновляем баланс с повторными попытками + updateBalanceWithRetries(updateBalance); + + // Вызываем колбэк успешной покупки, если он передан + if (onSuccess) { + onSuccess(); + } + }); + }, [onClose, updateBalance, onSuccess]); + + /** + * Обработчик нажатия на кнопку "Показать все пакеты" + */ + const handleShowAllPacks = useCallback(() => { + onClose(); + navigate('/profile'); + }, [onClose, navigate]); + + return ( + + ); +}; + +export default TokenPacksModalContainer; diff --git a/src/hooks/useGenerationState.ts b/src/hooks/useGenerationState.ts new file mode 100644 index 0000000..163d15a --- /dev/null +++ b/src/hooks/useGenerationState.ts @@ -0,0 +1,472 @@ +import { useState, useCallback } from 'react'; +import { LastGenerationData, GenerationState } from '../types/generation'; +import { isSameGeneration, validateGenerationParams, determineWorkflowType, prepareGenerationOptions } from '../utils/generationUtils'; +import { checkSufficientBalance, updateBalanceWithRetries } from '../utils/balanceUtils'; +import apiService from '../services/api'; +import { useBalance } from '../contexts/BalanceContext'; +import { WorkflowType } from '../constants/workflows'; + +/** + * Хук для управления состоянием генерации + */ +export const useGenerationState = ( + showNotification: (title: string, message: string, options?: any) => void, + setShowTokensModal: (show: boolean) => void, + setMissingTokens: (tokens: number) => void, + startImageCheck: () => void +) => { + const { updateBalance } = useBalance(); + + // Состояние для предотвращения множественных нажатий на кнопку генерации + const [isGenerating, setIsGenerating] = useState(false); + + // Состояние для таймера обнаружения проблем с подключением + const [requestTimeoutId, setRequestTimeoutId] = useState | null>(null); + + // Состояние для превью и данных изображения + const [previewUrl, setPreviewUrl] = useState(() => { + // Проверяем, есть ли превью в состоянии навигации или localStorage + const state = window.history.state?.usr; + return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined; + }); + + const [imageData, setImageData] = useState(() => { + const state = window.history.state?.usr; + return state?.imageData || localStorage.getItem('stickerImageData') || undefined; + }); + + // Состояния для выбора стиля и пресета + const [selectedStyle, setSelectedStyle] = useState('emotions'); + const [selectedStyleButtonId, setSelectedStyleButtonId] = useState('emotions'); + const [selectedPresetId, setSelectedPresetId] = useState(undefined); + const [isInputVisible, setIsInputVisible] = useState(false); + const [customPrompt, setCustomPrompt] = useState(''); + + // Состояния для выбора типа эмоций и мема + const [selectedEmotionType, setSelectedEmotionType] = useState<'memes' | 'prompts' | undefined>('prompts'); + const [selectedEmotionTypeButtonId, setSelectedEmotionTypeButtonId] = useState('prompts'); + const [selectedMemeId, setSelectedMemeId] = useState(undefined); + + // Состояния для выбора пола + const [genderDetection, setGenderDetection] = useState<'auto' | 'manual'>('auto'); + const [manualGender, setManualGender] = useState<'man' | 'woman' | undefined>(undefined); + const [selectedGenderButtonId, setSelectedGenderButtonId] = useState('auto'); + + // Состояния для отслеживания задачи генерации и изображения + const [currentTaskId, setCurrentTaskId] = useState(undefined); + const [generatedImageUrl, setGeneratedImageUrl] = useState(undefined); + const [checkInterval, setCheckInterval] = useState(null); + const [queuePosition, setQueuePosition] = useState(undefined); + + // Состояние для хранения данных о последней генерации + const [lastGenerationData, setLastGenerationData] = useState({}); + + /** + * Обработчик выбора стиля + */ + const handleStyleSelect = useCallback((style: string, buttonId?: string) => { + setSelectedStyle(style); + setSelectedStyleButtonId(buttonId); + console.log('Selected style:', style, 'Button ID:', buttonId); + }, []); + + /** + * Обработчик выбора пресета + */ + const handlePresetSelect = useCallback((preset: string, buttonId?: string) => { + setSelectedPresetId(buttonId); + console.log('Selected preset:', preset, 'Button ID:', buttonId); + + // Если выбран пресет, отличный от "Свой промпт", скрываем поле ввода текста + if (buttonId !== 'customPrompt') { + setIsInputVisible(false); + } + }, []); + + /** + * Обработчик выбора типа эмоций + */ + const handleEmotionTypeSelect = useCallback((emotionType: 'memes' | 'prompts', buttonId?: string) => { + setSelectedEmotionType(emotionType); + setSelectedEmotionTypeButtonId(buttonId); + // Сбрасываем выбранный мем или пресет при смене типа + setSelectedMemeId(undefined); + setSelectedPresetId(undefined); + console.log('Selected emotion type:', emotionType, 'Button ID:', buttonId); + }, []); + + /** + * Обработчик выбора мема + */ + const handleMemeSelect = useCallback((meme: string, buttonId?: string) => { + setSelectedMemeId(buttonId); + setSelectedPresetId(buttonId); // Используем тот же ID для совместимости с существующей логикой + console.log('Selected meme:', meme, 'Button ID:', buttonId); + }, []); + + /** + * Обработчик выбора способа определения пола + */ + const handleGenderDetectionSelect = useCallback((detection: 'auto' | 'manual', buttonId?: string) => { + if (detection === 'auto') { + setGenderDetection('auto'); + setManualGender(undefined); + setSelectedGenderButtonId(buttonId); + console.log('Selected gender detection:', detection, 'Button ID:', buttonId); + } + }, []); + + /** + * Обработчик выбора пола + */ + const handleGenderSelect = useCallback((gender: 'man' | 'woman', buttonId?: string) => { + setGenderDetection('manual'); + setManualGender(gender); + setSelectedGenderButtonId(buttonId); + console.log('Selected gender:', gender, 'Button ID:', buttonId); + }, []); + + /** + * Обработчик переключения видимости поля ввода текста + */ + const handleToggleInput = useCallback(() => { + setIsInputVisible(prev => !prev); + // Устанавливаем selectedPresetId в 'customPrompt' при нажатии на кнопку "Свой промпт" + setSelectedPresetId('customPrompt'); + }, []); + + /** + * Обработчик изменения текста промпта + */ + const handleCustomPromptChange = useCallback((text: string) => { + setCustomPrompt(text); + }, []); + + /** + * Функция для запуска генерации + */ + const startGeneration = useCallback(async () => { + // Устанавливаем флаг генерации, чтобы предотвратить повторные нажатия + setIsGenerating(true); + + // Проверяем валидность параметров генерации + const validationResult = validateGenerationParams(imageData, selectedPresetId, customPrompt); + + if (!validationResult.isValid) { + // Сбрасываем флаг генерации, так как процесс не начался + setIsGenerating(false); + + showNotification( + validationResult.errorTitle || 'Внимание', + validationResult.errorMessage || 'Проверьте параметры генерации', + { showGalleryButton: false } + ); + return; + } + + // Проверяем, является ли текущая генерация повторной + const currentData = { + imageData, + style: selectedStyle, + presetId: selectedPresetId, + customPrompt: selectedPresetId === 'customPrompt' ? customPrompt : undefined + }; + + console.log('Comparing generations:', { + current: { + imageDataLength: imageData?.length, + style: selectedStyle, + presetId: selectedPresetId, + customPrompt + }, + last: { + imageDataLength: lastGenerationData.imageData?.length, + style: lastGenerationData.style, + presetId: lastGenerationData.presetId, + customPrompt: lastGenerationData.customPrompt + } + }); + + const isSame = isSameGeneration(currentData, lastGenerationData); + console.log('Is same generation:', isSame); + + if (isSame) { + showNotification( + 'Внимание', + 'Нельзя отправить одну и ту же комбинацию изображения и образа подряд. Пожалуйста, измените изображение или выберите другой образ.', + { showGalleryButton: false } + ); + return; + } + + // Объявляем переменную для таймера вне блока try, чтобы она была доступна в блоке catch + let connectionTimeoutId: ReturnType | undefined; + + try { + // Проверяем баланс перед генерацией + const balanceResult = await checkSufficientBalance(); + + if (!balanceResult.isEnough) { + // Сбрасываем флаг генерации, так как процесс не начался + setIsGenerating(false); + + setMissingTokens(balanceResult.missingTokens); + setShowTokensModal(true); + return; + } + + // Показываем уведомление о начале генерации + showNotification( + 'Генерация стикера', + 'Отправка запроса...', + { isLoading: true, showButtons: false } + ); + + // Очищаем предыдущий таймер, если он существует + if (requestTimeoutId) { + console.log('Clearing previous timeout with ID:', requestTimeoutId); + clearTimeout(requestTimeoutId); + setRequestTimeoutId(null); + } + + // Устанавливаем таймер для обнаружения проблем с подключением (6 секунд) + connectionTimeoutId = setTimeout(() => { + console.log('Connection timeout triggered'); + // Вызываем handleRequestTimeout из useNotifications через showNotification + showNotification( + 'Генерация стикера', + 'Возникли проблемы с подключением. Пожалуйста, проверьте интернет-соединение.', + { isLoading: true, showButtons: false } + ); + }, 6000); + + // Сохраняем ID таймера + setRequestTimeoutId(connectionTimeoutId); + console.log('Set connection timeout with ID:', connectionTimeoutId); + + // Сбрасываем URL изображения + setGeneratedImageUrl(undefined); + + // Если выбран "Свой промпт" и введен текст, используем его + const userPrompt = selectedPresetId === 'customPrompt' && customPrompt ? customPrompt : undefined; + + // Создаем объект с параметрами генерации + const generationOptions = prepareGenerationOptions( + selectedPresetId, + userPrompt, + genderDetection, + manualGender, + selectedStyle === 'emotions' && selectedEmotionType === 'memes' ? selectedMemeId : undefined + ); + + console.log('Generation options:', generationOptions); + + // Определяем тип воркфлоу в зависимости от выбранного стиля и типа эмоций + const workflowType = determineWorkflowType(selectedStyle, selectedEmotionType); + + console.log('Using workflow type:', workflowType, 'for style:', selectedStyle); + + // Запускаем проверку новых изображений + startImageCheck(); + + // Отправляем запрос на генерацию с использованием нового метода + const response = await apiService.generateImageWithWorkflow( + imageData!, + workflowType, + generationOptions + ); + console.log('Generation response:', response); + + // Очищаем таймер обнаружения проблем с подключением + // Используем локальную переменную connectionTimeoutId, а не состояние requestTimeoutId + console.log('Clearing connection timeout with ID:', connectionTimeoutId); + clearTimeout(connectionTimeoutId); + setRequestTimeoutId(null); + + // Сохраняем данные о текущей генерации + setLastGenerationData({ + imageData, + style: selectedStyle, + presetId: selectedPresetId, + customPrompt: userPrompt + }); + + // Обновляем баланс с повторными попытками + updateBalanceWithRetries(updateBalance); + + // Проверяем, была ли ошибка перевода + if (response.translationFailed) { + // Сбрасываем флаг генерации + setIsGenerating(false); + + showNotification( + 'Недопустимый промпт', + 'Промпт содержит недопустимый контент. Пожалуйста, используйте более нейтральные формулировки.', + { isLoading: false, showGalleryButton: false, showButtons: true } + ); + + // Логирование деталей ошибки для отладки + if (response.errorDetails) { + console.error('Детали ошибки перевода:', response.errorDetails); + } + + return; + } + + // Если нет ошибки перевода, продолжаем обработку результата + if (response.result) { + // Получаем результат + const { result } = response; + + // Обновляем уведомление с информацией о позиции в очереди + if (result.queue_position !== undefined) { + const estimatedTime = apiService.calculateEstimatedWaitTime(result.queue_position); + const minutes = Math.floor(estimatedTime / 60); + const seconds = estimatedTime % 60; + const timeString = minutes > 0 + ? `${minutes} мин ${seconds} сек` + : `${seconds} сек`; + + // Форматируем сообщение + showNotification( + 'Генерация стикера', + `Создание стикеров началось!\n` + + `Позиция в очереди: ${result.queue_position}\n` + + `Время ожидания: ${timeString}\n\n` + + `Результат будет доступен в галерее после завершения генерации.`, + { isLoading: false, showButtons: true } + ); + } else { + showNotification( + 'Генерация стикера', + `Создание стикеров началось!\n\n` + + `Результат будет доступен в галерее после завершения генерации.`, + { isLoading: false, showButtons: true } + ); + } + + // Сохраняем ID задачи + if (result.Task_ID) { + setCurrentTaskId(result.Task_ID); + + // Устанавливаем позицию в очереди + if (result.queue_position !== undefined) { + setQueuePosition(result.queue_position); + } + } + } + } catch (error) { + console.error('Generation failed:', error); + + // Очищаем таймер обнаружения проблем с подключением, если он был установлен + if (connectionTimeoutId) { + console.log('Clearing connection timeout with ID (in catch):', connectionTimeoutId); + clearTimeout(connectionTimeoutId); + setRequestTimeoutId(null); + } + + // Сбрасываем флаг генерации + setIsGenerating(false); + + showNotification( + 'Ошибка', + 'Не удалось начать генерацию', + { isLoading: false, showGalleryButton: false, showButtons: true } + ); + + // Сбрасываем ID задачи и позицию в очереди + setCurrentTaskId(undefined); + setQueuePosition(undefined); + } + }, [ + imageData, + selectedStyle, + selectedPresetId, + customPrompt, + lastGenerationData, + genderDetection, + manualGender, + selectedEmotionType, + selectedMemeId, + showNotification, + setShowTokensModal, + setMissingTokens, + startImageCheck, + updateBalance + ]); + + /** + * Функция для сброса состояния генерации + */ + const resetGenerationState = useCallback(() => { + setIsGenerating(false); + setCurrentTaskId(undefined); + setQueuePosition(undefined); + + // Сбрасываем URL сгенерированного изображения + setGeneratedImageUrl(undefined); + + // Очищаем интервал проверки + if (checkInterval) { + clearInterval(checkInterval); + setCheckInterval(null); + } + + // Очищаем таймаут запроса + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + setRequestTimeoutId(null); + } + }, [checkInterval, requestTimeoutId]); + + /** + * Функция для обновления URL сгенерированного изображения + */ + const updateGeneratedImageUrl = useCallback((url: string | undefined) => { + setGeneratedImageUrl(url); + }, []); + + return { + // Состояния + isGenerating, + requestTimeoutId, + previewUrl, + imageData, + selectedStyle, + selectedStyleButtonId, + selectedPresetId, + isInputVisible, + customPrompt, + selectedEmotionType, + selectedEmotionTypeButtonId, + selectedMemeId, + genderDetection, + manualGender, + selectedGenderButtonId, + currentTaskId, + generatedImageUrl, + checkInterval, + queuePosition, + lastGenerationData, + + // Методы для обновления состояний + setIsGenerating, + setRequestTimeoutId, + setPreviewUrl, + setImageData, + handleStyleSelect, + handlePresetSelect, + handleEmotionTypeSelect, + handleMemeSelect, + handleGenderDetectionSelect, + handleGenderSelect, + handleToggleInput, + handleCustomPromptChange, + startGeneration, + resetGenerationState, + updateGeneratedImageUrl, + setCheckInterval + }; +}; + +export default useGenerationState; diff --git a/src/hooks/useImageCheck.ts b/src/hooks/useImageCheck.ts new file mode 100644 index 0000000..ff29eb9 --- /dev/null +++ b/src/hooks/useImageCheck.ts @@ -0,0 +1,121 @@ +import { useState, useCallback, useRef } from 'react'; +import { ImageCheckState } from '../types/generation'; +import apiService from '../services/api'; + +/** + * Хук для проверки новых изображений + */ +export const useImageCheck = ( + onNewImageFound: (url: string | undefined) => void +) => { + // Состояние для отслеживания интервала проверки изображений + const [imagesCheckInterval, setImagesCheckInterval] = useState(null); + + // Используем useRef вместо useState для хранения начальных значений + // Это позволит избежать проблемы с асинхронным обновлением состояний + const initialImagesCountRef = useRef(0); + const initialImageIdsRef = useRef([]); + + /** + * Функция для проверки новых изображений + */ + const checkForNewImages = useCallback(async () => { + try { + // Получаем текущий список изображений + const images = await apiService.getGeneratedImages(); + + // Проверяем, увеличилось ли количество изображений + if (images.length > initialImagesCountRef.current) { + // Если количество изображений увеличилось, проверяем, что первое изображение действительно новое + const latestImage = images[0]; + const isNewImage = !initialImageIdsRef.current.includes(latestImage.link); + + if (isNewImage && latestImage.url) { + // Устанавливаем URL изображения только если это действительно новое изображение и URL существует + onNewImageFound(latestImage.url); + + // Очищаем интервал, так как изображение найдено + if (imagesCheckInterval) { + clearInterval(imagesCheckInterval); + setImagesCheckInterval(null); + } + } + } + } catch (error) { + console.error('Ошибка при проверке новых изображений:', error); + } + }, [imagesCheckInterval, onNewImageFound]); + + /** + * Функция для запуска проверки новых изображений + */ + const startImageCheck = useCallback(async () => { + try { + // Получаем текущий список изображений перед генерацией + const currentImages = await apiService.getGeneratedImages(); + + // Используем ref вместо состояний для мгновенного обновления + initialImagesCountRef.current = currentImages.length; + initialImageIdsRef.current = currentImages.map(img => img.link); + + // Очищаем предыдущий интервал, если он был + if (imagesCheckInterval) { + clearInterval(imagesCheckInterval); + } + + // Устанавливаем новый интервал + const intervalId = window.setInterval(() => { + checkForNewImages(); + }, 2000); // Проверяем каждые 2 секунды + + setImagesCheckInterval(intervalId as unknown as number); + } catch (error) { + console.error('Ошибка при получении списка изображений:', error); + // Продолжаем проверку даже при ошибке получения списка + + // Очищаем предыдущий интервал, если он был + if (imagesCheckInterval) { + clearInterval(imagesCheckInterval); + } + + // Устанавливаем новый интервал + const intervalId = window.setInterval(() => { + checkForNewImages(); + }, 2000); // Проверяем каждые 2 секунды + + setImagesCheckInterval(intervalId as unknown as number); + } + }, [imagesCheckInterval, checkForNewImages]); + + /** + * Функция для остановки проверки новых изображений + */ + const stopImageCheck = useCallback(() => { + if (imagesCheckInterval) { + clearInterval(imagesCheckInterval); + setImagesCheckInterval(null); + } + }, [imagesCheckInterval]); + + /** + * Функция для очистки ресурсов при размонтировании компонента + */ + const cleanup = useCallback(() => { + stopImageCheck(); + }, [stopImageCheck]); + + return { + // Состояния + imagesCheckInterval, + initialImagesCount: initialImagesCountRef.current, + initialImageIds: initialImageIdsRef.current, + + // Методы + startImageCheck, + stopImageCheck, + checkForNewImages, + cleanup + }; +}; + +export default useImageCheck; diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts new file mode 100644 index 0000000..a833c71 --- /dev/null +++ b/src/hooks/useNotifications.ts @@ -0,0 +1,162 @@ +import { useState, useCallback } from 'react'; +import { NotificationState } from '../types/generation'; +import { useNavigate } from 'react-router-dom'; + +/** + * Хук для управления уведомлениями + */ +export const useNotifications = () => { + const navigate = useNavigate(); + + // Состояния для модального окна уведомления + const [isNotificationVisible, setIsNotificationVisible] = useState(false); + const [notificationTitle, setNotificationTitle] = useState(''); + const [notificationMessage, setNotificationMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [showGalleryButton, setShowGalleryButton] = useState(true); + const [showButtons, setShowButtons] = useState(true); + const [continueButtonText, setContinueButtonText] = useState('Продолжить'); + + // Состояния для модального окна с токенами + const [showTokensModal, setShowTokensModal] = useState(false); + const [missingTokens, setMissingTokens] = useState(0); + const [lastPurchasedPack, setLastPurchasedPack] = useState(null); + + /** + * Функция для обработки таймаута запроса + */ + const handleRequestTimeout = useCallback(() => { + setNotificationMessage( + 'Возникли проблемы с подключением. Пожалуйста, проверьте интернет-соединение.\n\n' + + 'Запрос все еще обрабатывается, но это может занять больше времени, чем обычно.' + ); + // Добавляем кнопку отмены, чтобы пользователь мог прервать запрос + setShowButtons(true); + setContinueButtonText('Отменить'); + }, []); + + /** + * Вспомогательная функция для показа модальных окон с ошибками + */ + const showErrorModal = useCallback((title: string, message: string) => { + setNotificationTitle(title); + setNotificationMessage(message); + setIsLoading(false); + setShowGalleryButton(false); + setShowButtons(true); + setContinueButtonText('Закрыть'); + setIsNotificationVisible(true); + }, []); + + /** + * Функция для показа уведомления + */ + const showNotification = useCallback((title: string, message: string, options?: { + isLoading?: boolean; + showGalleryButton?: boolean; + showButtons?: boolean; + continueButtonText?: string; + }) => { + setNotificationTitle(title); + setNotificationMessage(message); + setIsLoading(options?.isLoading ?? false); + setShowGalleryButton(options?.showGalleryButton ?? true); + setShowButtons(options?.showButtons ?? true); + setContinueButtonText(options?.continueButtonText ?? 'Продолжить'); + setIsNotificationVisible(true); + }, []); + + /** + * Обработчик нажатия на кнопку "В галерею" + */ + const handleGalleryClick = useCallback(() => { + setIsNotificationVisible(false); + navigate('/gallery'); + }, [navigate]); + + /** + * Обработчик нажатия на кнопку "Продолжить"/"Закрыть"/"Отменить" + */ + const handleContinueClick = useCallback(() => { + setIsNotificationVisible(false); + }, []); + + /** + * Функция для закрытия модального окна и сброса состояния генерации + * Используется для сброса состояния isGenerating при закрытии модального окна + */ + const handleCloseAndReset = useCallback((resetGenerationState: () => void) => { + setIsNotificationVisible(false); + resetGenerationState(); + }, []); + + /** + * Функция для показа уведомления об успешной отправке обратной связи + */ + const showFeedbackSentNotification = useCallback(() => { + showNotification( + 'Спасибо за обратную связь', + 'Ваше сообщение успешно отправлено', + { + isLoading: false, + showGalleryButton: false, + showButtons: true, + continueButtonText: 'Закрыть' + } + ); + }, [showNotification]); + + /** + * Функция для показа уведомления об успешной оплате + */ + const showPaymentSuccessNotification = useCallback((tokens: number, bonusTokens: number) => { + showNotification( + 'Оплата успешна!', + `Вы успешно приобрели ${tokens + bonusTokens} токенов.`, + { + isLoading: false, + showGalleryButton: false, + showButtons: true, + continueButtonText: 'Закрыть' + } + ); + }, [showNotification]); + + return { + // Состояния + isNotificationVisible, + notificationTitle, + notificationMessage, + isLoading, + showGalleryButton, + showButtons, + continueButtonText, + showTokensModal, + missingTokens, + lastPurchasedPack, + + // Методы + setIsNotificationVisible, + setNotificationTitle, + setNotificationMessage, + setIsLoading, + setShowGalleryButton, + setShowButtons, + setContinueButtonText, + setShowTokensModal, + setMissingTokens, + setLastPurchasedPack, + + // Обработчики + handleRequestTimeout, + showErrorModal, + showNotification, + handleGalleryClick, + handleContinueClick, + handleCloseAndReset, + showFeedbackSentNotification, + showPaymentSuccessNotification + }; +}; + +export default useNotifications; diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx index bb9b046..0461d65 100644 --- a/src/screens/Home.tsx +++ b/src/screens/Home.tsx @@ -1,507 +1,109 @@ -import React, { useState, useCallback, useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import BlockRenderer from '../components/blocks/BlockRenderer'; import styles from './Home.module.css'; -import { homeScreenConfig } from '../config/homeScreen'; -import { stylePresets } from '../config/stylePresets'; -import apiService from '../services/api'; -import NotificationModal from '../components/shared/NotificationModal'; import FeedbackHandler, { FeedbackHandlerRef } from '../components/shared/FeedbackHandler'; -import TokenPacksModal from '../components/tokens/TokenPacksModal'; -import { paymentService } from '../services/paymentService'; -import { tokenPacks } from '../constants/tokenPacks'; -import { getCurrentUserId } from '../constants/user'; -import { useBalance } from '../contexts/BalanceContext'; -import { sendTargetEvent } from '../services/analyticsService'; -import { WorkflowType } from '../constants/workflows'; - -// Интерфейс для хранения данных о последней генерации -interface LastGenerationData { - imageData?: string; - style?: string; - presetId?: string; - customPrompt?: string; -} +import NotificationModal from '../components/shared/NotificationModal'; +import useGenerationState from '../hooks/useGenerationState'; +import useImageCheck from '../hooks/useImageCheck'; +import useNotifications from '../hooks/useNotifications'; +import StyleSelector from '../components/generation/StyleSelector'; +import EmotionTypeSelector from '../components/generation/EmotionTypeSelector'; +import MemeSelector from '../components/generation/MemeSelector'; +import PresetSelector from '../components/generation/PresetSelector'; +import GenderSelector from '../components/generation/GenderSelector'; +import GenerationButton from '../components/generation/GenerationButton'; +import TokenPacksModalContainer from '../components/tokens/TokenPacksModalContainer'; +import MainActions from '../components/generation/MainActions'; +import PhotoUpload from '../components/generation/PhotoUpload'; +import BlockRenderer from '../components/blocks/BlockRenderer'; +import { homeScreenConfig } from '../config/homeScreen'; +/** + * Компонент главного экрана приложения + */ const Home: React.FC = () => { const navigate = useNavigate(); const feedbackHandlerRef = useRef(null); - const { updateBalance } = useBalance(); // Используем контекст баланса - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [previewUrl, setPreviewUrl] = useState(() => { - // Проверяем, есть ли превью в состоянии навигации или localStorage - const state = window.history.state?.usr; - return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined; + // Используем хук для управления уведомлениями + const { + isNotificationVisible, + notificationTitle, + notificationMessage, + isLoading, + showGalleryButton, + showButtons, + continueButtonText, + showTokensModal, + missingTokens, + setShowTokensModal, + setMissingTokens, + handleGalleryClick, + handleContinueClick, + handleCloseAndReset, + showNotification, + showFeedbackSentNotification + } = useNotifications(); + + // Используем хук для проверки новых изображений + const { + startImageCheck, + cleanup: cleanupImageCheck + } = useImageCheck((url) => { + updateGeneratedImageUrl(url); }); - - const [imageData, _setImageData] = useState(() => { - const state = window.history.state?.usr; - return state?.imageData || localStorage.getItem('stickerImageData') || undefined; - }); - const [isInputVisible, setIsInputVisible] = useState(false); - const [selectedStyle, setSelectedStyle] = useState('emotions'); // По умолчанию выбран стиль "Эмоции" - const [selectedStyleButtonId, setSelectedStyleButtonId] = useState('emotions'); // Для хранения ID выбранной кнопки стиля - const [selectedPresetId, setSelectedPresetId] = useState(undefined); // Для хранения ID выбранного пресета - const [customPrompt, setCustomPrompt] = useState(''); // Для хранения пользовательского промпта - // Состояния для выбора типа эмоций и мема - const [selectedEmotionType, setSelectedEmotionType] = useState<'memes' | 'prompts' | undefined>('prompts'); // По умолчанию выбран тип "Промпты" - const [selectedEmotionTypeButtonId, setSelectedEmotionTypeButtonId] = useState('prompts'); // Для хранения ID выбранной кнопки типа эмоций - const [selectedMemeId, setSelectedMemeId] = useState(undefined); // Для хранения ID выбранного мема + // Используем хук для управления состоянием генерации + const { + isGenerating, + imageData, + selectedStyle, + selectedStyleButtonId, + selectedPresetId, + isInputVisible, + customPrompt, + selectedEmotionType, + selectedEmotionTypeButtonId, + selectedMemeId, + selectedGenderButtonId, + currentTaskId, + generatedImageUrl, + queuePosition, + setImageData, + handleStyleSelect, + handlePresetSelect, + handleEmotionTypeSelect, + handleMemeSelect, + handleGenderDetectionSelect, + handleGenderSelect, + handleToggleInput, + handleCustomPromptChange, + startGeneration, + resetGenerationState, + updateGeneratedImageUrl + } = useGenerationState( + showNotification, + setShowTokensModal, + setMissingTokens, + startImageCheck + ); - // Состояния для выбора пола - const [genderDetection, setGenderDetection] = useState<'auto' | 'manual'>('auto'); // По умолчанию автоматическое определение пола - const [manualGender, setManualGender] = useState<'man' | 'woman' | undefined>(undefined); // Для хранения выбранного пола - const [selectedGenderButtonId, setSelectedGenderButtonId] = useState('auto'); // Для хранения ID выбранной кнопки пола - - // Состояния для модального окна уведомления - const [isNotificationVisible, setIsNotificationVisible] = useState(false); - const [notificationTitle, setNotificationTitle] = useState(''); - const [notificationMessage, setNotificationMessage] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [showGalleryButton, setShowGalleryButton] = useState(true); - const [showButtons, setShowButtons] = useState(true); - const [continueButtonText, setContinueButtonText] = useState('Продолжить'); - const [lastGenerationData, setLastGenerationData] = useState({}); - const [showTokensModal, setShowTokensModal] = useState(false); - const [missingTokens, setMissingTokens] = useState(0); - const [lastPurchasedPack, setLastPurchasedPack] = useState(null); - - // Новые состояния для отслеживания задачи генерации и изображения - const [currentTaskId, setCurrentTaskId] = useState(undefined); - const [generatedImageUrl, setGeneratedImageUrl] = useState(undefined); - const [checkInterval, setCheckInterval] = useState(null); - const [queuePosition, setQueuePosition] = useState(undefined); - - // Состояния для отслеживания изображений - const [imagesCheckInterval, setImagesCheckInterval] = useState(null); - - // Используем useRef вместо useState для хранения начальных значений - // Это позволит избежать проблемы с асинхронным обновлением состояний - const initialImagesCountRef = useRef(0); - const initialImageIdsRef = useRef([]); - - // Обработчики для модального окна - const handleGalleryClick = useCallback(() => { - setIsNotificationVisible(false); - navigate('/gallery'); - }, [navigate]); - - const handleContinueClick = useCallback(() => { - setIsNotificationVisible(false); - }, []); - - // Функция для проверки новых изображений - const checkForNewImages = useCallback(async () => { - try { - // Получаем текущий список изображений - const images = await apiService.getGeneratedImages(); - - // Проверяем, увеличилось ли количество изображений - if (images.length > initialImagesCountRef.current) { - // Если количество изображений увеличилось, проверяем, что первое изображение действительно новое - const latestImage = images[0]; - const isNewImage = !initialImageIdsRef.current.includes(latestImage.link); - - if (isNewImage) { - // Устанавливаем URL изображения только если это действительно новое изображение - setGeneratedImageUrl(latestImage.url); - - // Очищаем интервал, так как изображение найдено - if (imagesCheckInterval) { - clearInterval(imagesCheckInterval); - setImagesCheckInterval(null); - } - } - } - } catch (error) { - console.error('Ошибка при проверке новых изображений:', error); - } - }, [imagesCheckInterval]); - - const handleBlockAction = useCallback(async (actionType: string, actionValue: string, blockId?: string, buttonId?: string) => { - if (actionType === 'selectStyle') { - // Обработка выбора стиля - setSelectedStyle(actionValue); - setSelectedStyleButtonId(buttonId); - console.log('Selected style:', actionValue, 'Button ID:', buttonId); - return; - } + // Эффект для обработки закрытия приложения + useEffect(() => { + // Обработчик события beforeunload для очистки данных при закрытии приложения + const handleBeforeUnload = () => { + localStorage.removeItem('stickerPreviewUrl'); + localStorage.removeItem('stickerImageData'); + }; - if (actionType === 'selectPreset') { - // Обработка выбора пресета - setSelectedPresetId(buttonId); - console.log('Selected preset:', actionValue, 'Button ID:', buttonId); - - // Если выбран пресет, отличный от "Свой промпт", скрываем поле ввода текста - if (buttonId !== 'customPrompt') { - setIsInputVisible(false); - } - - return; - } + window.addEventListener('beforeunload', handleBeforeUnload); - if (actionType === 'selectGenderDetection') { - // Обработка выбора способа определения пола - setGenderDetection('auto'); - setManualGender(undefined); - setSelectedGenderButtonId(buttonId); - console.log('Selected gender detection:', actionValue, 'Button ID:', buttonId); - return; - } - - if (actionType === 'selectGender') { - // Обработка выбора пола - setGenderDetection('manual'); - setManualGender(actionValue as 'man' | 'woman'); - setSelectedGenderButtonId(buttonId); - console.log('Selected gender:', actionValue, 'Button ID:', buttonId); - return; - } - - if (actionType === 'selectEmotionType') { - // Обработка выбора типа эмоций - setSelectedEmotionType(actionValue as 'memes' | 'prompts'); - setSelectedEmotionTypeButtonId(buttonId); - // Сбрасываем выбранный мем или пресет при смене типа - setSelectedMemeId(undefined); - setSelectedPresetId(undefined); - console.log('Selected emotion type:', actionValue, 'Button ID:', buttonId); - return; - } - - if (actionType === 'selectMeme') { - // Обработка выбора мема - setSelectedMemeId(buttonId); - setSelectedPresetId(buttonId); // Используем тот же ID для совместимости с существующей логикой - console.log('Selected meme:', actionValue, 'Button ID:', buttonId); - return; - } - - if (actionType === 'function') { - if (actionValue === 'startGeneration') { - // Проверка наличия изображения - if (!imageData) { - setNotificationTitle('Внимание'); - setNotificationMessage('Сначала загрузите изображение'); - setIsLoading(false); - setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена - setShowButtons(true); // Показываем кнопки - setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" - setCurrentTaskId(undefined); // Сбрасываем ID задачи, чтобы не отображалась анимация - setQueuePosition(undefined); // Сбрасываем позицию в очереди - setIsNotificationVisible(true); - return; - } - - // Проверка выбора пресета промпта - if (!selectedPresetId) { - setNotificationTitle('Внимание'); - setNotificationMessage('Выберите образ для генерации'); - setIsLoading(false); - setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена - setShowButtons(true); // Показываем кнопки - setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" - setCurrentTaskId(undefined); // Сбрасываем ID задачи, чтобы не отображалась анимация - setQueuePosition(undefined); // Сбрасываем позицию в очереди - setIsNotificationVisible(true); - return; - } - - // Проверка ввода текста, если выбран "Свой промпт" - if (selectedPresetId === 'customPrompt' && !customPrompt.trim()) { - setNotificationTitle('Внимание'); - setNotificationMessage('Введите текст промпта'); - setIsLoading(false); - setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена - setShowButtons(true); // Показываем кнопки - setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" - setCurrentTaskId(undefined); // Сбрасываем ID задачи, чтобы не отображалась анимация - setQueuePosition(undefined); // Сбрасываем позицию в очереди - setIsNotificationVisible(true); - return; - } - - // Добавляем логирование для отладки - console.log('Comparing generations:', { - current: { - imageDataLength: imageData?.length, - style: selectedStyle, - presetId: selectedPresetId, - customPrompt - }, - last: { - imageDataLength: lastGenerationData.imageData?.length, - style: lastGenerationData.style, - presetId: lastGenerationData.presetId, - customPrompt: lastGenerationData.customPrompt - } - }); - - // Проверка на повторную генерацию той же комбинации - const isSameGeneration = - lastGenerationData.imageData === imageData && - lastGenerationData.style === selectedStyle && - lastGenerationData.presetId === selectedPresetId && - (selectedPresetId !== 'customPrompt' || lastGenerationData.customPrompt === customPrompt); - - console.log('Is same generation:', isSameGeneration); - - if (isSameGeneration) { - setNotificationTitle('Внимание'); - setNotificationMessage('Нельзя отправить одну и ту же комбинацию изображения и образа подряд. Пожалуйста, измените изображение или выберите другой образ.'); - setIsLoading(false); - setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена - setShowButtons(true); // Показываем кнопки - setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" - setCurrentTaskId(undefined); // Сбрасываем ID задачи, чтобы не отображалась анимация - setQueuePosition(undefined); // Сбрасываем позицию в очереди - setIsNotificationVisible(true); - return; - } - - try { - // Проверяем баланс перед генерацией - const userTokens = await apiService.getBalance(getCurrentUserId()); - const TOKENS_PER_GENERATION = 10; - - if (userTokens < TOKENS_PER_GENERATION) { - setMissingTokens(TOKENS_PER_GENERATION - userTokens); - setShowTokensModal(true); - return; - } - - // Получаем текущий список изображений перед генерацией - try { - const currentImages = await apiService.getGeneratedImages(); - // Используем ref вместо состояний для мгновенного обновления - initialImagesCountRef.current = currentImages.length; - initialImageIdsRef.current = currentImages.map(img => img.link); - } catch (error) { - console.error('Ошибка при получении списка изображений:', error); - // Продолжаем генерацию даже при ошибке получения списка - } - - // Сбрасываем URL изображения и показываем уведомление о начале генерации - setGeneratedImageUrl(undefined); // Важно: сбрасываем URL изображения перед началом генерации - setNotificationTitle('Генерация стикера'); - setNotificationMessage('Отправка запроса...'); - setIsLoading(true); - setShowGalleryButton(true); - setShowButtons(false); - setContinueButtonText('Продолжить'); - setIsNotificationVisible(true); - - // Если выбран "Свой промпт" и введен текст, используем его - const userPrompt = selectedPresetId === 'customPrompt' && customPrompt ? customPrompt : undefined; - - // Создаем объект с параметрами генерации - const generationOptions: any = { - promptId: selectedPresetId, - userPrompt, - genderDetection, - manualGender - }; - - // Если выбран тип "Мемы", добавляем ID мема - if (selectedStyle === 'emotions' && selectedEmotionType === 'memes' && selectedMemeId) { - generationOptions.memeId = selectedMemeId; - } - - console.log('Generation options:', generationOptions); - - // Определяем тип воркфлоу в зависимости от выбранного стиля и типа эмоций - let workflowType = WorkflowType.CHIBI; - if (selectedStyle === 'chibi') { - workflowType = WorkflowType.CHIBI; - } else if (selectedStyle === 'emotions') { - if (selectedEmotionType === 'memes') { - workflowType = WorkflowType.MEME; - } else { - workflowType = WorkflowType.PROMPT; - } - } else if (selectedStyle === 'realism') { - workflowType = WorkflowType.PROMPT; - } - - console.log('Using workflow type:', workflowType, 'for style:', selectedStyle); - - // Отправляем запрос на генерацию с использованием нового метода - const response = await apiService.generateImageWithWorkflow( - imageData, - workflowType, - generationOptions - ); - console.log('Generation response:', response); - - // Сохраняем данные о текущей генерации - setLastGenerationData({ - imageData, - style: selectedStyle, - presetId: selectedPresetId, - customPrompt: userPrompt - }); - - // Функция для выполнения серии запросов на обновление баланса - const updateBalanceWithRetries = () => { - // Функция для выполнения одной попытки обновления баланса - const fetchBalance = async (attempt: number) => { - try { - console.log(`Попытка ${attempt}/5 обновления баланса после генерации...`); - await updateBalance(); - } catch (error) { - console.error(`Ошибка при обновлении баланса (попытка ${attempt}/5):`, error); - } - }; - - // Выполняем первую попытку сразу - fetchBalance(1); - - // Выполняем остальные попытки с интервалом в 1 секунду - for (let i = 2; i <= 5; i++) { - setTimeout(() => fetchBalance(i), (i - 1) * 1000); - } - }; - - // Запускаем серию запросов на обновление баланса - updateBalanceWithRetries(); - - // Проверяем, была ли ошибка перевода - if (response.translationFailed) { - setNotificationTitle('Недопустимый промпт'); - setNotificationMessage('Промпт содержит недопустимый контент. Пожалуйста, используйте более нейтральные формулировки.'); - - // Логирование деталей ошибки для отладки - if (response.errorDetails) { - console.error('Детали ошибки перевода:', response.errorDetails); - } - - setIsLoading(false); - setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была успешно запущена - setShowButtons(true); // Показываем кнопки в случае ошибки перевода - setContinueButtonText('Закрыть'); // Меняем текст кнопки на "Закрыть" - setCurrentTaskId(undefined); // Сбрасываем ID задачи при ошибке - setQueuePosition(undefined); // Сбрасываем позицию в очереди - return; - } - - // Если нет ошибки перевода, продолжаем обработку результата - if (response.result) { - // Получаем результат - const { result } = response; - - // Обновляем уведомление с информацией о позиции в очереди - if (result.queue_position !== undefined) { - const estimatedTime = apiService.calculateEstimatedWaitTime(result.queue_position); - const minutes = Math.floor(estimatedTime / 60); - const seconds = estimatedTime % 60; - const timeString = minutes > 0 - ? `${minutes} мин ${seconds} сек` - : `${seconds} сек`; - - // Форматируем сообщение в выбранном формате (Вариант 2) - setNotificationMessage( - `Создание стикеров началось!\n` + - `Позиция в очереди: ${result.queue_position}\n` + - `Время ожидания: ${timeString}\n\n` + - `Результат будет доступен в галерее после завершения генерации.` - ); - } else { - setNotificationMessage( - `Создание стикеров началось!\n\n` + - `Результат будет доступен в галерее после завершения генерации.` - ); - } - - // Сохраняем ID задачи - if (result.Task_ID) { - setCurrentTaskId(result.Task_ID); - - // Устанавливаем позицию в очереди - if (result.queue_position !== undefined) { - setQueuePosition(result.queue_position); - } - - // Запускаем интервал для проверки новых изображений - // Очищаем предыдущий интервал, если он был - if (imagesCheckInterval) { - clearInterval(imagesCheckInterval); - } - - // Устанавливаем новый интервал - const intervalId = window.setInterval(() => { - checkForNewImages(); - }, 2000); // Проверяем каждые 2 секунды - - setImagesCheckInterval(intervalId); - } - - // Показываем кнопки и меняем текст кнопки "Продолжить" на "Закрыть" - setShowButtons(true); - setContinueButtonText('Закрыть'); - } - - setIsLoading(false); - } catch (error) { - console.error('Generation failed:', error); - setNotificationTitle('Ошибка'); - setNotificationMessage('Не удалось начать генерацию'); - setIsLoading(false); - setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была успешно запущена - setShowButtons(true); // Показываем кнопки в случае ошибки - setContinueButtonText('Закрыть'); // Меняем текст кнопки на "Закрыть" - setCurrentTaskId(undefined); // Сбрасываем ID задачи при ошибке - setQueuePosition(undefined); // Сбрасываем позицию в очереди - setIsNotificationVisible(true); - } - return; - } - - if (actionValue === 'toggleInput') { - // Добавляем логирование для отладки - console.log('Нажата кнопка "Свой промпт"', { - blockId, - buttonId, - selectedStyle, - selectedEmotionType, - isInputVisible: !isInputVisible // Новое значение - }); - - setIsInputVisible(prev => !prev); - // Устанавливаем selectedPresetId в 'customPrompt' при нажатии на кнопку "Свой промпт" - setSelectedPresetId('customPrompt'); - return; - } - - if (actionValue === 'openTelegramBot') { - // Проверяем, доступен ли объект Telegram - if (window.Telegram && window.Telegram.WebApp) { - window.Telegram.WebApp.openTelegramLink('https://t.me/youtube_s_loader_bot'); - } else { - // Запасной вариант, если API Telegram недоступен - window.open('https://t.me/youtube_s_loader_bot', '_blank'); - } - return; - } - - if (actionValue === 'sendFeedback') { - // Открываем модальное окно обратной связи - feedbackHandlerRef.current?.openFeedbackModal(); - return; - } - } else if (actionType === 'route') { - // Добавляем обработку для действий типа 'route' - navigate(actionValue); - return; - } - - // Если выбрана любая другая кнопка (кроме "Свой промпт"), скрываем поле ввода - if (!(actionType === 'function' && actionValue === 'toggleInput' && buttonId === 'customPrompt')) { - setIsInputVisible(false); - } - }, [navigate, imageData, selectedStyle, selectedPresetId, customPrompt, lastGenerationData, genderDetection, manualGender, selectedEmotionType, selectedMemeId, imagesCheckInterval, checkForNewImages]); - + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + cleanupImageCheck(); + }; + }, [cleanupImageCheck]); + // Эффект для обновления window.history.state при загрузке из localStorage useEffect(() => { // Если есть данные в localStorage, но нет в history.state, обновляем history.state @@ -522,45 +124,32 @@ const Home: React.FC = () => { ); } }, []); + // Обработчик отправки обратной связи + const handleSendFeedback = useCallback(() => { + feedbackHandlerRef.current?.openFeedbackModal(); + }, []); - // Эффект для обработки закрытия приложения - useEffect(() => { - // Обработчик события beforeunload для очистки данных при закрытии приложения - const handleBeforeUnload = () => { - localStorage.removeItem('stickerPreviewUrl'); - localStorage.removeItem('stickerImageData'); - }; - - window.addEventListener('beforeunload', handleBeforeUnload); - - return () => { - window.removeEventListener('beforeunload', handleBeforeUnload); - - // Очищаем интервалы при размонтировании компонента - if (imagesCheckInterval) { - clearInterval(imagesCheckInterval); - } - if (checkInterval) { - clearInterval(checkInterval); - } - }; - }, [imagesCheckInterval, checkInterval]); - - // Функция для получения кнопок в зависимости от блока - const getBlockButtons = useCallback((block: any) => { - if (block.id === 'quickActions') { - // Возвращаем только кнопки из выбранного стиля - return stylePresets[selectedStyle]?.buttons || []; + // Обработчик открытия Telegram бота + const handleOpenTelegramBot = useCallback(() => { + // Проверяем, доступен ли объект Telegram + if (window.Telegram && window.Telegram.WebApp) { + window.Telegram.WebApp.openTelegramLink('https://t.me/youtube_s_loader_bot'); + } else { + // Запасной вариант, если API Telegram недоступен + window.open('https://t.me/youtube_s_loader_bot', '_blank'); } + }, []); + + // Обработчик изменения данных изображения + const handleImageDataChange = useCallback((imageData: string) => { + // Сохраняем данные изображения в состоянии + updateGeneratedImageUrl(undefined); // Сбрасываем URL сгенерированного изображения + setImageData(imageData); - if (block.id === 'emotionPromptsSelection' && selectedStyle === 'emotions' && selectedEmotionType === 'prompts') { - // Возвращаем только кнопки из стиля "Эмоции" - return stylePresets.emotions?.buttons || []; - } - - return block.buttons; - }, [selectedStyle, selectedEmotionType]); - + // Сохраняем данные изображения в localStorage + localStorage.setItem('stickerImageData', imageData); + }, [updateGeneratedImageUrl, setImageData]); + return (
{/* Модальное окно уведомления */} @@ -570,7 +159,7 @@ const Home: React.FC = () => { message={notificationMessage} isLoading={isLoading} onGalleryClick={handleGalleryClick} - onContinueClick={handleContinueClick} + onContinueClick={() => handleCloseAndReset(resetGenerationState)} showGalleryButton={showGalleryButton} showButtons={showButtons} continueButtonText={continueButtonText} @@ -584,243 +173,86 @@ const Home: React.FC = () => { {/* Компонент обработки обратной связи */} { - // Показываем уведомление об успешной отправке - setNotificationTitle('Спасибо за обратную связь'); - setNotificationMessage('Ваше сообщение успешно отправлено'); - setIsLoading(false); - setShowGalleryButton(false); // Скрываем кнопку "В галерею" для уведомления об обратной связи - setShowButtons(true); // Показываем кнопки - setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" - setIsNotificationVisible(true); - }} + onFeedbackSent={showFeedbackSentNotification} + /> + + {/* Модальное окно с пакетами токенов */} + setShowTokensModal(false)} + missingTokens={missingTokens} + onSuccess={startGeneration} />
- {/* Модальное окно с пакетами токенов */} - setShowTokensModal(false)} - onShowAllPacks={() => navigate('/profile')} - missingTokens={missingTokens} - onBuyPack={(packId: string) => { - const pack = tokenPacks.find(p => p.id === packId); - if (!pack) return; - - setShowTokensModal(false); - setLastPurchasedPack(pack); - - paymentService.showBuyTokensPopup(pack, async (userData) => { - if (userData) { - // Функция для выполнения серии запросов на обновление баланса - const updateBalanceWithRetries = () => { - // Функция для выполнения одной попытки обновления баланса - const fetchBalance = async (attempt: number) => { - try { - console.log(`Попытка ${attempt}/5 обновления баланса после пополнения...`); - await updateBalance(); - } catch (error) { - console.error(`Ошибка при обновлении баланса (попытка ${attempt}/5):`, error); - } - }; - - // Выполняем первую попытку сразу - fetchBalance(1); - - // Выполняем остальные попытки с интервалом в 1 секунду - for (let i = 2; i <= 5; i++) { - setTimeout(() => fetchBalance(i), (i - 1) * 1000); - } - }; - - // Запускаем серию запросов на обновление баланса - updateBalanceWithRetries(); - - // Показываем модальное окно с информацией об успешной оплате - setNotificationTitle('Оплата успешна!'); - setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов.`); - setIsLoading(false); - setShowGalleryButton(false); - setShowButtons(true); - setContinueButtonText('Закрыть'); - setIsNotificationVisible(true); - } else { - // Если данные не получены, делаем запрос на получение данных пользователя - try { - // Получаем баланс пользователя - const balance = await apiService.getBalance(getCurrentUserId()); - - // Функция для выполнения серии запросов на обновление баланса - const updateBalanceWithRetries = () => { - // Функция для выполнения одной попытки обновления баланса - const fetchBalance = async (attempt: number) => { - try { - console.log(`Попытка ${attempt}/5 обновления баланса после пополнения...`); - await updateBalance(); - } catch (error) { - console.error(`Ошибка при обновлении баланса (попытка ${attempt}/5):`, error); - } - }; - - // Выполняем первую попытку сразу - fetchBalance(1); - - // Выполняем остальные попытки с интервалом в 1 секунду - for (let i = 2; i <= 5; i++) { - setTimeout(() => fetchBalance(i), (i - 1) * 1000); - } - }; - - // Запускаем серию запросов на обновление баланса - updateBalanceWithRetries(); - - // Показываем модальное окно с информацией об успешной оплате - setNotificationTitle('Оплата успешна!'); - setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов. Ваш текущий баланс: ${balance} токенов.`); - setIsLoading(false); - setShowGalleryButton(false); - setShowButtons(true); - setContinueButtonText('Закрыть'); - setIsNotificationVisible(true); - } catch (error) { - console.error('Ошибка при обновлении данных пользователя:', error); - } - } - }); - }} - /> - {/* Блоки из конфигурации */}
- {(() => { - // Получаем все блоки из конфигурации - const allBlocks = homeScreenConfig.homeScreen.blocks.filter(block => block.type !== 'generateButton'); + {/* Верхний блок с кнопками */} + - // Создаем массив блоков для отображения в нужном порядке - const blocksToRender = []; + {/* Блок загрузки фото */} + - // Проходим по всем блокам и добавляем их в массив для отображения в нужном порядке - for (const block of allBlocks) { - // Блоки для выбора типа эмоций показываем только если выбран стиль "Эмоции" - if (block.id === 'emotionTypeTitle' || block.id === 'emotionTypeSelection') { - if (selectedStyle === 'emotions') { - blocksToRender.push(block); - } - continue; - } - - // Блок с полем ввода текста (customPrompt) показываем после emotionTypeSelection и перед emotionPromptsSelection - // если выбран стиль "Эмоции" и тип "Промпты" и нажата кнопка "Свой промпт" - if (block.id === 'customPrompt') { - // Для стиля "Чиби" показываем в обычном порядке - if (selectedStyle === 'chibi' && isInputVisible) { - blocksToRender.push(block); - } - // Для стиля "Эмоции" и типа "Промпты" блок будет добавлен позже в специальном месте - continue; - } - - // Блок выбора мемов показываем только если выбран стиль "Эмоции" и тип "Мемы" - if (block.id === 'memeSelection') { - if (selectedStyle === 'emotions' && selectedEmotionType === 'memes') { - blocksToRender.push(block); - } - continue; - } - - // Блок выбора промптов для эмоций показываем только если выбран стиль "Эмоции" и тип "Промпты" - if (block.id === 'emotionPromptsSelection') { - if (selectedStyle === 'emotions' && selectedEmotionType === 'prompts') { - // Если выбран стиль "Эмоции" и тип "Промпты", то сначала добавляем поле ввода текста, - // если оно должно быть видимым - if (isInputVisible) { - const textInputBlock = allBlocks.find(b => b.id === 'customPrompt'); - if (textInputBlock) { - blocksToRender.push(textInputBlock); - } - } - - // Затем добавляем блок с промптами - blocksToRender.push(block); - } - continue; - } - - // Блок с пресетами (quickActions) показываем только если выбран стиль "Чиби" или не выбран стиль "Эмоции" - if (block.id === 'quickActions') { - if (selectedStyle !== 'emotions') { - blocksToRender.push(block); - } - continue; - } - - // Блок с заголовком "Выбери образ" (step3) показываем только если не выбран стиль "Эмоции" или уже выбран тип эмоций - if (block.id === 'step3') { - if (selectedStyle !== 'emotions' || selectedEmotionType !== undefined) { - blocksToRender.push(block); - } - continue; - } - - // Остальные блоки показываем всегда - blocksToRender.push(block); - } + {/* Компонент выбора пола */} + - // Отображаем блоки - return blocksToRender.map((block) => { - // Создаем копию блока с модифицированными кнопками - const modifiedBlock = { - ...block, - buttons: getBlockButtons(block) - }; - - // Определяем, какой ID выбранной кнопки передавать в зависимости от типа блока - let selectedButtonId; - if (block.id === 'styleActions') { - // Для блока стилей передаем ID кнопки выбранного стиля - selectedButtonId = selectedStyleButtonId; - } else if (block.id === 'quickActions') { - // Для блока пресетов передаем ID выбранного пресета - selectedButtonId = selectedPresetId; - } else if (block.id === 'genderSelection') { - // Для блока выбора пола передаем ID выбранной кнопки пола - selectedButtonId = selectedGenderButtonId; - } else if (block.id === 'emotionTypeSelection') { - // Для блока выбора типа эмоций передаем ID выбранной кнопки типа эмоций - selectedButtonId = selectedEmotionTypeButtonId; - } else if (block.id === 'memeSelection') { - // Для блока выбора мема передаем ID выбранного мема - selectedButtonId = selectedMemeId; - } else if (block.id === 'emotionPromptsSelection') { - // Для блока выбора промптов для эмоций передаем ID выбранного пресета - selectedButtonId = selectedPresetId; - } - - return ( - + + {/* Заголовок "3 выбери образ" */} + {(() => { + const titleBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'step3'); + return titleBlock ? ( + {}} /> - ); - }); - })()} + ) : null; + })()} + + {/* Компонент выбора типа эмоций */} + + + {/* Компонент выбора мема или пресета в зависимости от выбранного типа эмоций */} + {selectedStyle === 'emotions' && selectedEmotionType === 'memes' ? ( + + ) : ( + + )}
- {homeScreenConfig.homeScreen.blocks - .filter(block => block.type === 'generateButton') - .map((block) => ( -
- -
- ))} + + {/* Компонент кнопки генерации */} +
); diff --git a/src/types/generation.ts b/src/types/generation.ts new file mode 100644 index 0000000..b92280a --- /dev/null +++ b/src/types/generation.ts @@ -0,0 +1,83 @@ +import { WorkflowType } from '../constants/workflows'; + +/** + * Интерфейс для хранения данных о последней генерации + */ +export interface LastGenerationData { + imageData?: string; + style?: string; + presetId?: string; + customPrompt?: string; +} + +/** + * Параметры для генерации изображения + */ +export interface GenerationOptions { + promptId?: string; + userPrompt?: string; + genderDetection: 'auto' | 'manual'; + manualGender?: 'man' | 'woman'; + memeId?: string; +} + +/** + * Результат генерации изображения + */ +export interface GenerationResult { + Task_ID?: string; + queue_position?: number; + translationFailed?: boolean; + errorDetails?: string; +} + +/** + * Состояние генерации + */ +export interface GenerationState { + isGenerating: boolean; + requestTimeoutId: ReturnType | null; + previewUrl?: string; + imageData?: string; + selectedStyle: string; + selectedStyleButtonId?: string; + selectedPresetId?: string; + isInputVisible: boolean; + customPrompt: string; + selectedEmotionType?: 'memes' | 'prompts'; + selectedEmotionTypeButtonId?: string; + selectedMemeId?: string; + genderDetection: 'auto' | 'manual'; + manualGender?: 'man' | 'woman'; + selectedGenderButtonId?: string; + currentTaskId?: string; + generatedImageUrl?: string; + checkInterval: number | null; + queuePosition?: number; + lastGenerationData: LastGenerationData; +} + +/** + * Состояние проверки изображений + */ +export interface ImageCheckState { + imagesCheckInterval: number | null; + initialImagesCount: number; + initialImageIds: string[]; +} + +/** + * Состояние уведомлений + */ +export interface NotificationState { + isNotificationVisible: boolean; + notificationTitle: string; + notificationMessage: string; + isLoading: boolean; + showGalleryButton: boolean; + showButtons: boolean; + continueButtonText: string; + showTokensModal: boolean; + missingTokens: number; + lastPurchasedPack: any; +} diff --git a/src/utils/balanceUtils.ts b/src/utils/balanceUtils.ts new file mode 100644 index 0000000..ab24214 --- /dev/null +++ b/src/utils/balanceUtils.ts @@ -0,0 +1,76 @@ +import apiService from '../services/api'; +import { getCurrentUserId } from '../constants/user'; + +/** + * Константа для количества токенов, необходимых для одной генерации + */ +export const TOKENS_PER_GENERATION = 10; + +/** + * Проверяет, достаточно ли у пользователя токенов для генерации + * @returns Объект с результатом проверки + */ +export const checkSufficientBalance = async (): Promise<{ + isEnough: boolean; + missingTokens: number; + currentBalance: number; +}> => { + try { + // Получаем текущий баланс пользователя + const userTokens = await apiService.getBalance(getCurrentUserId()); + + // Проверяем, достаточно ли токенов для генерации + if (userTokens < TOKENS_PER_GENERATION) { + return { + isEnough: false, + missingTokens: TOKENS_PER_GENERATION - userTokens, + currentBalance: userTokens + }; + } + + return { + isEnough: true, + missingTokens: 0, + currentBalance: userTokens + }; + } catch (error) { + console.error('Ошибка при проверке баланса:', error); + + // В случае ошибки возвращаем объект с флагом isEnough = false + return { + isEnough: false, + missingTokens: TOKENS_PER_GENERATION, + currentBalance: 0 + }; + } +}; + +/** + * Обновляет баланс пользователя с повторными попытками + * @param updateBalanceCallback Функция для обновления баланса + * @param attempts Количество попыток + * @param interval Интервал между попытками в миллисекундах + */ +export const updateBalanceWithRetries = ( + updateBalanceCallback: () => Promise, + attempts = 5, + interval = 1000 +): void => { + // Функция для выполнения одной попытки обновления баланса + const fetchBalance = async (attempt: number) => { + try { + console.log(`Попытка ${attempt}/${attempts} обновления баланса...`); + await updateBalanceCallback(); + } catch (error) { + console.error(`Ошибка при обновлении баланса (попытка ${attempt}/${attempts}):`, error); + } + }; + + // Выполняем первую попытку сразу + fetchBalance(1); + + // Выполняем остальные попытки с заданным интервалом + for (let i = 2; i <= attempts; i++) { + setTimeout(() => fetchBalance(i), (i - 1) * interval); + } +}; diff --git a/src/utils/generationUtils.ts b/src/utils/generationUtils.ts new file mode 100644 index 0000000..e7d71a0 --- /dev/null +++ b/src/utils/generationUtils.ts @@ -0,0 +1,143 @@ +import { WorkflowType } from '../constants/workflows'; +import { GenerationOptions } from '../types/generation'; + +/** + * Определяет тип воркфлоу в зависимости от выбранного стиля и типа эмоций + * @param style Выбранный стиль + * @param emotionType Выбранный тип эмоций + * @returns Тип воркфлоу + */ +export const determineWorkflowType = ( + style: string, + emotionType?: 'memes' | 'prompts' +): WorkflowType => { + if (style === 'chibi') { + return WorkflowType.CHIBI; + } else if (style === 'emotions') { + if (emotionType === 'memes') { + return WorkflowType.MEME; + } else { + return WorkflowType.PROMPT; + } + } else if (style === 'realism') { + return WorkflowType.PROMPT; + } + + // По умолчанию возвращаем PROMPT + return WorkflowType.PROMPT; +}; + +/** + * Подготавливает параметры для генерации + * @param presetId ID выбранного пресета + * @param customPrompt Пользовательский промпт + * @param genderDetection Способ определения пола + * @param manualGender Выбранный пол + * @param memeId ID выбранного мема + * @returns Объект с параметрами генерации + */ +export const prepareGenerationOptions = ( + presetId?: string, + customPrompt?: string, + genderDetection: 'auto' | 'manual' = 'auto', + manualGender?: 'man' | 'woman', + memeId?: string +): GenerationOptions => { + const options: GenerationOptions = { + genderDetection + }; + + // Добавляем ID пресета, если он выбран + if (presetId) { + options.promptId = presetId; + } + + // Добавляем пользовательский промпт, если он введен + if (presetId === 'customPrompt' && customPrompt) { + options.userPrompt = customPrompt; + } + + // Добавляем выбранный пол, если выбран ручной способ определения пола + if (genderDetection === 'manual' && manualGender) { + options.manualGender = manualGender; + } + + // Добавляем ID мема, если он выбран + if (memeId) { + options.memeId = memeId; + } + + return options; +}; + +/** + * Проверяет валидность параметров генерации + * @param imageData Данные изображения + * @param presetId ID выбранного пресета + * @param customPrompt Пользовательский промпт + * @returns Объект с результатом проверки + */ +export const validateGenerationParams = ( + imageData?: string, + presetId?: string, + customPrompt?: string +): { isValid: boolean; errorTitle?: string; errorMessage?: string } => { + // Проверка наличия изображения + if (!imageData) { + return { + isValid: false, + errorTitle: 'Внимание', + errorMessage: 'Сначала загрузите изображение' + }; + } + + // Проверка выбора пресета промпта + if (!presetId) { + return { + isValid: false, + errorTitle: 'Внимание', + errorMessage: 'Выберите образ для генерации' + }; + } + + // Проверка ввода текста, если выбран "Свой промпт" + if (presetId === 'customPrompt' && (!customPrompt || !customPrompt.trim())) { + return { + isValid: false, + errorTitle: 'Внимание', + errorMessage: 'Введите текст промпта' + }; + } + + return { isValid: true }; +}; + +/** + * Проверяет, является ли текущая генерация повторной + * @param currentData Текущие данные генерации + * @param lastData Данные последней генерации + * @returns true, если генерация повторная, иначе false + */ +export const isSameGeneration = ( + currentData: { + imageData?: string; + style?: string; + presetId?: string; + customPrompt?: string; + }, + lastData: { + imageData?: string; + style?: string; + presetId?: string; + customPrompt?: string; + } +): boolean => { + // Проверяем, совпадают ли все параметры генерации + const isSame = + currentData.imageData === lastData.imageData && + currentData.style === lastData.style && + currentData.presetId === lastData.presetId && + (currentData.presetId !== 'customPrompt' || currentData.customPrompt === lastData.customPrompt); + + return isSame; +};