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 React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { BlockButton } from '../../types/blocks';
|
import { BlockButton } from '../../types/blocks';
|
||||||
|
import ImageWithFallback from '../shared/ImageWithFallback';
|
||||||
import styles from './SquareButton.module.css';
|
import styles from './SquareButton.module.css';
|
||||||
|
|
||||||
interface SquareButtonProps extends BlockButton {
|
interface SquareButtonProps extends BlockButton {
|
||||||
@ -87,7 +88,14 @@ const SquareButton: React.FC<SquareButtonProps> = ({
|
|||||||
data-selected={isSelected ? 'true' : 'false'} /* Добавляем data-атрибут для дополнительной стилизации */
|
data-selected={isSelected ? 'true' : 'false'} /* Добавляем data-атрибут для дополнительной стилизации */
|
||||||
>
|
>
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
<img src={imageUrl} alt={title} className={styles.iconImage} />
|
<ImageWithFallback
|
||||||
|
src={imageUrl}
|
||||||
|
alt={title}
|
||||||
|
className={styles.iconImage}
|
||||||
|
maxRetries={3} // Увеличиваем количество повторных попыток для кнопок
|
||||||
|
showErrorUI={false} // Отключаем отображение UI с ошибкой
|
||||||
|
variant="button" // Используем вариант для кнопок
|
||||||
|
/>
|
||||||
) : icon ? (
|
) : icon ? (
|
||||||
<span className={styles.icon}>{icon}</span>
|
<span className={styles.icon}>{icon}</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
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;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Специальный контейнер для кнопок - без фона */
|
||||||
|
.buttonContainer {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -22,6 +27,18 @@
|
|||||||
user-drag: none;
|
user-drag: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Специальный стиль для изображений в кнопках */
|
||||||
|
.buttonImage {
|
||||||
|
position: absolute;
|
||||||
|
top: 1%;
|
||||||
|
left: 0%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
transform: scale(1);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -9,6 +9,8 @@ interface ImageWithFallbackProps {
|
|||||||
onContextMenu?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onContextMenu?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
isDeleteMode?: boolean;
|
isDeleteMode?: boolean;
|
||||||
|
showErrorUI?: boolean; // Показывать ли UI с ошибкой
|
||||||
|
variant?: 'default' | 'button'; // Вариант отображения: обычный или для кнопок
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
||||||
@ -18,7 +20,9 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
maxRetries = 2, // По умолчанию 2 попытки автоматической перезагрузки
|
maxRetries = 2, // По умолчанию 2 попытки автоматической перезагрузки
|
||||||
isDeleteMode = false
|
isDeleteMode = false,
|
||||||
|
showErrorUI = true, // По умолчанию показываем UI с ошибкой
|
||||||
|
variant = 'default' // По умолчанию обычный вариант отображения
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
@ -111,12 +115,12 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.container} ${className}`}
|
className={`${styles.container} ${variant === 'button' ? styles.buttonContainer : ''} ${className}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
>
|
>
|
||||||
{/* Показываем индикатор загрузки, если изображение загружается */}
|
{/* Показываем индикатор загрузки, если изображение загружается */}
|
||||||
{loading && (
|
{loading && variant !== 'button' && (
|
||||||
<div className={styles.loadingContainer}>
|
<div className={styles.loadingContainer}>
|
||||||
<div className={styles.spinner}></div>
|
<div className={styles.spinner}></div>
|
||||||
</div>
|
</div>
|
||||||
@ -128,13 +132,13 @@ const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
|
|||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
className={`${styles.image} ${error ? styles.hidden : ''}`}
|
className={`${styles.image} ${variant === 'button' ? styles.buttonImage : ''} ${error ? styles.hidden : ''}`}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка */}
|
{/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка и showErrorUI=true */}
|
||||||
{error && (
|
{error && showErrorUI && (
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<div className={styles.errorIcon}>!</div>
|
<div className={styles.errorIcon}>!</div>
|
||||||
<div className={styles.errorMessage}>Ошибка загрузки</div>
|
<div className={styles.errorMessage}>Ошибка загрузки</div>
|
||||||
|
|||||||
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 { useNavigate } from 'react-router-dom';
|
||||||
import BlockRenderer from '../components/blocks/BlockRenderer';
|
|
||||||
import styles from './Home.module.css';
|
import styles from './Home.module.css';
|
||||||
import { homeScreenConfig } from '../config/homeScreen';
|
|
||||||
import { stylePresets } from '../config/stylePresets';
|
|
||||||
import apiService from '../services/api';
|
|
||||||
import NotificationModal from '../components/shared/NotificationModal';
|
|
||||||
import FeedbackHandler, { FeedbackHandlerRef } from '../components/shared/FeedbackHandler';
|
import FeedbackHandler, { FeedbackHandlerRef } from '../components/shared/FeedbackHandler';
|
||||||
import TokenPacksModal from '../components/tokens/TokenPacksModal';
|
import NotificationModal from '../components/shared/NotificationModal';
|
||||||
import { paymentService } from '../services/paymentService';
|
import useGenerationState from '../hooks/useGenerationState';
|
||||||
import { tokenPacks } from '../constants/tokenPacks';
|
import useImageCheck from '../hooks/useImageCheck';
|
||||||
import { getCurrentUserId } from '../constants/user';
|
import useNotifications from '../hooks/useNotifications';
|
||||||
import { useBalance } from '../contexts/BalanceContext';
|
import StyleSelector from '../components/generation/StyleSelector';
|
||||||
import { sendTargetEvent } from '../services/analyticsService';
|
import EmotionTypeSelector from '../components/generation/EmotionTypeSelector';
|
||||||
import { WorkflowType } from '../constants/workflows';
|
import MemeSelector from '../components/generation/MemeSelector';
|
||||||
|
import PresetSelector from '../components/generation/PresetSelector';
|
||||||
// Интерфейс для хранения данных о последней генерации
|
import GenderSelector from '../components/generation/GenderSelector';
|
||||||
interface LastGenerationData {
|
import GenerationButton from '../components/generation/GenerationButton';
|
||||||
imageData?: string;
|
import TokenPacksModalContainer from '../components/tokens/TokenPacksModalContainer';
|
||||||
style?: string;
|
import MainActions from '../components/generation/MainActions';
|
||||||
presetId?: string;
|
import PhotoUpload from '../components/generation/PhotoUpload';
|
||||||
customPrompt?: string;
|
import BlockRenderer from '../components/blocks/BlockRenderer';
|
||||||
}
|
import { homeScreenConfig } from '../config/homeScreen';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Компонент главного экрана приложения
|
||||||
|
*/
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const feedbackHandlerRef = useRef<FeedbackHandlerRef>(null);
|
const feedbackHandlerRef = useRef<FeedbackHandlerRef>(null);
|
||||||
const { updateBalance } = useBalance(); // Используем контекст баланса
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// Используем хук для управления уведомлениями
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
|
const {
|
||||||
// Проверяем, есть ли превью в состоянии навигации или localStorage
|
isNotificationVisible,
|
||||||
const state = window.history.state?.usr;
|
notificationTitle,
|
||||||
return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined;
|
notificationMessage,
|
||||||
|
isLoading,
|
||||||
|
showGalleryButton,
|
||||||
|
showButtons,
|
||||||
|
continueButtonText,
|
||||||
|
showTokensModal,
|
||||||
|
missingTokens,
|
||||||
|
setShowTokensModal,
|
||||||
|
setMissingTokens,
|
||||||
|
handleGalleryClick,
|
||||||
|
handleContinueClick,
|
||||||
|
handleCloseAndReset,
|
||||||
|
showNotification,
|
||||||
|
showFeedbackSentNotification
|
||||||
|
} = useNotifications();
|
||||||
|
|
||||||
|
// Используем хук для проверки новых изображений
|
||||||
|
const {
|
||||||
|
startImageCheck,
|
||||||
|
cleanup: cleanupImageCheck
|
||||||
|
} = useImageCheck((url) => {
|
||||||
|
updateGeneratedImageUrl(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [imageData, _setImageData] = useState<string | undefined>(() => {
|
// Используем хук для управления состоянием генерации
|
||||||
const state = window.history.state?.usr;
|
const {
|
||||||
return state?.imageData || localStorage.getItem('stickerImageData') || undefined;
|
isGenerating,
|
||||||
});
|
|
||||||
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 [selectedEmotionType, setSelectedEmotionType] = useState<'memes' | 'prompts' | undefined>('prompts'); // По умолчанию выбран тип "Промпты"
|
|
||||||
const [selectedEmotionTypeButtonId, setSelectedEmotionTypeButtonId] = useState<string | undefined>('prompts'); // Для хранения ID выбранной кнопки типа эмоций
|
|
||||||
const [selectedMemeId, setSelectedMemeId] = useState<string | undefined>(undefined); // Для хранения ID выбранного мема
|
|
||||||
|
|
||||||
// Состояния для выбора пола
|
|
||||||
const [genderDetection, setGenderDetection] = useState<'auto' | 'manual'>('auto'); // По умолчанию автоматическое определение пола
|
|
||||||
const [manualGender, setManualGender] = useState<'man' | 'woman' | undefined>(undefined); // Для хранения выбранного пола
|
|
||||||
const [selectedGenderButtonId, setSelectedGenderButtonId] = useState<string | undefined>('auto'); // Для хранения ID выбранной кнопки пола
|
|
||||||
|
|
||||||
// Состояния для модального окна уведомления
|
|
||||||
const [isNotificationVisible, setIsNotificationVisible] = useState(false);
|
|
||||||
const [notificationTitle, setNotificationTitle] = useState('');
|
|
||||||
const [notificationMessage, setNotificationMessage] = useState('');
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [showGalleryButton, setShowGalleryButton] = useState(true);
|
|
||||||
const [showButtons, setShowButtons] = useState(true);
|
|
||||||
const [continueButtonText, setContinueButtonText] = useState('Продолжить');
|
|
||||||
const [lastGenerationData, setLastGenerationData] = useState<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,
|
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,
|
selectedStyle,
|
||||||
|
selectedStyleButtonId,
|
||||||
|
selectedPresetId,
|
||||||
|
isInputVisible,
|
||||||
|
customPrompt,
|
||||||
selectedEmotionType,
|
selectedEmotionType,
|
||||||
isInputVisible: !isInputVisible // Новое значение
|
selectedEmotionTypeButtonId,
|
||||||
});
|
selectedMemeId,
|
||||||
|
selectedGenderButtonId,
|
||||||
|
currentTaskId,
|
||||||
|
generatedImageUrl,
|
||||||
|
queuePosition,
|
||||||
|
setImageData,
|
||||||
|
handleStyleSelect,
|
||||||
|
handlePresetSelect,
|
||||||
|
handleEmotionTypeSelect,
|
||||||
|
handleMemeSelect,
|
||||||
|
handleGenderDetectionSelect,
|
||||||
|
handleGenderSelect,
|
||||||
|
handleToggleInput,
|
||||||
|
handleCustomPromptChange,
|
||||||
|
startGeneration,
|
||||||
|
resetGenerationState,
|
||||||
|
updateGeneratedImageUrl
|
||||||
|
} = useGenerationState(
|
||||||
|
showNotification,
|
||||||
|
setShowTokensModal,
|
||||||
|
setMissingTokens,
|
||||||
|
startImageCheck
|
||||||
|
);
|
||||||
|
|
||||||
setIsInputVisible(prev => !prev);
|
// Эффект для обработки закрытия приложения
|
||||||
// Устанавливаем selectedPresetId в 'customPrompt' при нажатии на кнопку "Свой промпт"
|
useEffect(() => {
|
||||||
setSelectedPresetId('customPrompt');
|
// Обработчик события beforeunload для очистки данных при закрытии приложения
|
||||||
return;
|
const handleBeforeUnload = () => {
|
||||||
}
|
localStorage.removeItem('stickerPreviewUrl');
|
||||||
|
localStorage.removeItem('stickerImageData');
|
||||||
|
};
|
||||||
|
|
||||||
if (actionValue === 'openTelegramBot') {
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
// Проверяем, доступен ли объект 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') {
|
return () => {
|
||||||
// Открываем модальное окно обратной связи
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
feedbackHandlerRef.current?.openFeedbackModal();
|
cleanupImageCheck();
|
||||||
return;
|
};
|
||||||
}
|
}, [cleanupImageCheck]);
|
||||||
} else if (actionType === 'route') {
|
|
||||||
// Добавляем обработку для действий типа 'route'
|
|
||||||
navigate(actionValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если выбрана любая другая кнопка (кроме "Свой промпт"), скрываем поле ввода
|
|
||||||
if (!(actionType === 'function' && actionValue === 'toggleInput' && buttonId === 'customPrompt')) {
|
|
||||||
setIsInputVisible(false);
|
|
||||||
}
|
|
||||||
}, [navigate, imageData, selectedStyle, selectedPresetId, customPrompt, lastGenerationData, genderDetection, manualGender, selectedEmotionType, selectedMemeId, imagesCheckInterval, checkForNewImages]);
|
|
||||||
|
|
||||||
// Эффект для обновления window.history.state при загрузке из localStorage
|
// Эффект для обновления window.history.state при загрузке из localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -522,44 +124,31 @@ const Home: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
// Обработчик отправки обратной связи
|
||||||
|
const handleSendFeedback = useCallback(() => {
|
||||||
|
feedbackHandlerRef.current?.openFeedbackModal();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Эффект для обработки закрытия приложения
|
// Обработчик открытия Telegram бота
|
||||||
useEffect(() => {
|
const handleOpenTelegramBot = useCallback(() => {
|
||||||
// Обработчик события beforeunload для очистки данных при закрытии приложения
|
// Проверяем, доступен ли объект Telegram
|
||||||
const handleBeforeUnload = () => {
|
if (window.Telegram && window.Telegram.WebApp) {
|
||||||
localStorage.removeItem('stickerPreviewUrl');
|
window.Telegram.WebApp.openTelegramLink('https://t.me/youtube_s_loader_bot');
|
||||||
localStorage.removeItem('stickerImageData');
|
} else {
|
||||||
};
|
// Запасной вариант, если API Telegram недоступен
|
||||||
|
window.open('https://t.me/youtube_s_loader_bot', '_blank');
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
||||||
|
|
||||||
// Очищаем интервалы при размонтировании компонента
|
|
||||||
if (imagesCheckInterval) {
|
|
||||||
clearInterval(imagesCheckInterval);
|
|
||||||
}
|
}
|
||||||
if (checkInterval) {
|
}, []);
|
||||||
clearInterval(checkInterval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [imagesCheckInterval, checkInterval]);
|
|
||||||
|
|
||||||
// Функция для получения кнопок в зависимости от блока
|
// Обработчик изменения данных изображения
|
||||||
const getBlockButtons = useCallback((block: any) => {
|
const handleImageDataChange = useCallback((imageData: string) => {
|
||||||
if (block.id === 'quickActions') {
|
// Сохраняем данные изображения в состоянии
|
||||||
// Возвращаем только кнопки из выбранного стиля
|
updateGeneratedImageUrl(undefined); // Сбрасываем URL сгенерированного изображения
|
||||||
return stylePresets[selectedStyle]?.buttons || [];
|
setImageData(imageData);
|
||||||
}
|
|
||||||
|
|
||||||
if (block.id === 'emotionPromptsSelection' && selectedStyle === 'emotions' && selectedEmotionType === 'prompts') {
|
// Сохраняем данные изображения в localStorage
|
||||||
// Возвращаем только кнопки из стиля "Эмоции"
|
localStorage.setItem('stickerImageData', imageData);
|
||||||
return stylePresets.emotions?.buttons || [];
|
}, [updateGeneratedImageUrl, setImageData]);
|
||||||
}
|
|
||||||
|
|
||||||
return block.buttons;
|
|
||||||
}, [selectedStyle, selectedEmotionType]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -570,7 +159,7 @@ const Home: React.FC = () => {
|
|||||||
message={notificationMessage}
|
message={notificationMessage}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onGalleryClick={handleGalleryClick}
|
onGalleryClick={handleGalleryClick}
|
||||||
onContinueClick={handleContinueClick}
|
onContinueClick={() => handleCloseAndReset(resetGenerationState)}
|
||||||
showGalleryButton={showGalleryButton}
|
showGalleryButton={showGalleryButton}
|
||||||
showButtons={showButtons}
|
showButtons={showButtons}
|
||||||
continueButtonText={continueButtonText}
|
continueButtonText={continueButtonText}
|
||||||
@ -584,243 +173,86 @@ const Home: React.FC = () => {
|
|||||||
{/* Компонент обработки обратной связи */}
|
{/* Компонент обработки обратной связи */}
|
||||||
<FeedbackHandler
|
<FeedbackHandler
|
||||||
ref={feedbackHandlerRef}
|
ref={feedbackHandlerRef}
|
||||||
onFeedbackSent={() => {
|
onFeedbackSent={showFeedbackSentNotification}
|
||||||
// Показываем уведомление об успешной отправке
|
/>
|
||||||
setNotificationTitle('Спасибо за обратную связь');
|
|
||||||
setNotificationMessage('Ваше сообщение успешно отправлено');
|
{/* Модальное окно с пакетами токенов */}
|
||||||
setIsLoading(false);
|
<TokenPacksModalContainer
|
||||||
setShowGalleryButton(false); // Скрываем кнопку "В галерею" для уведомления об обратной связи
|
isVisible={showTokensModal}
|
||||||
setShowButtons(true); // Показываем кнопки
|
onClose={() => setShowTokensModal(false)}
|
||||||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
missingTokens={missingTokens}
|
||||||
setIsNotificationVisible(true);
|
onSuccess={startGeneration}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* Модальное окно с пакетами токенов */}
|
|
||||||
<TokenPacksModal
|
|
||||||
isVisible={showTokensModal}
|
|
||||||
onClose={() => setShowTokensModal(false)}
|
|
||||||
onShowAllPacks={() => navigate('/profile')}
|
|
||||||
missingTokens={missingTokens}
|
|
||||||
onBuyPack={(packId: string) => {
|
|
||||||
const pack = tokenPacks.find(p => p.id === packId);
|
|
||||||
if (!pack) return;
|
|
||||||
|
|
||||||
setShowTokensModal(false);
|
|
||||||
setLastPurchasedPack(pack);
|
|
||||||
|
|
||||||
paymentService.showBuyTokensPopup(pack, async (userData) => {
|
|
||||||
if (userData) {
|
|
||||||
// Функция для выполнения серии запросов на обновление баланса
|
|
||||||
const updateBalanceWithRetries = () => {
|
|
||||||
// Функция для выполнения одной попытки обновления баланса
|
|
||||||
const fetchBalance = async (attempt: number) => {
|
|
||||||
try {
|
|
||||||
console.log(`Попытка ${attempt}/5 обновления баланса после пополнения...`);
|
|
||||||
await updateBalance();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Ошибка при обновлении баланса (попытка ${attempt}/5):`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Выполняем первую попытку сразу
|
|
||||||
fetchBalance(1);
|
|
||||||
|
|
||||||
// Выполняем остальные попытки с интервалом в 1 секунду
|
|
||||||
for (let i = 2; i <= 5; i++) {
|
|
||||||
setTimeout(() => fetchBalance(i), (i - 1) * 1000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запускаем серию запросов на обновление баланса
|
|
||||||
updateBalanceWithRetries();
|
|
||||||
|
|
||||||
// Показываем модальное окно с информацией об успешной оплате
|
|
||||||
setNotificationTitle('Оплата успешна!');
|
|
||||||
setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов.`);
|
|
||||||
setIsLoading(false);
|
|
||||||
setShowGalleryButton(false);
|
|
||||||
setShowButtons(true);
|
|
||||||
setContinueButtonText('Закрыть');
|
|
||||||
setIsNotificationVisible(true);
|
|
||||||
} else {
|
|
||||||
// Если данные не получены, делаем запрос на получение данных пользователя
|
|
||||||
try {
|
|
||||||
// Получаем баланс пользователя
|
|
||||||
const balance = await apiService.getBalance(getCurrentUserId());
|
|
||||||
|
|
||||||
// Функция для выполнения серии запросов на обновление баланса
|
|
||||||
const updateBalanceWithRetries = () => {
|
|
||||||
// Функция для выполнения одной попытки обновления баланса
|
|
||||||
const fetchBalance = async (attempt: number) => {
|
|
||||||
try {
|
|
||||||
console.log(`Попытка ${attempt}/5 обновления баланса после пополнения...`);
|
|
||||||
await updateBalance();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Ошибка при обновлении баланса (попытка ${attempt}/5):`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Выполняем первую попытку сразу
|
|
||||||
fetchBalance(1);
|
|
||||||
|
|
||||||
// Выполняем остальные попытки с интервалом в 1 секунду
|
|
||||||
for (let i = 2; i <= 5; i++) {
|
|
||||||
setTimeout(() => fetchBalance(i), (i - 1) * 1000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запускаем серию запросов на обновление баланса
|
|
||||||
updateBalanceWithRetries();
|
|
||||||
|
|
||||||
// Показываем модальное окно с информацией об успешной оплате
|
|
||||||
setNotificationTitle('Оплата успешна!');
|
|
||||||
setNotificationMessage(`Вы успешно приобрели ${pack.tokens + pack.bonusTokens} токенов. Ваш текущий баланс: ${balance} токенов.`);
|
|
||||||
setIsLoading(false);
|
|
||||||
setShowGalleryButton(false);
|
|
||||||
setShowButtons(true);
|
|
||||||
setContinueButtonText('Закрыть');
|
|
||||||
setIsNotificationVisible(true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при обновлении данных пользователя:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* Блоки из конфигурации */}
|
|
||||||
<div className={styles.blocks}>
|
<div className={styles.blocks}>
|
||||||
|
{/* Верхний блок с кнопками */}
|
||||||
|
<MainActions
|
||||||
|
onSendFeedback={handleSendFeedback}
|
||||||
|
onOpenTelegramBot={handleOpenTelegramBot}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Блок загрузки фото */}
|
||||||
|
<PhotoUpload
|
||||||
|
onImageDataChange={handleImageDataChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Компонент выбора пола */}
|
||||||
|
<GenderSelector
|
||||||
|
selectedGenderButtonId={selectedGenderButtonId}
|
||||||
|
onGenderDetectionSelect={handleGenderDetectionSelect}
|
||||||
|
onGenderSelect={handleGenderSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Компонент выбора стиля */}
|
||||||
|
<StyleSelector
|
||||||
|
selectedStyleButtonId={selectedStyleButtonId}
|
||||||
|
onStyleSelect={handleStyleSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Заголовок "3 выбери образ" */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// Получаем все блоки из конфигурации
|
const titleBlock = homeScreenConfig.homeScreen.blocks.find(block => block.id === 'step3');
|
||||||
const allBlocks = homeScreenConfig.homeScreen.blocks.filter(block => block.type !== 'generateButton');
|
return titleBlock ? (
|
||||||
|
|
||||||
// Создаем массив блоков для отображения в нужном порядке
|
|
||||||
const blocksToRender = [];
|
|
||||||
|
|
||||||
// Проходим по всем блокам и добавляем их в массив для отображения в нужном порядке
|
|
||||||
for (const block of allBlocks) {
|
|
||||||
// Блоки для выбора типа эмоций показываем только если выбран стиль "Эмоции"
|
|
||||||
if (block.id === 'emotionTypeTitle' || block.id === 'emotionTypeSelection') {
|
|
||||||
if (selectedStyle === 'emotions') {
|
|
||||||
blocksToRender.push(block);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Блок с полем ввода текста (customPrompt) показываем после emotionTypeSelection и перед emotionPromptsSelection
|
|
||||||
// если выбран стиль "Эмоции" и тип "Промпты" и нажата кнопка "Свой промпт"
|
|
||||||
if (block.id === 'customPrompt') {
|
|
||||||
// Для стиля "Чиби" показываем в обычном порядке
|
|
||||||
if (selectedStyle === 'chibi' && isInputVisible) {
|
|
||||||
blocksToRender.push(block);
|
|
||||||
}
|
|
||||||
// Для стиля "Эмоции" и типа "Промпты" блок будет добавлен позже в специальном месте
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Блок выбора мемов показываем только если выбран стиль "Эмоции" и тип "Мемы"
|
|
||||||
if (block.id === 'memeSelection') {
|
|
||||||
if (selectedStyle === 'emotions' && selectedEmotionType === 'memes') {
|
|
||||||
blocksToRender.push(block);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Блок выбора промптов для эмоций показываем только если выбран стиль "Эмоции" и тип "Промпты"
|
|
||||||
if (block.id === 'emotionPromptsSelection') {
|
|
||||||
if (selectedStyle === 'emotions' && selectedEmotionType === 'prompts') {
|
|
||||||
// Если выбран стиль "Эмоции" и тип "Промпты", то сначала добавляем поле ввода текста,
|
|
||||||
// если оно должно быть видимым
|
|
||||||
if (isInputVisible) {
|
|
||||||
const textInputBlock = allBlocks.find(b => b.id === 'customPrompt');
|
|
||||||
if (textInputBlock) {
|
|
||||||
blocksToRender.push(textInputBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Затем добавляем блок с промптами
|
|
||||||
blocksToRender.push(block);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Блок с пресетами (quickActions) показываем только если выбран стиль "Чиби" или не выбран стиль "Эмоции"
|
|
||||||
if (block.id === 'quickActions') {
|
|
||||||
if (selectedStyle !== 'emotions') {
|
|
||||||
blocksToRender.push(block);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Блок с заголовком "Выбери образ" (step3) показываем только если не выбран стиль "Эмоции" или уже выбран тип эмоций
|
|
||||||
if (block.id === 'step3') {
|
|
||||||
if (selectedStyle !== 'emotions' || selectedEmotionType !== undefined) {
|
|
||||||
blocksToRender.push(block);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Остальные блоки показываем всегда
|
|
||||||
blocksToRender.push(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отображаем блоки
|
|
||||||
return blocksToRender.map((block) => {
|
|
||||||
// Создаем копию блока с модифицированными кнопками
|
|
||||||
const modifiedBlock = {
|
|
||||||
...block,
|
|
||||||
buttons: getBlockButtons(block)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Определяем, какой ID выбранной кнопки передавать в зависимости от типа блока
|
|
||||||
let selectedButtonId;
|
|
||||||
if (block.id === 'styleActions') {
|
|
||||||
// Для блока стилей передаем ID кнопки выбранного стиля
|
|
||||||
selectedButtonId = selectedStyleButtonId;
|
|
||||||
} else if (block.id === 'quickActions') {
|
|
||||||
// Для блока пресетов передаем ID выбранного пресета
|
|
||||||
selectedButtonId = selectedPresetId;
|
|
||||||
} else if (block.id === 'genderSelection') {
|
|
||||||
// Для блока выбора пола передаем ID выбранной кнопки пола
|
|
||||||
selectedButtonId = selectedGenderButtonId;
|
|
||||||
} else if (block.id === 'emotionTypeSelection') {
|
|
||||||
// Для блока выбора типа эмоций передаем ID выбранной кнопки типа эмоций
|
|
||||||
selectedButtonId = selectedEmotionTypeButtonId;
|
|
||||||
} else if (block.id === 'memeSelection') {
|
|
||||||
// Для блока выбора мема передаем ID выбранного мема
|
|
||||||
selectedButtonId = selectedMemeId;
|
|
||||||
} else if (block.id === 'emotionPromptsSelection') {
|
|
||||||
// Для блока выбора промптов для эмоций передаем ID выбранного пресета
|
|
||||||
selectedButtonId = selectedPresetId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BlockRenderer
|
<BlockRenderer
|
||||||
key={block.id}
|
block={titleBlock}
|
||||||
block={modifiedBlock}
|
onAction={() => {}}
|
||||||
onAction={handleBlockAction}
|
|
||||||
selectedButtonId={selectedButtonId}
|
|
||||||
extraProps={block.type === 'textInput' ? {
|
|
||||||
visible: isInputVisible,
|
|
||||||
onTextChange: setCustomPrompt
|
|
||||||
} : undefined}
|
|
||||||
/>
|
/>
|
||||||
);
|
) : null;
|
||||||
});
|
|
||||||
})()}
|
})()}
|
||||||
</div>
|
|
||||||
{homeScreenConfig.homeScreen.blocks
|
{/* Компонент выбора типа эмоций */}
|
||||||
.filter(block => block.type === 'generateButton')
|
<EmotionTypeSelector
|
||||||
.map((block) => (
|
selectedEmotionTypeButtonId={selectedEmotionTypeButtonId}
|
||||||
<div className={styles.generateButtonContainer} key={block.id}>
|
onEmotionTypeSelect={handleEmotionTypeSelect}
|
||||||
<BlockRenderer
|
visible={selectedStyle === 'emotions'}
|
||||||
block={block}
|
|
||||||
onAction={handleBlockAction}
|
|
||||||
extraProps={undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Компонент выбора мема или пресета в зависимости от выбранного типа эмоций */}
|
||||||
|
{selectedStyle === 'emotions' && selectedEmotionType === 'memes' ? (
|
||||||
|
<MemeSelector
|
||||||
|
selectedMemeId={selectedMemeId}
|
||||||
|
onMemeSelect={handleMemeSelect}
|
||||||
|
visible={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PresetSelector
|
||||||
|
selectedPresetId={selectedPresetId}
|
||||||
|
onPresetSelect={handlePresetSelect}
|
||||||
|
selectedStyle={selectedStyle}
|
||||||
|
selectedEmotionType={selectedEmotionType}
|
||||||
|
isInputVisible={isInputVisible}
|
||||||
|
onToggleInput={handleToggleInput}
|
||||||
|
onCustomPromptChange={handleCustomPromptChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
{/* Компонент кнопки генерации */}
|
||||||
|
<GenerationButton
|
||||||
|
onStartGeneration={startGeneration}
|
||||||
|
isGenerating={isGenerating}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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