Fix image loading issues in buttons by enhancing ImageWithFallback component with button variant
This commit is contained in:
parent
9f9daf0803
commit
825be2d0a6
@ -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<SquareButtonProps> = ({
|
||||
data-selected={isSelected ? 'true' : 'false'} /* Добавляем data-атрибут для дополнительной стилизации */
|
||||
>
|
||||
{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 ? (
|
||||
<span className={styles.icon}>{icon}</span>
|
||||
) : null}
|
||||
|
||||
46
src/components/generation/EmotionTypeSelector.tsx
Normal file
46
src/components/generation/EmotionTypeSelector.tsx
Normal 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;
|
||||
44
src/components/generation/GenderSelector.tsx
Normal file
44
src/components/generation/GenderSelector.tsx
Normal 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;
|
||||
43
src/components/generation/GenerationButton.tsx
Normal file
43
src/components/generation/GenerationButton.tsx
Normal 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;
|
||||
50
src/components/generation/MainActions.tsx
Normal file
50
src/components/generation/MainActions.tsx
Normal 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;
|
||||
46
src/components/generation/MemeSelector.tsx
Normal file
46
src/components/generation/MemeSelector.tsx
Normal 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;
|
||||
44
src/components/generation/PhotoUpload.tsx
Normal file
44
src/components/generation/PhotoUpload.tsx
Normal 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;
|
||||
87
src/components/generation/PresetSelector.tsx
Normal file
87
src/components/generation/PresetSelector.tsx
Normal 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;
|
||||
47
src/components/generation/StyleSelector.tsx
Normal file
47
src/components/generation/StyleSelector.tsx
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -9,6 +9,8 @@ interface ImageWithFallbackProps {
|
||||
onContextMenu?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
maxRetries?: number;
|
||||
isDeleteMode?: boolean;
|
||||
showErrorUI?: boolean; // Показывать ли UI с ошибкой
|
||||
variant?: 'default' | 'button'; // Вариант отображения: обычный или для кнопок
|
||||
}
|
||||
|
||||
const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
||||
@ -18,7 +20,9 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
||||
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<ImageWithFallbackProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.container} ${className}`}
|
||||
className={`${styles.container} ${variant === 'button' ? styles.buttonContainer : ''} ${className}`}
|
||||
onClick={handleClick}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
{/* Показываем индикатор загрузки, если изображение загружается */}
|
||||
{loading && (
|
||||
{loading && variant !== 'button' && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner}></div>
|
||||
</div>
|
||||
@ -128,13 +132,13 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
||||
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 && (
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorIcon}>!</div>
|
||||
<div className={styles.errorMessage}>Ошибка загрузки</div>
|
||||
|
||||
67
src/components/tokens/TokenPacksModalContainer.tsx
Normal file
67
src/components/tokens/TokenPacksModalContainer.tsx
Normal 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;
|
||||
472
src/hooks/useGenerationState.ts
Normal file
472
src/hooks/useGenerationState.ts
Normal 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
121
src/hooks/useImageCheck.ts
Normal 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;
|
||||
162
src/hooks/useNotifications.ts
Normal file
162
src/hooks/useNotifications.ts
Normal 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;
|
||||
@ -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 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<FeedbackHandlerRef>(null);
|
||||
const { updateBalance } = useBalance(); // Используем контекст баланса
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
|
||||
// Проверяем, есть ли превью в состоянии навигации или 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<string | undefined>(() => {
|
||||
const state = window.history.state?.usr;
|
||||
return state?.imageData || localStorage.getItem('stickerImageData') || undefined;
|
||||
});
|
||||
const [isInputVisible, setIsInputVisible] = useState(false);
|
||||
const [selectedStyle, setSelectedStyle] = useState<string>('emotions'); // По умолчанию выбран стиль "Эмоции"
|
||||
const [selectedStyleButtonId, setSelectedStyleButtonId] = useState<string | undefined>('emotions'); // Для хранения ID выбранной кнопки стиля
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(undefined); // Для хранения ID выбранного пресета
|
||||
const [customPrompt, setCustomPrompt] = useState<string>(''); // Для хранения пользовательского промпта
|
||||
// Используем хук для управления состоянием генерации
|
||||
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 [selectedEmotionType, setSelectedEmotionType] = useState<'memes' | 'prompts' | undefined>('prompts'); // По умолчанию выбран тип "Промпты"
|
||||
const [selectedEmotionTypeButtonId, setSelectedEmotionTypeButtonId] = useState<string | undefined>('prompts'); // Для хранения ID выбранной кнопки типа эмоций
|
||||
const [selectedMemeId, setSelectedMemeId] = useState<string | undefined>(undefined); // Для хранения ID выбранного мема
|
||||
// Эффект для обработки закрытия приложения
|
||||
useEffect(() => {
|
||||
// Обработчик события beforeunload для очистки данных при закрытии приложения
|
||||
const handleBeforeUnload = () => {
|
||||
localStorage.removeItem('stickerPreviewUrl');
|
||||
localStorage.removeItem('stickerImageData');
|
||||
};
|
||||
|
||||
// Состояния для выбора пола
|
||||
const [genderDetection, setGenderDetection] = useState<'auto' | 'manual'>('auto'); // По умолчанию автоматическое определение пола
|
||||
const [manualGender, setManualGender] = useState<'man' | 'woman' | undefined>(undefined); // Для хранения выбранного пола
|
||||
const [selectedGenderButtonId, setSelectedGenderButtonId] = useState<string | undefined>('auto'); // Для хранения ID выбранной кнопки пола
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
// Состояния для модального окна уведомления
|
||||
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<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]);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
cleanupImageCheck();
|
||||
};
|
||||
}, [cleanupImageCheck]);
|
||||
|
||||
// Эффект для обновления window.history.state при загрузке из localStorage
|
||||
useEffect(() => {
|
||||
@ -522,44 +124,31 @@ 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');
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (block.id === 'emotionPromptsSelection' && selectedStyle === 'emotions' && selectedEmotionType === 'prompts') {
|
||||
// Возвращаем только кнопки из стиля "Эмоции"
|
||||
return stylePresets.emotions?.buttons || [];
|
||||
}
|
||||
// Обработчик изменения данных изображения
|
||||
const handleImageDataChange = useCallback((imageData: string) => {
|
||||
// Сохраняем данные изображения в состоянии
|
||||
updateGeneratedImageUrl(undefined); // Сбрасываем URL сгенерированного изображения
|
||||
setImageData(imageData);
|
||||
|
||||
return block.buttons;
|
||||
}, [selectedStyle, selectedEmotionType]);
|
||||
// Сохраняем данные изображения в localStorage
|
||||
localStorage.setItem('stickerImageData', imageData);
|
||||
}, [updateGeneratedImageUrl, setImageData]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -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 = () => {
|
||||
{/* Компонент обработки обратной связи */}
|
||||
<FeedbackHandler
|
||||
ref={feedbackHandlerRef}
|
||||
onFeedbackSent={() => {
|
||||
// Показываем уведомление об успешной отправке
|
||||
setNotificationTitle('Спасибо за обратную связь');
|
||||
setNotificationMessage('Ваше сообщение успешно отправлено');
|
||||
setIsLoading(false);
|
||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею" для уведомления об обратной связи
|
||||
setShowButtons(true); // Показываем кнопки
|
||||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||||
setIsNotificationVisible(true);
|
||||
}}
|
||||
onFeedbackSent={showFeedbackSentNotification}
|
||||
/>
|
||||
|
||||
{/* Модальное окно с пакетами токенов */}
|
||||
<TokenPacksModalContainer
|
||||
isVisible={showTokensModal}
|
||||
onClose={() => setShowTokensModal(false)}
|
||||
missingTokens={missingTokens}
|
||||
onSuccess={startGeneration}
|
||||
/>
|
||||
|
||||
<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}>
|
||||
{(() => {
|
||||
// Получаем все блоки из конфигурации
|
||||
const allBlocks = homeScreenConfig.homeScreen.blocks.filter(block => block.type !== 'generateButton');
|
||||
{/* Верхний блок с кнопками */}
|
||||
<MainActions
|
||||
onSendFeedback={handleSendFeedback}
|
||||
onOpenTelegramBot={handleOpenTelegramBot}
|
||||
/>
|
||||
|
||||
// Создаем массив блоков для отображения в нужном порядке
|
||||
const blocksToRender = [];
|
||||
{/* Блок загрузки фото */}
|
||||
<PhotoUpload
|
||||
onImageDataChange={handleImageDataChange}
|
||||
/>
|
||||
|
||||
// Проходим по всем блокам и добавляем их в массив для отображения в нужном порядке
|
||||
for (const block of allBlocks) {
|
||||
// Блоки для выбора типа эмоций показываем только если выбран стиль "Эмоции"
|
||||
if (block.id === 'emotionTypeTitle' || block.id === 'emotionTypeSelection') {
|
||||
if (selectedStyle === 'emotions') {
|
||||
blocksToRender.push(block);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
{/* Компонент выбора пола */}
|
||||
<GenderSelector
|
||||
selectedGenderButtonId={selectedGenderButtonId}
|
||||
onGenderDetectionSelect={handleGenderDetectionSelect}
|
||||
onGenderSelect={handleGenderSelect}
|
||||
/>
|
||||
|
||||
// Блок с полем ввода текста (customPrompt) показываем после emotionTypeSelection и перед emotionPromptsSelection
|
||||
// если выбран стиль "Эмоции" и тип "Промпты" и нажата кнопка "Свой промпт"
|
||||
if (block.id === 'customPrompt') {
|
||||
// Для стиля "Чиби" показываем в обычном порядке
|
||||
if (selectedStyle === 'chibi' && isInputVisible) {
|
||||
blocksToRender.push(block);
|
||||
}
|
||||
// Для стиля "Эмоции" и типа "Промпты" блок будет добавлен позже в специальном месте
|
||||
continue;
|
||||
}
|
||||
{/* Компонент выбора стиля */}
|
||||
<StyleSelector
|
||||
selectedStyleButtonId={selectedStyleButtonId}
|
||||
onStyleSelect={handleStyleSelect}
|
||||
/>
|
||||
|
||||
// Блок выбора мемов показываем только если выбран стиль "Эмоции" и тип "Мемы"
|
||||
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 ? (
|
||||
<BlockRenderer
|
||||
key={block.id}
|
||||
block={modifiedBlock}
|
||||
onAction={handleBlockAction}
|
||||
selectedButtonId={selectedButtonId}
|
||||
extraProps={block.type === 'textInput' ? {
|
||||
visible: isInputVisible,
|
||||
onTextChange: setCustomPrompt
|
||||
} : undefined}
|
||||
block={titleBlock}
|
||||
onAction={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
) : 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>
|
||||
{homeScreenConfig.homeScreen.blocks
|
||||
.filter(block => block.type === 'generateButton')
|
||||
.map((block) => (
|
||||
<div className={styles.generateButtonContainer} key={block.id}>
|
||||
<BlockRenderer
|
||||
block={block}
|
||||
onAction={handleBlockAction}
|
||||
extraProps={undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Компонент кнопки генерации */}
|
||||
<GenerationButton
|
||||
onStartGeneration={startGeneration}
|
||||
isGenerating={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
83
src/types/generation.ts
Normal file
83
src/types/generation.ts
Normal 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
76
src/utils/balanceUtils.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
143
src/utils/generationUtils.ts
Normal file
143
src/utils/generationUtils.ts
Normal 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;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user