Fix image loading issues in buttons by enhancing ImageWithFallback component with button variant

This commit is contained in:
kazachilo 2025-04-11 13:54:13 +03:00
parent 9f9daf0803
commit 825be2d0a6
19 changed files with 1755 additions and 763 deletions

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { BlockButton } from '../../types/blocks'; import { BlockButton } from '../../types/blocks';
import ImageWithFallback from '../shared/ImageWithFallback';
import styles from './SquareButton.module.css'; import styles from './SquareButton.module.css';
interface SquareButtonProps extends BlockButton { interface SquareButtonProps extends BlockButton {
@ -87,7 +88,14 @@ const SquareButton: React.FC<SquareButtonProps> = ({
data-selected={isSelected ? 'true' : 'false'} /* Добавляем data-атрибут для дополнительной стилизации */ data-selected={isSelected ? 'true' : 'false'} /* Добавляем data-атрибут для дополнительной стилизации */
> >
{imageUrl ? ( {imageUrl ? (
<img src={imageUrl} alt={title} className={styles.iconImage} /> <ImageWithFallback
src={imageUrl}
alt={title}
className={styles.iconImage}
maxRetries={3} // Увеличиваем количество повторных попыток для кнопок
showErrorUI={false} // Отключаем отображение UI с ошибкой
variant="button" // Используем вариант для кнопок
/>
) : icon ? ( ) : icon ? (
<span className={styles.icon}>{icon}</span> <span className={styles.icon}>{icon}</span>
) : null} ) : null}

View File

@ -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<EmotionTypeSelectorProps> = ({
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 (
<BlockRenderer
block={selectorBlock}
onAction={handleAction}
selectedButtonId={selectedEmotionTypeButtonId}
/>
);
};
export default EmotionTypeSelector;

View File

@ -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<GenderSelectorProps> = ({
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 (
<BlockRenderer
block={genderBlock}
onAction={handleAction}
selectedButtonId={selectedGenderButtonId}
/>
);
};
export default GenderSelector;

View File

@ -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<GenerationButtonProps> = ({
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 (
<div className={styles.generateButtonContainer}>
<BlockRenderer
block={generateButton}
onAction={handleAction}
extraProps={{ disabled: isGenerating }}
/>
</div>
);
};
export default GenerationButton;

View File

@ -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<MainActionsProps> = ({
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 (
<>
<BlockRenderer
block={actionsBlock}
onAction={handleAction}
/>
<BlockRenderer
block={dividerBlock}
onAction={() => {}}
/>
</>
);
};
export default MainActions;

View File

@ -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<MemeSelectorProps> = ({
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 (
<BlockRenderer
block={memeBlock}
onAction={handleAction}
selectedButtonId={selectedMemeId}
/>
);
};
export default MemeSelector;

View File

@ -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<PhotoUploadProps> = ({
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 (
<>
<BlockRenderer
block={titleBlock}
onAction={() => {}}
/>
<BlockRenderer
block={uploadBlock}
onAction={handleAction}
/>
</>
);
};
export default PhotoUpload;

View File

@ -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<PresetSelectorProps> = ({
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 && (
<BlockRenderer
block={modifiedBlock}
onAction={handleAction}
selectedButtonId={selectedPresetId}
/>
)}
{customPromptBlock && isInputVisible && (
<BlockRenderer
block={customPromptBlock}
onAction={() => {}}
extraProps={{
visible: isInputVisible,
onTextChange: onCustomPromptChange
}}
/>
)}
</>
);
};
export default PresetSelector;

View File

@ -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<StyleSelectorProps> = ({
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 (
<>
<BlockRenderer
block={titleBlock}
onAction={() => {}}
/>
<BlockRenderer
block={styleBlock}
onAction={handleAction}
selectedButtonId={selectedStyleButtonId}
/>
</>
);
};
export default StyleSelector;

View File

@ -12,6 +12,11 @@
user-select: none; user-select: none;
} }
/* Специальный контейнер для кнопок - без фона */
.buttonContainer {
background-color: transparent;
}
.image { .image {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -22,6 +27,18 @@
user-drag: none; user-drag: none;
} }
/* Специальный стиль для изображений в кнопках */
.buttonImage {
position: absolute;
top: 1%;
left: 0%;
width: 100%;
height: 100%;
object-fit: contain;
transform: scale(1);
transform-origin: center;
}
.hidden { .hidden {
opacity: 0; opacity: 0;
position: absolute; position: absolute;

View File

@ -9,6 +9,8 @@ interface ImageWithFallbackProps {
onContextMenu?: (e: React.MouseEvent<HTMLDivElement>) => void; onContextMenu?: (e: React.MouseEvent<HTMLDivElement>) => void;
maxRetries?: number; maxRetries?: number;
isDeleteMode?: boolean; isDeleteMode?: boolean;
showErrorUI?: boolean; // Показывать ли UI с ошибкой
variant?: 'default' | 'button'; // Вариант отображения: обычный или для кнопок
} }
const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
@ -18,7 +20,9 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
onClick, onClick,
onContextMenu, onContextMenu,
maxRetries = 2, // По умолчанию 2 попытки автоматической перезагрузки maxRetries = 2, // По умолчанию 2 попытки автоматической перезагрузки
isDeleteMode = false isDeleteMode = false,
showErrorUI = true, // По умолчанию показываем UI с ошибкой
variant = 'default' // По умолчанию обычный вариант отображения
}) => { }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
@ -111,12 +115,12 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
return ( return (
<div <div
className={`${styles.container} ${className}`} className={`${styles.container} ${variant === 'button' ? styles.buttonContainer : ''} ${className}`}
onClick={handleClick} onClick={handleClick}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
> >
{/* Показываем индикатор загрузки, если изображение загружается */} {/* Показываем индикатор загрузки, если изображение загружается */}
{loading && ( {loading && variant !== 'button' && (
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>
<div className={styles.spinner}></div> <div className={styles.spinner}></div>
</div> </div>
@ -128,13 +132,13 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
src={imageSrc} src={imageSrc}
alt={alt} alt={alt}
draggable="false" draggable="false"
className={`${styles.image} ${error ? styles.hidden : ''}`} className={`${styles.image} ${variant === 'button' ? styles.buttonImage : ''} ${error ? styles.hidden : ''}`}
onLoad={handleLoad} onLoad={handleLoad}
onError={handleError} onError={handleError}
/> />
{/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка */} {/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка и showErrorUI=true */}
{error && ( {error && showErrorUI && (
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<div className={styles.errorIcon}>!</div> <div className={styles.errorIcon}>!</div>
<div className={styles.errorMessage}>Ошибка загрузки</div> <div className={styles.errorMessage}>Ошибка загрузки</div>

View File

@ -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<TokenPacksModalContainerProps> = ({
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 (
<TokenPacksModal
isVisible={isVisible}
onClose={onClose}
onShowAllPacks={handleShowAllPacks}
missingTokens={missingTokens}
onBuyPack={handleBuyPack}
/>
);
};
export default TokenPacksModalContainer;

View File

@ -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<ReturnType<typeof setTimeout> | null>(null);
// Состояние для превью и данных изображения
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
// Проверяем, есть ли превью в состоянии навигации или localStorage
const state = window.history.state?.usr;
return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined;
});
const [imageData, setImageData] = useState<string | undefined>(() => {
const state = window.history.state?.usr;
return state?.imageData || localStorage.getItem('stickerImageData') || undefined;
});
// Состояния для выбора стиля и пресета
const [selectedStyle, setSelectedStyle] = useState<string>('emotions');
const [selectedStyleButtonId, setSelectedStyleButtonId] = useState<string | undefined>('emotions');
const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(undefined);
const [isInputVisible, setIsInputVisible] = useState(false);
const [customPrompt, setCustomPrompt] = useState<string>('');
// Состояния для выбора типа эмоций и мема
const [selectedEmotionType, setSelectedEmotionType] = useState<'memes' | 'prompts' | undefined>('prompts');
const [selectedEmotionTypeButtonId, setSelectedEmotionTypeButtonId] = useState<string | undefined>('prompts');
const [selectedMemeId, setSelectedMemeId] = useState<string | undefined>(undefined);
// Состояния для выбора пола
const [genderDetection, setGenderDetection] = useState<'auto' | 'manual'>('auto');
const [manualGender, setManualGender] = useState<'man' | 'woman' | undefined>(undefined);
const [selectedGenderButtonId, setSelectedGenderButtonId] = useState<string | undefined>('auto');
// Состояния для отслеживания задачи генерации и изображения
const [currentTaskId, setCurrentTaskId] = useState<string | undefined>(undefined);
const [generatedImageUrl, setGeneratedImageUrl] = useState<string | undefined>(undefined);
const [checkInterval, setCheckInterval] = useState<number | null>(null);
const [queuePosition, setQueuePosition] = useState<number | undefined>(undefined);
// Состояние для хранения данных о последней генерации
const [lastGenerationData, setLastGenerationData] = useState<LastGenerationData>({});
/**
* Обработчик выбора стиля
*/
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<typeof setTimeout> | 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;

121
src/hooks/useImageCheck.ts Normal file
View File

@ -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<number | null>(null);
// Используем useRef вместо useState для хранения начальных значений
// Это позволит избежать проблемы с асинхронным обновлением состояний
const initialImagesCountRef = useRef<number>(0);
const initialImageIdsRef = useRef<string[]>([]);
/**
* Функция для проверки новых изображений
*/
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;

View File

@ -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<any>(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;

View File

@ -1,506 +1,108 @@
import React, { useState, useCallback, useEffect, useRef } from 'react'; import React, { useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import BlockRenderer from '../components/blocks/BlockRenderer';
import styles from './Home.module.css'; 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 FeedbackHandler, { FeedbackHandlerRef } from '../components/shared/FeedbackHandler';
import TokenPacksModal from '../components/tokens/TokenPacksModal'; import NotificationModal from '../components/shared/NotificationModal';
import { paymentService } from '../services/paymentService'; import useGenerationState from '../hooks/useGenerationState';
import { tokenPacks } from '../constants/tokenPacks'; import useImageCheck from '../hooks/useImageCheck';
import { getCurrentUserId } from '../constants/user'; import useNotifications from '../hooks/useNotifications';
import { useBalance } from '../contexts/BalanceContext'; import StyleSelector from '../components/generation/StyleSelector';
import { sendTargetEvent } from '../services/analyticsService'; import EmotionTypeSelector from '../components/generation/EmotionTypeSelector';
import { WorkflowType } from '../constants/workflows'; import MemeSelector from '../components/generation/MemeSelector';
import PresetSelector from '../components/generation/PresetSelector';
// Интерфейс для хранения данных о последней генерации import GenderSelector from '../components/generation/GenderSelector';
interface LastGenerationData { import GenerationButton from '../components/generation/GenerationButton';
imageData?: string; import TokenPacksModalContainer from '../components/tokens/TokenPacksModalContainer';
style?: string; import MainActions from '../components/generation/MainActions';
presetId?: string; import PhotoUpload from '../components/generation/PhotoUpload';
customPrompt?: string; import BlockRenderer from '../components/blocks/BlockRenderer';
} import { homeScreenConfig } from '../config/homeScreen';
/**
* Компонент главного экрана приложения
*/
const Home: React.FC = () => { const Home: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const feedbackHandlerRef = useRef<FeedbackHandlerRef>(null); const feedbackHandlerRef = useRef<FeedbackHandlerRef>(null);
const { updateBalance } = useBalance(); // Используем контекст баланса
// eslint-disable-next-line @typescript-eslint/no-unused-vars // Используем хук для управления уведомлениями
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => { const {
// Проверяем, есть ли превью в состоянии навигации или localStorage isNotificationVisible,
const state = window.history.state?.usr; notificationTitle,
return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined; 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<string | undefined>(() => { // Используем хук для управления состоянием генерации
const state = window.history.state?.usr; const {
return state?.imageData || localStorage.getItem('stickerImageData') || undefined; isGenerating,
}); imageData,
const [isInputVisible, setIsInputVisible] = useState(false); selectedStyle,
const [selectedStyle, setSelectedStyle] = useState<string>('emotions'); // По умолчанию выбран стиль "Эмоции" selectedStyleButtonId,
const [selectedStyleButtonId, setSelectedStyleButtonId] = useState<string | undefined>('emotions'); // Для хранения ID выбранной кнопки стиля selectedPresetId,
const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(undefined); // Для хранения ID выбранного пресета isInputVisible,
const [customPrompt, setCustomPrompt] = useState<string>(''); // Для хранения пользовательского промпта 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 [selectedEmotionType, setSelectedEmotionType] = useState<'memes' | 'prompts' | undefined>('prompts'); // По умолчанию выбран тип "Промпты" useEffect(() => {
const [selectedEmotionTypeButtonId, setSelectedEmotionTypeButtonId] = useState<string | undefined>('prompts'); // Для хранения ID выбранной кнопки типа эмоций // Обработчик события beforeunload для очистки данных при закрытии приложения
const [selectedMemeId, setSelectedMemeId] = useState<string | undefined>(undefined); // Для хранения ID выбранного мема const handleBeforeUnload = () => {
localStorage.removeItem('stickerPreviewUrl');
localStorage.removeItem('stickerImageData');
};
// Состояния для выбора пола window.addEventListener('beforeunload', handleBeforeUnload);
const [genderDetection, setGenderDetection] = useState<'auto' | 'manual'>('auto'); // По умолчанию автоматическое определение пола
const [manualGender, setManualGender] = useState<'man' | 'woman' | undefined>(undefined); // Для хранения выбранного пола
const [selectedGenderButtonId, setSelectedGenderButtonId] = useState<string | undefined>('auto'); // Для хранения ID выбранной кнопки пола
// Состояния для модального окна уведомления return () => {
const [isNotificationVisible, setIsNotificationVisible] = useState(false); window.removeEventListener('beforeunload', handleBeforeUnload);
const [notificationTitle, setNotificationTitle] = useState(''); cleanupImageCheck();
const [notificationMessage, setNotificationMessage] = useState(''); };
const [isLoading, setIsLoading] = useState(false); }, [cleanupImageCheck]);
const [showGalleryButton, setShowGalleryButton] = useState(true);
const [showButtons, setShowButtons] = useState(true);
const [continueButtonText, setContinueButtonText] = useState('Продолжить');
const [lastGenerationData, setLastGenerationData] = useState<LastGenerationData>({});
const [showTokensModal, setShowTokensModal] = useState(false);
const [missingTokens, setMissingTokens] = useState(0);
const [lastPurchasedPack, setLastPurchasedPack] = useState<any>(null);
// Новые состояния для отслеживания задачи генерации и изображения
const [currentTaskId, setCurrentTaskId] = useState<string | undefined>(undefined);
const [generatedImageUrl, setGeneratedImageUrl] = useState<string | undefined>(undefined);
const [checkInterval, setCheckInterval] = useState<number | null>(null);
const [queuePosition, setQueuePosition] = useState<number | undefined>(undefined);
// Состояния для отслеживания изображений
const [imagesCheckInterval, setImagesCheckInterval] = useState<number | null>(null);
// Используем useRef вместо useState для хранения начальных значений
// Это позволит избежать проблемы с асинхронным обновлением состояний
const initialImagesCountRef = useRef<number>(0);
const initialImageIdsRef = useRef<string[]>([]);
// Обработчики для модального окна
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;
}
if (actionType === 'selectPreset') {
// Обработка выбора пресета
setSelectedPresetId(buttonId);
console.log('Selected preset:', actionValue, 'Button ID:', buttonId);
// Если выбран пресет, отличный от "Свой промпт", скрываем поле ввода текста
if (buttonId !== 'customPrompt') {
setIsInputVisible(false);
}
return;
}
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]);
// Эффект для обновления window.history.state при загрузке из localStorage // Эффект для обновления window.history.state при загрузке из localStorage
useEffect(() => { useEffect(() => {
@ -522,44 +124,31 @@ const Home: React.FC = () => {
); );
} }
}, []); }, []);
// Обработчик отправки обратной связи
const handleSendFeedback = useCallback(() => {
feedbackHandlerRef.current?.openFeedbackModal();
}, []);
// Эффект для обработки закрытия приложения // Обработчик открытия Telegram бота
useEffect(() => { const handleOpenTelegramBot = useCallback(() => {
// Обработчик события beforeunload для очистки данных при закрытии приложения // Проверяем, доступен ли объект Telegram
const handleBeforeUnload = () => { if (window.Telegram && window.Telegram.WebApp) {
localStorage.removeItem('stickerPreviewUrl'); window.Telegram.WebApp.openTelegramLink('https://t.me/youtube_s_loader_bot');
localStorage.removeItem('stickerImageData'); } else {
}; // Запасной вариант, если API Telegram недоступен
window.open('https://t.me/youtube_s_loader_bot', '_blank');
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 || [];
} }
}, []);
if (block.id === 'emotionPromptsSelection' && selectedStyle === 'emotions' && selectedEmotionType === 'prompts') { // Обработчик изменения данных изображения
// Возвращаем только кнопки из стиля "Эмоции" const handleImageDataChange = useCallback((imageData: string) => {
return stylePresets.emotions?.buttons || []; // Сохраняем данные изображения в состоянии
} updateGeneratedImageUrl(undefined); // Сбрасываем URL сгенерированного изображения
setImageData(imageData);
return block.buttons; // Сохраняем данные изображения в localStorage
}, [selectedStyle, selectedEmotionType]); localStorage.setItem('stickerImageData', imageData);
}, [updateGeneratedImageUrl, setImageData]);
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -570,7 +159,7 @@ const Home: React.FC = () => {
message={notificationMessage} message={notificationMessage}
isLoading={isLoading} isLoading={isLoading}
onGalleryClick={handleGalleryClick} onGalleryClick={handleGalleryClick}
onContinueClick={handleContinueClick} onContinueClick={() => handleCloseAndReset(resetGenerationState)}
showGalleryButton={showGalleryButton} showGalleryButton={showGalleryButton}
showButtons={showButtons} showButtons={showButtons}
continueButtonText={continueButtonText} continueButtonText={continueButtonText}
@ -584,243 +173,86 @@ const Home: React.FC = () => {
{/* Компонент обработки обратной связи */} {/* Компонент обработки обратной связи */}
<FeedbackHandler <FeedbackHandler
ref={feedbackHandlerRef} ref={feedbackHandlerRef}
onFeedbackSent={() => { onFeedbackSent={showFeedbackSentNotification}
// Показываем уведомление об успешной отправке />
setNotificationTitle('Спасибо за обратную связь');
setNotificationMessage('Ваше сообщение успешно отправлено'); {/* Модальное окно с пакетами токенов */}
setIsLoading(false); <TokenPacksModalContainer
setShowGalleryButton(false); // Скрываем кнопку "В галерею" для уведомления об обратной связи isVisible={showTokensModal}
setShowButtons(true); // Показываем кнопки onClose={() => setShowTokensModal(false)}
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" missingTokens={missingTokens}
setIsNotificationVisible(true); onSuccess={startGeneration}
}}
/> />
<div className={styles.content}> <div className={styles.content}>
{/* Модальное окно с пакетами токенов */}
<TokenPacksModal
isVisible={showTokensModal}
onClose={() => 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);
}
}
});
}}
/>
{/* Блоки из конфигурации */}
<div className={styles.blocks}> <div className={styles.blocks}>
{(() => { {/* Верхний блок с кнопками */}
// Получаем все блоки из конфигурации <MainActions
const allBlocks = homeScreenConfig.homeScreen.blocks.filter(block => block.type !== 'generateButton'); onSendFeedback={handleSendFeedback}
onOpenTelegramBot={handleOpenTelegramBot}
/>
// Создаем массив блоков для отображения в нужном порядке {/* Блок загрузки фото */}
const blocksToRender = []; <PhotoUpload
onImageDataChange={handleImageDataChange}
/>
// Проходим по всем блокам и добавляем их в массив для отображения в нужном порядке {/* Компонент выбора пола */}
for (const block of allBlocks) { <GenderSelector
// Блоки для выбора типа эмоций показываем только если выбран стиль "Эмоции" selectedGenderButtonId={selectedGenderButtonId}
if (block.id === 'emotionTypeTitle' || block.id === 'emotionTypeSelection') { onGenderDetectionSelect={handleGenderDetectionSelect}
if (selectedStyle === 'emotions') { onGenderSelect={handleGenderSelect}
blocksToRender.push(block); />
}
continue;
}
// Блок с полем ввода текста (customPrompt) показываем после emotionTypeSelection и перед emotionPromptsSelection {/* Компонент выбора стиля */}
// если выбран стиль "Эмоции" и тип "Промпты" и нажата кнопка "Свой промпт" <StyleSelector
if (block.id === 'customPrompt') { selectedStyleButtonId={selectedStyleButtonId}
// Для стиля "Чиби" показываем в обычном порядке onStyleSelect={handleStyleSelect}
if (selectedStyle === 'chibi' && isInputVisible) { />
blocksToRender.push(block);
}
// Для стиля "Эмоции" и типа "Промпты" блок будет добавлен позже в специальном месте
continue;
}
// Блок выбора мемов показываем только если выбран стиль "Эмоции" и тип "Мемы" {/* Заголовок "3 выбери образ" */}
if (block.id === 'memeSelection') { {(() => {
if (selectedStyle === 'emotions' && selectedEmotionType === 'memes') { const titleBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'step3');
blocksToRender.push(block); return titleBlock ? (
}
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 (
<BlockRenderer <BlockRenderer
key={block.id} block={titleBlock}
block={modifiedBlock} onAction={() => {}}
onAction={handleBlockAction}
selectedButtonId={selectedButtonId}
extraProps={block.type === 'textInput' ? {
visible: isInputVisible,
onTextChange: setCustomPrompt
} : undefined}
/> />
); ) : null;
}); })()}
})()}
{/* Компонент выбора типа эмоций */}
<EmotionTypeSelector
selectedEmotionTypeButtonId={selectedEmotionTypeButtonId}
onEmotionTypeSelect={handleEmotionTypeSelect}
visible={selectedStyle === 'emotions'}
/>
{/* Компонент выбора мема или пресета в зависимости от выбранного типа эмоций */}
{selectedStyle === 'emotions' && selectedEmotionType === 'memes' ? (
<MemeSelector
selectedMemeId={selectedMemeId}
onMemeSelect={handleMemeSelect}
visible={true}
/>
) : (
<PresetSelector
selectedPresetId={selectedPresetId}
onPresetSelect={handlePresetSelect}
selectedStyle={selectedStyle}
selectedEmotionType={selectedEmotionType}
isInputVisible={isInputVisible}
onToggleInput={handleToggleInput}
onCustomPromptChange={handleCustomPromptChange}
/>
)}
</div> </div>
{homeScreenConfig.homeScreen.blocks
.filter(block => block.type === 'generateButton') {/* Компонент кнопки генерации */}
.map((block) => ( <GenerationButton
<div className={styles.generateButtonContainer} key={block.id}> onStartGeneration={startGeneration}
<BlockRenderer isGenerating={isGenerating}
block={block} />
onAction={handleBlockAction}
extraProps={undefined}
/>
</div>
))}
</div> </div>
</div> </div>
); );

83
src/types/generation.ts Normal file
View File

@ -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<typeof setTimeout> | 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;
}

76
src/utils/balanceUtils.ts Normal file
View File

@ -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<void>,
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);
}
};

View File

@ -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;
};