544 lines
27 KiB
TypeScript
544 lines
27 KiB
TypeScript
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import BlockRenderer from '../components/blocks/BlockRenderer';
|
||
import styles from './Home.module.css';
|
||
import { homeScreenConfig } from '../config/homeScreen';
|
||
import { stylePresets } from '../config/stylePresets';
|
||
import apiService from '../services/api';
|
||
import NotificationModal from '../components/shared/NotificationModal';
|
||
import FeedbackHandler, { FeedbackHandlerRef } from '../components/shared/FeedbackHandler';
|
||
import TokenPacksModal from '../components/tokens/TokenPacksModal';
|
||
import { paymentService } from '../services/paymentService';
|
||
import { tokenPacks } from '../constants/tokenPacks';
|
||
import { getCurrentUserId } from '../constants/user';
|
||
import { useBalance } from '../contexts/BalanceContext';
|
||
import { sendTargetEvent } from '../services/analyticsService';
|
||
|
||
// Интерфейс для хранения данных о последней генерации
|
||
interface LastGenerationData {
|
||
imageData?: string;
|
||
style?: string;
|
||
presetId?: string;
|
||
customPrompt?: string;
|
||
}
|
||
|
||
const Home: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const feedbackHandlerRef = useRef<FeedbackHandlerRef>(null);
|
||
const { updateBalance } = useBalance(); // Используем контекст баланса
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
|
||
// Проверяем, есть ли превью в состоянии навигации или localStorage
|
||
const state = window.history.state?.usr;
|
||
return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined;
|
||
});
|
||
|
||
const [imageData, _setImageData] = useState<string | undefined>(() => {
|
||
const state = window.history.state?.usr;
|
||
return state?.imageData || localStorage.getItem('stickerImageData') || undefined;
|
||
});
|
||
const [isInputVisible, setIsInputVisible] = useState(false);
|
||
const [selectedStyle, setSelectedStyle] = useState<string>('chibi'); // По умолчанию выбран первый стиль
|
||
const [selectedStyleButtonId, setSelectedStyleButtonId] = useState<string | undefined>('chibi'); // Для хранения ID выбранной кнопки стиля
|
||
const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(undefined); // Для хранения ID выбранного пресета
|
||
const [customPrompt, setCustomPrompt] = useState<string>(''); // Для хранения пользовательского промпта
|
||
|
||
// Состояния для модального окна уведомления
|
||
const [isNotificationVisible, setIsNotificationVisible] = useState(false);
|
||
const [notificationTitle, setNotificationTitle] = useState('');
|
||
const [notificationMessage, setNotificationMessage] = useState('');
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [promptText, setPromptText] = useState('');
|
||
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 handleGalleryClick = useCallback(() => {
|
||
setIsNotificationVisible(false);
|
||
navigate('/gallery');
|
||
}, [navigate]);
|
||
|
||
const handleContinueClick = useCallback(() => {
|
||
setIsNotificationVisible(false);
|
||
}, []);
|
||
|
||
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);
|
||
return;
|
||
}
|
||
|
||
if (actionType === 'function') {
|
||
if (actionValue === 'startGeneration') {
|
||
// Проверка наличия изображения
|
||
if (!imageData) {
|
||
setNotificationTitle('Внимание');
|
||
setNotificationMessage('Сначала загрузите изображение');
|
||
setIsLoading(false);
|
||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена
|
||
setShowButtons(true); // Показываем кнопки
|
||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||
setIsNotificationVisible(true);
|
||
return;
|
||
}
|
||
|
||
// Проверка выбора пресета промпта
|
||
if (!selectedPresetId) {
|
||
setNotificationTitle('Внимание');
|
||
setNotificationMessage('Выберите образ для генерации');
|
||
setIsLoading(false);
|
||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена
|
||
setShowButtons(true); // Показываем кнопки
|
||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||
setIsNotificationVisible(true);
|
||
return;
|
||
}
|
||
|
||
// Проверка ввода текста, если выбран "Свой промпт"
|
||
if (selectedPresetId === 'customPrompt' && !customPrompt.trim()) {
|
||
setNotificationTitle('Внимание');
|
||
setNotificationMessage('Введите текст промпта');
|
||
setIsLoading(false);
|
||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена
|
||
setShowButtons(true); // Показываем кнопки
|
||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||
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('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||
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;
|
||
}
|
||
|
||
// Показываем уведомление о начале генерации
|
||
setNotificationTitle('Генерация стикера');
|
||
setNotificationMessage('Отправка запроса...');
|
||
setIsLoading(true);
|
||
setPromptText('');
|
||
setShowGalleryButton(true);
|
||
setShowButtons(false);
|
||
setContinueButtonText('Продолжить');
|
||
setIsNotificationVisible(true);
|
||
|
||
// Если выбран "Свой промпт" и введен текст, используем его
|
||
const userPrompt = selectedPresetId === 'customPrompt' && customPrompt ? customPrompt : undefined;
|
||
|
||
// Отправляем запрос на генерацию
|
||
const response = await apiService.generateImage(imageData, selectedStyle, selectedPresetId, userPrompt);
|
||
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('Закрыть'); // Меняем текст кнопки на "Закрыть"
|
||
return;
|
||
}
|
||
|
||
// Если нет ошибки перевода, продолжаем обработку результата
|
||
if (response.result && response.usedPrompt) {
|
||
// Получаем результат и использованный промпт
|
||
const { result, usedPrompt } = 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` +
|
||
`Результат будет доступен в галерее после завершения генерации.`
|
||
);
|
||
}
|
||
|
||
// Устанавливаем использованный промпт, показываем кнопки и меняем текст кнопки "Продолжить" на "Закрыть"
|
||
setPromptText(usedPrompt);
|
||
setShowButtons(true);
|
||
setContinueButtonText('Закрыть');
|
||
}
|
||
|
||
setIsLoading(false);
|
||
} catch (error) {
|
||
console.error('Generation failed:', error);
|
||
setNotificationTitle('Ошибка');
|
||
setNotificationMessage('Не удалось начать генерацию');
|
||
setIsLoading(false);
|
||
setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была успешно запущена
|
||
setShowButtons(true); // Показываем кнопки в случае ошибки
|
||
setContinueButtonText('Закрыть'); // Меняем текст кнопки на "Закрыть"
|
||
setIsNotificationVisible(true);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (actionValue === 'toggleInput') {
|
||
setIsInputVisible(prev => !prev);
|
||
// Устанавливаем selectedPresetId в 'customPrompt' при нажатии на кнопку "Свой промпт"
|
||
setSelectedPresetId('customPrompt');
|
||
console.log('Выбран свой промпт, установлен ID:', 'customPrompt');
|
||
return;
|
||
}
|
||
|
||
if (actionValue === 'openTelegramBot') {
|
||
// Проверяем, доступен ли объект Telegram
|
||
if (window.Telegram && window.Telegram.WebApp) {
|
||
window.Telegram.WebApp.openTelegramLink('https://t.me/youtube_s_loader_bot');
|
||
} else {
|
||
// Запасной вариант, если API Telegram недоступен
|
||
window.open('https://t.me/youtube_s_loader_bot', '_blank');
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (actionValue === 'sendFeedback') {
|
||
// Открываем модальное окно обратной связи
|
||
feedbackHandlerRef.current?.openFeedbackModal();
|
||
return;
|
||
}
|
||
} else if (actionType === 'route') {
|
||
// Добавляем обработку для действий типа 'route'
|
||
navigate(actionValue);
|
||
return;
|
||
}
|
||
|
||
// Если выбрана любая другая кнопка, скрываем поле ввода
|
||
setIsInputVisible(false);
|
||
}, [navigate, imageData, selectedStyle, selectedPresetId, customPrompt, lastGenerationData]);
|
||
|
||
// Эффект для обновления window.history.state при загрузке из localStorage
|
||
useEffect(() => {
|
||
// Если есть данные в localStorage, но нет в history.state, обновляем history.state
|
||
const state = window.history.state?.usr;
|
||
const localStoragePreviewUrl = localStorage.getItem('stickerPreviewUrl');
|
||
const localStorageImageData = localStorage.getItem('stickerImageData');
|
||
|
||
if (!state?.previewUrl && localStoragePreviewUrl && localStorageImageData) {
|
||
window.history.replaceState(
|
||
{
|
||
usr: {
|
||
previewUrl: localStoragePreviewUrl,
|
||
imageData: localStorageImageData
|
||
}
|
||
},
|
||
'',
|
||
window.location.pathname
|
||
);
|
||
}
|
||
}, []);
|
||
|
||
// Эффект для обработки закрытия приложения
|
||
useEffect(() => {
|
||
// Обработчик события beforeunload для очистки данных при закрытии приложения
|
||
const handleBeforeUnload = () => {
|
||
localStorage.removeItem('stickerPreviewUrl');
|
||
localStorage.removeItem('stickerImageData');
|
||
};
|
||
|
||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||
|
||
return () => {
|
||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||
};
|
||
}, []);
|
||
|
||
// Функция для получения кнопок в зависимости от блока
|
||
const getBlockButtons = useCallback((block: any) => {
|
||
if (block.id === 'quickActions') {
|
||
// Берем кнопку "Свой промпт" из конфигурации
|
||
const customPromptButton = block.buttons[0];
|
||
// Добавляем к ней кнопки из выбранного стиля
|
||
return [customPromptButton, ...(stylePresets[selectedStyle]?.buttons || [])];
|
||
}
|
||
return block.buttons;
|
||
}, [selectedStyle]);
|
||
|
||
return (
|
||
<div className={styles.container}>
|
||
{/* Модальное окно уведомления */}
|
||
<NotificationModal
|
||
isVisible={isNotificationVisible}
|
||
title={notificationTitle}
|
||
message={notificationMessage}
|
||
isLoading={isLoading}
|
||
promptText={promptText}
|
||
onGalleryClick={handleGalleryClick}
|
||
onContinueClick={handleContinueClick}
|
||
showGalleryButton={showGalleryButton}
|
||
showButtons={showButtons}
|
||
continueButtonText={continueButtonText}
|
||
isPrimaryGalleryButton={true}
|
||
/>
|
||
|
||
{/* Компонент обработки обратной связи */}
|
||
<FeedbackHandler
|
||
ref={feedbackHandlerRef}
|
||
onFeedbackSent={() => {
|
||
// Показываем уведомление об успешной отправке
|
||
setNotificationTitle('Спасибо за обратную связь');
|
||
setNotificationMessage('Ваше сообщение успешно отправлено');
|
||
setIsLoading(false);
|
||
setShowGalleryButton(false); // Скрываем кнопку "В галерею" для уведомления об обратной связи
|
||
setShowButtons(true); // Показываем кнопки
|
||
setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть"
|
||
setIsNotificationVisible(true);
|
||
}}
|
||
/>
|
||
|
||
<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}>
|
||
{homeScreenConfig.homeScreen.blocks
|
||
.filter(block => block.type !== 'generateButton')
|
||
.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;
|
||
}
|
||
|
||
return (
|
||
<BlockRenderer
|
||
key={block.id}
|
||
block={modifiedBlock}
|
||
onAction={handleBlockAction}
|
||
selectedButtonId={selectedButtonId}
|
||
extraProps={block.type === 'textInput' ? {
|
||
visible: isInputVisible,
|
||
onTextChange: setCustomPrompt
|
||
} : undefined}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
{homeScreenConfig.homeScreen.blocks
|
||
.filter(block => block.type === 'generateButton')
|
||
.map((block) => (
|
||
<div className={styles.generateButtonContainer} key={block.id}>
|
||
<BlockRenderer
|
||
block={block}
|
||
onAction={handleBlockAction}
|
||
extraProps={undefined}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Home;
|