feat: обновлен механизм удаления стикеров в галерее

- обавлено удаление стикеров по file_id
- зменен механизм выхода из режима удаления (теперь по касанию в любое место кроме крестика)
- далена кнопка 'отово' и связанные стили
- лучшен UX режима удаления
This commit is contained in:
kazachilo 2025-03-25 17:53:25 +03:00
parent 6426b05597
commit b9c026a23c
8 changed files with 595 additions and 132 deletions

View File

@ -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;
}
}

View File

@ -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<ImageWithFallbackProps> = ({
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<HTMLImageElement>(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 (
<div
className={`${styles.container} ${className}`}
onClick={handleClick}
>
{/* Показываем индикатор загрузки, если изображение загружается */}
{loading && (
<div className={styles.loadingContainer}>
<div className={styles.spinner}></div>
</div>
)}
{/* Показываем изображение */}
<img
ref={imgRef}
src={imageSrc}
alt={alt}
className={`${styles.image} ${error ? styles.hidden : ''}`}
onLoad={handleLoad}
onError={handleError}
/>
{/* Показываем сообщение об ошибке и кнопку перезагрузки, если произошла ошибка */}
{error && (
<div className={styles.errorContainer}>
<div className={styles.errorIcon}>!</div>
<div className={styles.errorMessage}>Ошибка загрузки</div>
<button
className={styles.retryButton}
onClick={(e) => {
e.stopPropagation(); // Предотвращаем всплытие события
handleRetry();
}}
>
Перезагрузить
</button>
</div>
)}
</div>
);
};
export default ImageWithFallback;

View File

@ -44,6 +44,7 @@
font-size: 14px;
margin-bottom: var(--spacing-small);
line-height: 1.4;
white-space: pre-line;
}
.promptContainer {

View File

@ -9,7 +9,9 @@ interface NotificationModalProps {
promptText?: string;
onGalleryClick: () => void;
onContinueClick: () => void;
showGalleryButton?: boolean; // Новый параметр для управления видимостью кнопки "В галерею"
showGalleryButton?: boolean; // Параметр для управления видимостью кнопки "В галерею"
showButtons?: boolean; // Новый параметр для управления видимостью всех кнопок
continueButtonText?: string; // Новый параметр для изменения текста кнопки "Продолжить"
}
const NotificationModal: React.FC<NotificationModalProps> = ({
@ -20,7 +22,9 @@ const NotificationModal: React.FC<NotificationModalProps> = ({
promptText,
onGalleryClick,
onContinueClick,
showGalleryButton = true // По умолчанию кнопка видима
showGalleryButton = true, // По умолчанию кнопка "В галерею" видима
showButtons = true, // По умолчанию все кнопки видимы
continueButtonText = 'Продолжить' // По умолчанию текст кнопки "Продолжить"
}) => {
if (!isVisible) return null;
@ -42,22 +46,24 @@ const NotificationModal: React.FC<NotificationModalProps> = ({
</div>
)}
<div className={styles.buttons}>
{showGalleryButton && (
{showButtons && (
<div className={styles.buttons}>
{showGalleryButton && (
<button
className={`${styles.button} ${styles.primaryButton}`}
onClick={onGalleryClick}
>
В галерею
</button>
)}
<button
className={`${styles.button} ${styles.primaryButton}`}
onClick={onGalleryClick}
className={`${styles.button} ${!showGalleryButton ? styles.primaryButton : styles.secondaryButton}`}
onClick={onContinueClick}
>
В галерею
{continueButtonText}
</button>
)}
<button
className={`${styles.button} ${!showGalleryButton ? styles.primaryButton : styles.secondaryButton}`}
onClick={onContinueClick}
>
Продолжить
</button>
</div>
</div>
)}
</div>
</div>
);

View File

@ -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 {

View File

@ -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<GeneratedImage[]>([]);
@ -16,6 +18,12 @@ const GalleryScreen: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const startY = useRef<number | null>(null);
const threshold = 80; // Порог для активации обновления
// Состояния для режима удаления
const [isDeleteMode, setIsDeleteMode] = useState(false);
const [selectedForDelete, setSelectedForDelete] = useState<GeneratedImage | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const longPressTimer = useRef<number | null>(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 && (
<div className={styles.imageGrid}>
<div
className={`${styles.imageGrid} ${isDeleteMode ? styles.deleteMode : ''}`}
onClick={handleGridClick}
>
{images.map((image, index) => (
<div
key={image.id}
className={styles.imageItem}
onClick={() => image.url && setSelectedImage(image.url)}
onTouchStart={() => !isDeleteMode && startLongPressTimer(image)}
onTouchEnd={() => !isDeleteMode && cancelLongPressTimer()}
onTouchMove={() => !isDeleteMode && cancelLongPressTimer()}
>
<img
<ImageWithFallback
src={image.url || ''}
alt={`Стикер ${index + 1}`}
className={styles.image}
onClick={() => !isDeleteMode && image.url && setSelectedImage(image.url)}
maxRetries={3}
/>
{isDeleteMode && (
<div
className={styles.deleteButton}
onClick={() => handleDeleteClick(image)}
>
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Полноэкранный просмотр */}
@ -252,6 +334,18 @@ const GalleryScreen: React.FC = () => {
onClose={() => setSelectedImage(null)}
/>
)}
{/* Модальное окно подтверждения удаления */}
<NotificationModal
isVisible={!!selectedForDelete}
title="Удаление стикера"
message="Вы уверены, что хотите удалить этот стикер?"
isLoading={isDeleting}
showGalleryButton={false}
continueButtonText="Удалить"
onContinueClick={handleConfirmDelete}
onGalleryClick={() => setSelectedForDelete(null)}
/>
</div>
);
};

View File

@ -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<LastGenerationData>({});
@ -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(`Ваша задача отправлена на генерацию!\озиция в очереди: ${result.queue_position}\римерное время ожидания: ${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);
}}
/>

View File

@ -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<GenerationResult> {
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<string> {
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<GenerationResult> {
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;