StickerAI-Front/src/screens/Home.tsx

544 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;