From b9c026a23ce71010e54c0b8069ba84efdfbc3087 Mon Sep 17 00:00:00 2001 From: kazachilo Date: Tue, 25 Mar 2025 17:53:25 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=85=D0=B0=D0=BD=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B2=20=D0=B3?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D1=80=D0=B5=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - обавлено удаление стикеров по file_id - зменен механизм выхода из режима удаления (теперь по касанию в любое место кроме крестика) - далена кнопка 'отово' и связанные стили - лучшен UX режима удаления --- .../shared/ImageWithFallback.module.css | 108 ++++++++ src/components/shared/ImageWithFallback.tsx | 150 +++++++++++ .../shared/NotificationModal.module.css | 1 + src/components/shared/NotificationModal.tsx | 36 +-- src/screens/Gallery.module.css | 46 ++++ src/screens/Gallery.tsx | 102 +++++++- src/screens/Home.tsx | 50 +++- src/services/api.ts | 234 ++++++++++-------- 8 files changed, 595 insertions(+), 132 deletions(-) create mode 100644 src/components/shared/ImageWithFallback.module.css create mode 100644 src/components/shared/ImageWithFallback.tsx diff --git a/src/components/shared/ImageWithFallback.module.css b/src/components/shared/ImageWithFallback.module.css new file mode 100644 index 0000000..ccae8b8 --- /dev/null +++ b/src/components/shared/ImageWithFallback.module.css @@ -0,0 +1,108 @@ +.container { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + background-color: var(--color-surface-variant, #f0f0f0); + display: flex; + justify-content: center; + align-items: center; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; + transition: opacity 0.3s ease; +} + +.hidden { + opacity: 0; + position: absolute; + z-index: -1; +} + +.loadingContainer { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--color-surface-variant, #f0f0f0); + z-index: 1; +} + +.spinner { + width: 30px; + height: 30px; + border: 3px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top-color: var(--color-primary, #3498db); + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.errorContainer { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: var(--color-surface-variant, #f0f0f0); + z-index: 2; + padding: 10px; + text-align: center; +} + +.errorIcon { + width: 30px; + height: 30px; + border-radius: 50%; + background-color: var(--color-error, #d32f2f); + color: white; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 18px; + margin-bottom: 8px; +} + +.errorMessage { + color: var(--color-text, #333); + font-size: 12px; + margin-bottom: 10px; +} + +.retryButton { + background-color: var(--color-primary, #3498db); + color: white; + border: none; + border-radius: var(--border-radius, 4px); + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s; +} + +.retryButton:hover { + background-color: var(--color-primary-dark, #2980b9); +} + +/* Для мобильных устройств делаем кнопку больше для удобства нажатия */ +@media (max-width: 768px) { + .retryButton { + padding: 8px 16px; + font-size: 14px; + } +} diff --git a/src/components/shared/ImageWithFallback.tsx b/src/components/shared/ImageWithFallback.tsx new file mode 100644 index 0000000..d841116 --- /dev/null +++ b/src/components/shared/ImageWithFallback.tsx @@ -0,0 +1,150 @@ +import React, { useState, useEffect, useRef } from 'react'; +import styles from './ImageWithFallback.module.css'; + +interface ImageWithFallbackProps { + src: string; + alt: string; + className?: string; + onClick?: () => void; + maxRetries?: number; +} + +const ImageWithFallback: React.FC = ({ + src, + alt, + className = '', + onClick, + maxRetries = 2 // По умолчанию 2 попытки автоматической перезагрузки +}) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [retryCount, setRetryCount] = useState(0); + const [imageSrc, setImageSrc] = useState(src); + const imgRef = useRef(null); + + // Сбрасываем состояние при изменении src + useEffect(() => { + setLoading(true); + setError(false); + setRetryCount(0); + setImageSrc(src); + }, [src]); + + // Проверяем, загружено ли изображение из кэша + useEffect(() => { + // Если изображение уже загружено (из кэша), сразу устанавливаем состояние + if (imgRef.current && imgRef.current.complete && imgRef.current.naturalWidth > 0) { + console.log('Изображение уже загружено (из кэша):', src); + setLoading(false); + setError(false); + } + + // Устанавливаем таймаут для предотвращения бесконечной загрузки + const timeoutId = setTimeout(() => { + if (loading) { + console.log('Таймаут загрузки изображения:', src); + // Проверяем, загружено ли изображение фактически + if (imgRef.current && imgRef.current.complete && imgRef.current.naturalWidth > 0) { + // Изображение загружено, но событие onLoad не сработало + console.log('Изображение фактически загружено, но событие onLoad не сработало:', src); + setLoading(false); + setError(false); + } else { + // Изображение действительно не загрузилось + console.error('Изображение не загрузилось после таймаута:', src); + setLoading(false); + setError(true); + } + } + }, 5000); // 5 секунд + + return () => clearTimeout(timeoutId); + }, [loading, src]); + + // Функция для перезагрузки изображения + const handleRetry = () => { + setLoading(true); + setError(false); + // Добавляем случайный параметр к URL для предотвращения кэширования + setImageSrc(`${src}${src.includes('?') ? '&' : '?'}retry=${Date.now()}`); + setRetryCount(prevCount => prevCount + 1); + }; + + // Обработчик успешной загрузки + const handleLoad = () => { + console.log('Изображение успешно загружено:', src); + setLoading(false); + setError(false); + }; + + // Обработчик ошибки загрузки + const handleError = () => { + console.error('Ошибка загрузки изображения:', src); + setLoading(false); + setError(true); + + // Автоматически пытаемся перезагрузить изображение, если не превышено максимальное количество попыток + if (retryCount < maxRetries) { + console.log(`Автоматическая попытка перезагрузки изображения (${retryCount + 1}/${maxRetries}):`, src); + // Увеличиваем задержку с каждой попыткой (экспоненциальный backoff) + const delay = Math.pow(2, retryCount) * 1000; + setTimeout(handleRetry, delay); // Пауза перед повторной попыткой + } else { + console.error('Не удалось загрузить изображение после нескольких попыток:', src); + } + }; + + // Обработчик клика по изображению + const handleClick = () => { + // Если есть ошибка и пользователь кликает на изображение с ошибкой, пытаемся перезагрузить + if (error) { + handleRetry(); + } else if (onClick) { + // Иначе вызываем переданный обработчик клика + onClick(); + } + }; + + return ( +
+ {/* Показываем индикатор загрузки, если изображение загружается */} + {loading && ( +
+
+
+ )} + + {/* Показываем изображение */} + {alt} + + {/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка */} + {error && ( +
+
!
+
Ошибка загрузки
+ +
+ )} +
+ ); +}; + +export default ImageWithFallback; diff --git a/src/components/shared/NotificationModal.module.css b/src/components/shared/NotificationModal.module.css index 7c77dad..e98c966 100644 --- a/src/components/shared/NotificationModal.module.css +++ b/src/components/shared/NotificationModal.module.css @@ -44,6 +44,7 @@ font-size: 14px; margin-bottom: var(--spacing-small); line-height: 1.4; + white-space: pre-line; } .promptContainer { diff --git a/src/components/shared/NotificationModal.tsx b/src/components/shared/NotificationModal.tsx index e184539..e9879fa 100644 --- a/src/components/shared/NotificationModal.tsx +++ b/src/components/shared/NotificationModal.tsx @@ -9,7 +9,9 @@ interface NotificationModalProps { promptText?: string; onGalleryClick: () => void; onContinueClick: () => void; - showGalleryButton?: boolean; // Новый параметр для управления видимостью кнопки "В галерею" + showGalleryButton?: boolean; // Параметр для управления видимостью кнопки "В галерею" + showButtons?: boolean; // Новый параметр для управления видимостью всех кнопок + continueButtonText?: string; // Новый параметр для изменения текста кнопки "Продолжить" } const NotificationModal: React.FC = ({ @@ -20,7 +22,9 @@ const NotificationModal: React.FC = ({ promptText, onGalleryClick, onContinueClick, - showGalleryButton = true // По умолчанию кнопка видима + showGalleryButton = true, // По умолчанию кнопка "В галерею" видима + showButtons = true, // По умолчанию все кнопки видимы + continueButtonText = 'Продолжить' // По умолчанию текст кнопки "Продолжить" }) => { if (!isVisible) return null; @@ -42,22 +46,24 @@ const NotificationModal: React.FC = ({ )} -
- {showGalleryButton && ( + {showButtons && ( +
+ {showGalleryButton && ( + + )} - )} - -
+
+ )} ); diff --git a/src/screens/Gallery.module.css b/src/screens/Gallery.module.css index 406f9b1..93b2c48 100644 --- a/src/screens/Gallery.module.css +++ b/src/screens/Gallery.module.css @@ -114,6 +114,52 @@ background-color: var(--color-surface); cursor: pointer; -webkit-tap-highlight-color: transparent; /* Убираем подсветку при тапе на мобильных */ + position: relative; +} + +.deleteMode .imageItem { + opacity: 0.8; +} + +.deleteButton { + position: absolute; + top: 8px; + right: 8px; + width: 24px; + height: 24px; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 2; + color: white; + font-size: 16px; +} + +.deleteControls { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 16px; + background-color: var(--color-surface); + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: center; + z-index: 10; +} + +.exitDeleteModeButton { + padding: 10px 24px; + background-color: var(--color-primary, #3498db); + color: white; + border: none; + border-radius: var(--border-radius); + font-size: 16px; + font-weight: bold; + cursor: pointer; } .image { diff --git a/src/screens/Gallery.tsx b/src/screens/Gallery.tsx index 14e9c5b..9b5ad2e 100644 --- a/src/screens/Gallery.tsx +++ b/src/screens/Gallery.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef, useCallback } from 'react'; import styles from './Gallery.module.css'; import apiService from '../services/api'; import { GeneratedImage, PendingTask } from '../types/api'; import ImageViewer from '../components/shared/ImageViewer'; +import ImageWithFallback from '../components/shared/ImageWithFallback'; +import NotificationModal from '../components/shared/NotificationModal'; const GalleryScreen: React.FC = () => { const [images, setImages] = useState([]); @@ -16,6 +18,12 @@ const GalleryScreen: React.FC = () => { const containerRef = useRef(null); const startY = useRef(null); const threshold = 80; // Порог для активации обновления + + // Состояния для режима удаления + const [isDeleteMode, setIsDeleteMode] = useState(false); + const [selectedForDelete, setSelectedForDelete] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const longPressTimer = useRef(null); // Функция для расчета времени ожидания const getEstimatedWaitTime = (queuePosition: number | null): string => { @@ -26,8 +34,66 @@ const GalleryScreen: React.FC = () => { return `${Math.floor(seconds / 60)} мин ${seconds % 60} сек`; }; + // Обработчики для режима удаления + const handleLongPress = useCallback((image: GeneratedImage) => { + setIsDeleteMode(true); + }, []); + + const startLongPressTimer = useCallback((image: GeneratedImage) => { + longPressTimer.current = setTimeout(() => { + handleLongPress(image); + }, 800); // 800ms для долгого нажатия + }, [handleLongPress]); + + const cancelLongPressTimer = useCallback(() => { + if (longPressTimer.current) { + clearTimeout(longPressTimer.current); + longPressTimer.current = null; + } + }, []); + + const handleDeleteClick = useCallback((image: GeneratedImage) => { + setSelectedForDelete(image); + }, []); + + const handleConfirmDelete = useCallback(async () => { + if (selectedForDelete) { + try { + setIsDeleting(true); + await apiService.deleteImage(selectedForDelete.link); + + // Обновляем список изображений + setImages(prevImages => + prevImages.filter(img => img.id !== selectedForDelete.id) + ); + + setIsDeleting(false); + setSelectedForDelete(null); + } catch (error) { + console.error('Error deleting image:', error); + setIsDeleting(false); + } + } + }, [selectedForDelete]); + + const exitDeleteMode = useCallback(() => { + setIsDeleteMode(false); + setSelectedForDelete(null); + }, []); + + // Обработчик клика по контейнеру для выхода из режима удаления + const handleGridClick = useCallback((e: React.MouseEvent) => { + // Проверяем, что клик был не по крестику удаления + if (isDeleteMode && !(e.target as HTMLElement).closest(`.${styles.deleteButton}`)) { + setIsDeleteMode(false); + setSelectedForDelete(null); + } + }, [isDeleteMode]); + // Функция для обновления всех данных const refreshAll = async () => { + // Выходим из режима удаления при обновлении + setIsDeleteMode(false); setRefreshing(true); try { // Загружаем изображения @@ -227,22 +293,38 @@ const GalleryScreen: React.FC = () => { )} {!loading && !error && images.length > 0 && ( -
+
{images.map((image, index) => (
image.url && setSelectedImage(image.url)} + onTouchStart={() => !isDeleteMode && startLongPressTimer(image)} + onTouchEnd={() => !isDeleteMode && cancelLongPressTimer()} + onTouchMove={() => !isDeleteMode && cancelLongPressTimer()} > - {`Стикер !isDeleteMode && image.url && setSelectedImage(image.url)} + maxRetries={3} /> + {isDeleteMode && ( +
handleDeleteClick(image)} + > + ✕ +
+ )}
))}
)} +
{/* Полноэкранный просмотр */} @@ -252,6 +334,18 @@ const GalleryScreen: React.FC = () => { onClose={() => setSelectedImage(null)} /> )} + + {/* Модальное окно подтверждения удаления */} + setSelectedForDelete(null)} + /> ); }; diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx index 11e2c28..276bfd9 100644 --- a/src/screens/Home.tsx +++ b/src/screens/Home.tsx @@ -45,6 +45,8 @@ const Home: React.FC = () => { 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({}); @@ -82,7 +84,9 @@ const Home: React.FC = () => { setNotificationTitle('Внимание'); setNotificationMessage('Сначала загрузите изображение'); setIsLoading(false); - setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений + setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена + setShowButtons(true); // Показываем кнопки + setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" setIsNotificationVisible(true); return; } @@ -92,7 +96,9 @@ const Home: React.FC = () => { setNotificationTitle('Внимание'); setNotificationMessage('Выберите образ для генерации'); setIsLoading(false); - setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений + setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена + setShowButtons(true); // Показываем кнопки + setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" setIsNotificationVisible(true); return; } @@ -102,7 +108,9 @@ const Home: React.FC = () => { setNotificationTitle('Внимание'); setNotificationMessage('Введите текст промпта'); setIsLoading(false); - setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений + setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена + setShowButtons(true); // Показываем кнопки + setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" setIsNotificationVisible(true); return; } @@ -136,7 +144,9 @@ const Home: React.FC = () => { setNotificationTitle('Внимание'); setNotificationMessage('Нельзя отправить одну и ту же комбинацию изображения и образа подряд. Пожалуйста, измените изображение или выберите другой образ.'); setIsLoading(false); - setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений + setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была запущена + setShowButtons(true); // Показываем кнопки + setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" setIsNotificationVisible(true); return; } @@ -148,6 +158,8 @@ const Home: React.FC = () => { setIsLoading(true); setPromptText(''); setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений о генерации + setShowButtons(false); // Скрываем все кнопки во время отправки запроса + setContinueButtonText('Продолжить'); // Сбрасываем текст кнопки на значение по умолчанию setIsNotificationVisible(true); // Если выбран "Свой промпт" и введен текст, используем его @@ -176,6 +188,9 @@ const Home: React.FC = () => { } setIsLoading(false); + setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была успешно запущена + setShowButtons(true); // Показываем кнопки в случае ошибки перевода + setContinueButtonText('Закрыть'); // Меняем текст кнопки на "Закрыть" return; } @@ -192,14 +207,25 @@ const Home: React.FC = () => { const timeString = minutes > 0 ? `${minutes} мин ${seconds} сек` : `${seconds} сек`; - - setNotificationMessage(`Ваша задача отправлена на генерацию!\nПозиция в очереди: ${result.queue_position}\nПримерное время ожидания: ${timeString}`); + + // Форматируем сообщение в выбранном формате (Вариант 2) + setNotificationMessage( + `Создание стикеров началось!\n` + + `Позиция в очереди: ${result.queue_position}\n` + + `Время ожидания: ${timeString}\n\n` + + `Результат будет доступен в галерее после завершения генерации.` + ); } else { - setNotificationMessage('Ваша задача отправлена на генерацию!'); + setNotificationMessage( + `Создание стикеров началось!\n\n` + + `Результат будет доступен в галерее после завершения генерации.` + ); } - // Устанавливаем использованный промпт и убираем индикатор загрузки + // Устанавливаем использованный промпт, показываем кнопки и меняем текст кнопки "Продолжить" на "Закрыть" setPromptText(usedPrompt); + setShowButtons(true); + setContinueButtonText('Закрыть'); } setIsLoading(false); @@ -208,7 +234,9 @@ const Home: React.FC = () => { setNotificationTitle('Ошибка'); setNotificationMessage('Не удалось начать генерацию'); setIsLoading(false); - setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений + setShowGalleryButton(false); // Скрываем кнопку "В галерею", так как генерация не была успешно запущена + setShowButtons(true); // Показываем кнопки в случае ошибки + setContinueButtonText('Закрыть'); // Меняем текст кнопки на "Закрыть" setIsNotificationVisible(true); } return; @@ -307,6 +335,8 @@ const Home: React.FC = () => { onGalleryClick={handleGalleryClick} onContinueClick={handleContinueClick} showGalleryButton={showGalleryButton} + showButtons={showButtons} + continueButtonText={continueButtonText} /> {/* Компонент обработки обратной связи */} @@ -318,6 +348,8 @@ const Home: React.FC = () => { setNotificationMessage('Ваше сообщение успешно отправлено'); setIsLoading(false); setShowGalleryButton(false); // Скрываем кнопку "В галерею" для уведомления об обратной связи + setShowButtons(true); // Показываем кнопки + setContinueButtonText('Закрыть'); // Устанавливаем текст кнопки "Закрыть" setIsNotificationVisible(true); }} /> diff --git a/src/services/api.ts b/src/services/api.ts index 4165a6a..b9b7ba1 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -111,122 +111,148 @@ const apiService = { throw new Error('Failed to fetch images'); } - // Получаем массив строк с file_id - const fileIds = await response.json() as string[]; + // Получаем массив массивов [file_id, created_at] + const rawData = await response.json() as [string, string][]; - // Преобразуем массив строк в массив объектов GeneratedImage - const images = fileIds.map((fileId, index) => ({ - id: index + 1, - link: fileId, - prompt_id: '', - status: 'COMPLETED', - created_at: new Date().toISOString(), - sticker_set_id: null, - url: `${API_BASE_URL}/stickers/proxy/sticker/${encodeURIComponent(fileId)}` - })); + // Преобразуем в массив объектов GeneratedImage + const images = rawData.map((item, index) => { + const [fileId, createdAt] = item; + return { + id: index + 1, + link: fileId, + prompt_id: '', + status: 'COMPLETED', + created_at: createdAt, + sticker_set_id: null, + url: `${API_BASE_URL}/stickers/proxy/sticker/${encodeURIComponent(fileId)}` + }; + }); - // Сортируем изображения от новых к старым (по id в обратном порядке) - return images.sort((a, b) => b.id - a.id); + // Сортируем изображения от новых к старым по дате создания + return images.sort((a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); } catch (error) { console.error('Error fetching images:', error); throw new Error('Failed to fetch images'); } }, -async generateImage(imageData: string, style?: string, promptId?: string, userPrompt?: string): Promise { - try { - // Создаем копию базового воркфлоу - const workflow = JSON.parse(JSON.stringify(baseWorkflow)); - - // Вставляем изображение в base64 формате в узел 563 - workflow['563'].inputs.image = imageData; - - // Переменная для хранения использованного промпта - let usedPrompt = ''; - let translationFailed = false; - - // Если указан пользовательский промпт и выбрана кнопка "Свой промпт" - if (userPrompt && promptId === 'customPrompt') { - console.log('Переводим пользовательский промпт:', userPrompt); - - // Используем новый метод перевода через LLM - const translationResult = await translateService.translateWithLLM(userPrompt); - - if (translationResult.success) { - // Успешный перевод - console.log('Переведенный промпт:', translationResult.text); - workflow['316'].inputs.prompt_1 = translationResult.text; - usedPrompt = translationResult.text; - } else { - // Перевод не удался после всех попыток - console.error('Не удалось перевести промпт:', translationResult.text); - translationFailed = true; - - // Не продолжаем генерацию, возвращаем ошибку с сообщением - return { - translationFailed: true, - usedPrompt: 'Недопустимый промпт', // Сообщение для пользователя - errorDetails: translationResult.text // Детали ошибки для отладки - }; + // Удаление изображения по file_id + async deleteImage(fileId: string): Promise { + try { + const response = await fetch(`${API_BASE_URL}/delete-by-link?link=${encodeURIComponent(fileId)}`, { + method: 'DELETE', + headers: { + 'accept': 'application/json', + } + }); + + if (!response.ok) { + throw new Error('Failed to delete image'); } - } - // Иначе используем предустановленный промпт (они уже переведены) - else if (promptId && prompts[promptId]) { - workflow['316'].inputs.prompt_1 = prompts[promptId]; - usedPrompt = prompts[promptId]; - console.log('Используем предустановленный промпт:', prompts[promptId]); - } - - // Если был сбой перевода, не продолжаем генерацию - if (translationFailed) { - return { translationFailed: true }; - } - - // Создаем строку JSON для workflow ПОСЛЕ того, как все изменения внесены - const workflowJson = JSON.stringify(workflow); - - // Определяем тег на основе выбранного стиля - const tag = styleToTagMap[style || 'chibi'] || 'chibi'; - console.log(`Используем тег "${tag}" для стиля "${style || 'chibi'}"`); - - // Создаем тело запроса как строку JSON вручную - const requestBodyJson = `{"tag":"${tag}","user_id":${getCurrentUserId()},"workflow":${workflowJson}}`; - - // Сохраняем JSON для отладки только при локальной разработке - if (!isTelegramWebAppAvailable()) { - const blob = new Blob([requestBodyJson], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'generation_request.json'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - - // Отправляем запрос - const response = await fetch(`${API_BASE_URL}/generate_image`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: requestBodyJson - }); - if (!response.ok) { - const errorData: ApiErrorType = await response.json(); - throw new GenerationError(errorData.detail); + return await response.json(); + } catch (error) { + console.error('Error deleting image:', error); + throw new Error('Failed to delete image'); } + }, + + async generateImage(imageData: string, style?: string, promptId?: string, userPrompt?: string): Promise { + try { + // Создаем копию базового воркфлоу + const workflow = JSON.parse(JSON.stringify(baseWorkflow)); + + // Вставляем изображение в base64 формате в узел 563 + workflow['563'].inputs.image = imageData; + + // Переменная для хранения использованного промпта + let usedPrompt = ''; + let translationFailed = false; + + // Если указан пользовательский промпт и выбрана кнопка "Свой промпт" + if (userPrompt && promptId === 'customPrompt') { + console.log('Переводим пользовательский промпт:', userPrompt); + + // Используем новый метод перевода через LLM + const translationResult = await translateService.translateWithLLM(userPrompt); + + if (translationResult.success) { + // Успешный перевод + console.log('Переведенный промпт:', translationResult.text); + workflow['316'].inputs.prompt_1 = translationResult.text; + usedPrompt = translationResult.text; + } else { + // Перевод не удался после всех попыток + console.error('Не удалось перевести промпт:', translationResult.text); + translationFailed = true; + + // Не продолжаем генерацию, возвращаем ошибку с сообщением + return { + translationFailed: true, + usedPrompt: 'Недопустимый промпт', // Сообщение для пользователя + errorDetails: translationResult.text // Детали ошибки для отладки + }; + } + } + // Иначе используем предустановленный промпт (они уже переведены) + else if (promptId && prompts[promptId]) { + workflow['316'].inputs.prompt_1 = prompts[promptId]; + usedPrompt = prompts[promptId]; + console.log('Используем предустановленный промпт:', prompts[promptId]); + } + + // Если был сбой перевода, не продолжаем генерацию + if (translationFailed) { + return { translationFailed: true }; + } + + // Создаем строку JSON для workflow ПОСЛЕ того, как все изменения внесены + const workflowJson = JSON.stringify(workflow); + + // Определяем тег на основе выбранного стиля + const tag = styleToTagMap[style || 'chibi'] || 'chibi'; + console.log(`Используем тег "${tag}" для стиля "${style || 'chibi'}"`); + + // Создаем тело запроса как строку JSON вручную + const requestBodyJson = `{"tag":"${tag}","user_id":${getCurrentUserId()},"workflow":${workflowJson}}`; + + // Сохраняем JSON для отладки только при локальной разработке + if (!isTelegramWebAppAvailable()) { + const blob = new Blob([requestBodyJson], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'generation_request.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + // Отправляем запрос + const response = await fetch(`${API_BASE_URL}/generate_image`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: requestBodyJson + }); - const result = await response.json() as GenerationResponse; - - // Возвращаем результат и использованный промпт - return { - result, - usedPrompt, - translationFailed: false - }; + if (!response.ok) { + const errorData: ApiErrorType = await response.json(); + throw new GenerationError(errorData.detail); + } + + const result = await response.json() as GenerationResponse; + + // Возвращаем результат и использованный промпт + return { + result, + usedPrompt, + translationFailed: false + }; } catch (error) { if (error instanceof GenerationError) { throw error;