обавлены улучшения: 1) Сохранение изображения в localStorage для сохранения между сеансами навигации 2) еханизм повторных попыток перевода промпта с отображением ошибки при неудаче
This commit is contained in:
parent
b2469b2e10
commit
7a58f26ead
@ -25,7 +25,7 @@ const BlockRenderer: React.FC<BlockRendererProps> = ({ block, onAction, extraPro
|
|||||||
return <GridButtonsBlock block={buttonBlock} onAction={onAction} isInputVisible={extraProps?.visible} />;
|
return <GridButtonsBlock block={buttonBlock} onAction={onAction} isInputVisible={extraProps?.visible} />;
|
||||||
case 'uploadPhoto':
|
case 'uploadPhoto':
|
||||||
return <UploadPhotoBlock
|
return <UploadPhotoBlock
|
||||||
previewUrl={window.history.state?.usr?.previewUrl}
|
previewUrl={window.history.state?.usr?.previewUrl || localStorage.getItem('stickerPreviewUrl')}
|
||||||
onPhotoSelect={(file) => {
|
onPhotoSelect={(file) => {
|
||||||
const tempUrl = URL.createObjectURL(file);
|
const tempUrl = URL.createObjectURL(file);
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
|
|||||||
@ -14,6 +14,10 @@ const UploadPhotoBlock: React.FC<UploadPhotoBlockProps> = ({ onPhotoSelect, prev
|
|||||||
|
|
||||||
const handleFileSelect = (file: File) => {
|
const handleFileSelect = (file: File) => {
|
||||||
if (file && file.type.startsWith('image/')) {
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
// Очищаем предыдущие данные изображения при загрузке нового
|
||||||
|
localStorage.removeItem('stickerPreviewUrl');
|
||||||
|
localStorage.removeItem('stickerImageData');
|
||||||
|
|
||||||
onPhotoSelect?.(file);
|
onPhotoSelect?.(file);
|
||||||
navigate('/crop-photo', { state: { file } });
|
navigate('/crop-photo', { state: { file } });
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/components/shared/NotificationModal.module.css
Normal file
113
src/components/shared/NotificationModal.module.css
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--spacing-medium);
|
||||||
|
width: calc(100% - var(--spacing-medium) * 2);
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 0 var(--spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: var(--spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptContainer {
|
||||||
|
background-color: var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
margin-bottom: var(--spacing-medium);
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptLabel {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.promptText {
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryButton {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryButton:hover {
|
||||||
|
background-color: #1976D2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryButton {
|
||||||
|
background-color: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryButton:hover {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(var(--color-text-rgb), 0.1);
|
||||||
|
border-top-color: var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-right: var(--spacing-small);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/components/shared/NotificationModal.tsx
Normal file
62
src/components/shared/NotificationModal.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './NotificationModal.module.css';
|
||||||
|
|
||||||
|
interface NotificationModalProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
promptText?: string;
|
||||||
|
onGalleryClick: () => void;
|
||||||
|
onContinueClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationModal: React.FC<NotificationModalProps> = ({
|
||||||
|
isVisible,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
isLoading = false,
|
||||||
|
promptText,
|
||||||
|
onGalleryClick,
|
||||||
|
onContinueClick
|
||||||
|
}) => {
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.modal}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{isLoading && <span className={styles.spinner}></span>}
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className={styles.message}>{message}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{promptText && (
|
||||||
|
<div className={styles.promptContainer}>
|
||||||
|
<div className={styles.promptLabel}>Использованный промпт:</div>
|
||||||
|
<div className={styles.promptText}>{promptText}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.primaryButton}`}
|
||||||
|
onClick={onGalleryClick}
|
||||||
|
>
|
||||||
|
В галерею
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.secondaryButton}`}
|
||||||
|
onClick={onContinueClick}
|
||||||
|
>
|
||||||
|
Продолжить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationModal;
|
||||||
@ -263,6 +263,11 @@ const CropPhoto: React.FC = () => {
|
|||||||
// Передаем не только URL, но и base64 данные
|
// Передаем не только URL, но и base64 данные
|
||||||
// Убираем префикс data:image/jpeg;base64, оставляем только данные
|
// Убираем префикс data:image/jpeg;base64, оставляем только данные
|
||||||
const imageData = previewUrl.split(',')[1];
|
const imageData = previewUrl.split(',')[1];
|
||||||
|
|
||||||
|
// Сохраняем данные в localStorage для сохранения между сеансами навигации
|
||||||
|
localStorage.setItem('stickerPreviewUrl', previewUrl);
|
||||||
|
localStorage.setItem('stickerImageData', imageData);
|
||||||
|
|
||||||
navigate('/', {
|
navigate('/', {
|
||||||
state: {
|
state: {
|
||||||
previewUrl,
|
previewUrl,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import BlockRenderer from '../components/blocks/BlockRenderer';
|
import BlockRenderer from '../components/blocks/BlockRenderer';
|
||||||
// import UploadPhotoBlock from '../components/blocks/UploadPhotoBlock'; // Не используется
|
// import UploadPhotoBlock from '../components/blocks/UploadPhotoBlock'; // Не используется
|
||||||
@ -6,26 +6,44 @@ import styles from './Home.module.css';
|
|||||||
import { homeScreenConfig } from '../config/homeScreen';
|
import { homeScreenConfig } from '../config/homeScreen';
|
||||||
import { stylePresets } from '../config/stylePresets';
|
import { stylePresets } from '../config/stylePresets';
|
||||||
import apiService from '../services/api';
|
import apiService from '../services/api';
|
||||||
|
import NotificationModal from '../components/shared/NotificationModal';
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
|
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
|
||||||
// Проверяем, есть ли превью в состоянии навигации
|
// Проверяем, есть ли превью в состоянии навигации или localStorage
|
||||||
const state = window.history.state?.usr;
|
const state = window.history.state?.usr;
|
||||||
return state?.previewUrl;
|
return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [imageData, _setImageData] = useState<string | undefined>(() => {
|
const [imageData, _setImageData] = useState<string | undefined>(() => {
|
||||||
const state = window.history.state?.usr;
|
const state = window.history.state?.usr;
|
||||||
return state?.imageData;
|
return state?.imageData || localStorage.getItem('stickerImageData') || undefined;
|
||||||
});
|
});
|
||||||
const [isInputVisible, setIsInputVisible] = useState(false);
|
const [isInputVisible, setIsInputVisible] = useState(false);
|
||||||
const [selectedStyle, setSelectedStyle] = useState<string>('chibi'); // По умолчанию выбран первый стиль
|
const [selectedStyle, setSelectedStyle] = useState<string>('chibi'); // По умолчанию выбран первый стиль
|
||||||
const [selectedButtonId, setSelectedButtonId] = useState<string | undefined>(undefined); // Для хранения ID выбранной кнопки стиля
|
const [selectedButtonId, setSelectedButtonId] = useState<string | undefined>(undefined); // Для хранения ID выбранной кнопки стиля
|
||||||
const [customPrompt, setCustomPrompt] = useState<string>(''); // Для хранения пользовательского промпта
|
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 handleGalleryClick = useCallback(() => {
|
||||||
|
setIsNotificationVisible(false);
|
||||||
|
navigate('/gallery');
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleContinueClick = useCallback(() => {
|
||||||
|
setIsNotificationVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleBlockAction = useCallback(async (actionType: string, actionValue: string, _blockId?: string, buttonId?: string) => {
|
const handleBlockAction = useCallback(async (actionType: string, actionValue: string, _blockId?: string, buttonId?: string) => {
|
||||||
if (actionType === 'function') {
|
if (actionType === 'function') {
|
||||||
if (actionValue === 'startGeneration') {
|
if (actionValue === 'startGeneration') {
|
||||||
@ -35,30 +53,58 @@ const Home: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Показываем уведомление о начале генерации
|
||||||
|
setNotificationTitle('Генерация стикера');
|
||||||
|
setNotificationMessage('Отправка запроса...');
|
||||||
|
setIsLoading(true);
|
||||||
|
setPromptText('');
|
||||||
|
setIsNotificationVisible(true);
|
||||||
|
|
||||||
// Если выбран "Свой промпт" и введен текст, используем его
|
// Если выбран "Свой промпт" и введен текст, используем его
|
||||||
const userPrompt = selectedButtonId === 'customPrompt' && customPrompt ? customPrompt : undefined;
|
const userPrompt = selectedButtonId === 'customPrompt' && customPrompt ? customPrompt : undefined;
|
||||||
const result = await apiService.generateImage(imageData, selectedStyle, selectedButtonId, userPrompt);
|
|
||||||
console.log('Generation started:', result);
|
|
||||||
|
|
||||||
// Показываем уведомление о позиции в очереди
|
// Отправляем запрос на генерацию
|
||||||
if (result.queue_position !== undefined) {
|
const response = await apiService.generateImage(imageData, selectedStyle, selectedButtonId, userPrompt);
|
||||||
const estimatedTime = apiService.calculateEstimatedWaitTime(result.queue_position);
|
console.log('Generation response:', response);
|
||||||
const minutes = Math.floor(estimatedTime / 60);
|
|
||||||
const seconds = estimatedTime % 60;
|
|
||||||
const timeString = minutes > 0
|
|
||||||
? `${minutes} мин ${seconds} сек`
|
|
||||||
: `${seconds} сек`;
|
|
||||||
|
|
||||||
alert(`Ваша задача отправлена на генерацию!\nПозиция в очереди: ${result.queue_position}\nПримерное время ожидания: ${timeString}`);
|
// Проверяем, была ли ошибка перевода
|
||||||
} else {
|
if (response.translationFailed) {
|
||||||
alert('Ваша задача отправлена на генерацию!');
|
setNotificationTitle('Ошибка перевода');
|
||||||
|
setNotificationMessage('Не удалось перевести промпт. Генерация отменена.');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Перенаправляем пользователя в галерею
|
// Если нет ошибки перевода, продолжаем обработку результата
|
||||||
navigate('/gallery');
|
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} сек`;
|
||||||
|
|
||||||
|
setNotificationMessage(`Ваша задача отправлена на генерацию!\nПозиция в очереди: ${result.queue_position}\nПримерное время ожидания: ${timeString}`);
|
||||||
|
} else {
|
||||||
|
setNotificationMessage('Ваша задача отправлена на генерацию!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем использованный промпт и убираем индикатор загрузки
|
||||||
|
setPromptText(usedPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Generation failed:', error);
|
console.error('Generation failed:', error);
|
||||||
alert('Не удалось начать генерацию');
|
setNotificationTitle('Ошибка');
|
||||||
|
setNotificationMessage('Не удалось начать генерацию');
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsNotificationVisible(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -101,6 +147,27 @@ const Home: React.FC = () => {
|
|||||||
setIsInputVisible(false);
|
setIsInputVisible(false);
|
||||||
}, [navigate, imageData, selectedStyle, selectedButtonId, customPrompt]);
|
}, [navigate, imageData, selectedStyle, selectedButtonId, customPrompt]);
|
||||||
|
|
||||||
|
// Эффект для обновления 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Функция для получения кнопок в зависимости от блока
|
// Функция для получения кнопок в зависимости от блока
|
||||||
const getBlockButtons = useCallback((block: any) => {
|
const getBlockButtons = useCallback((block: any) => {
|
||||||
if (block.id === 'quickActions') {
|
if (block.id === 'quickActions') {
|
||||||
@ -114,6 +181,17 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
{/* Модальное окно уведомления */}
|
||||||
|
<NotificationModal
|
||||||
|
isVisible={isNotificationVisible}
|
||||||
|
title={notificationTitle}
|
||||||
|
message={notificationMessage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
promptText={promptText}
|
||||||
|
onGalleryClick={handleGalleryClick}
|
||||||
|
onContinueClick={handleContinueClick}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* Блоки из конфигурации */}
|
{/* Блоки из конфигурации */}
|
||||||
<div className={styles.blocks}>
|
<div className={styles.blocks}>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { GenerationResponse, ApiError as ApiErrorType, GeneratedImage, PendingTask } from '../types/api';
|
import { GenerationResponse, ApiError as ApiErrorType, GeneratedImage, PendingTask, GenerationResult } from '../types/api';
|
||||||
import { baseWorkflow } from '../constants/baseWorkflow';
|
import { baseWorkflow } from '../constants/baseWorkflow';
|
||||||
import { prompts } from '../assets/prompts';
|
import { prompts } from '../assets/prompts';
|
||||||
import translateService from './translateService';
|
import translateService from './translateService';
|
||||||
@ -133,79 +133,98 @@ const apiService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async generateImage(imageData: string, style?: string, promptId?: string, userPrompt?: string) {
|
async generateImage(imageData: string, style?: string, promptId?: string, userPrompt?: string): Promise<GenerationResult> {
|
||||||
try {
|
try {
|
||||||
// Создаем копию базового воркфлоу
|
// Создаем копию базового воркфлоу
|
||||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||||
|
|
||||||
// Вставляем изображение в base64 формате в узел 563
|
// Вставляем изображение в base64 формате в узел 563
|
||||||
workflow['563'].inputs.image = imageData;
|
workflow['563'].inputs.image = imageData;
|
||||||
|
|
||||||
// Если указан пользовательский промпт и выбрана кнопка "Свой промпт"
|
// Переменная для хранения использованного промпта
|
||||||
if (userPrompt && promptId === 'customPrompt') {
|
let usedPrompt = '';
|
||||||
console.log('Переводим пользовательский промпт:', userPrompt);
|
let translationFailed = false;
|
||||||
|
|
||||||
try {
|
// Если указан пользовательский промпт и выбрана кнопка "Свой промпт"
|
||||||
// Переводим промпт и ждем результата
|
if (userPrompt && promptId === 'customPrompt') {
|
||||||
const translatedPrompt = await translateService.translateToEnglish(userPrompt);
|
console.log('Переводим пользовательский промпт:', userPrompt);
|
||||||
console.log('Переведенный промпт:', translatedPrompt);
|
|
||||||
|
|
||||||
// Явно заменяем промпт в воркфлоу
|
// Переводим промпт с 3 попытками
|
||||||
workflow['316'].inputs.prompt_1 = translatedPrompt;
|
const translationResult = await translateService.translateToEnglish(userPrompt, 3);
|
||||||
|
|
||||||
// Проверяем, что промпт действительно заменен
|
if (translationResult.success) {
|
||||||
console.log('Промпт в воркфлоу после замены:', workflow['316'].inputs.prompt_1);
|
// Успешный перевод
|
||||||
} catch (translationError) {
|
console.log('Переведенный промпт:', translationResult.text);
|
||||||
console.error('Ошибка при переводе:', translationError);
|
workflow['316'].inputs.prompt_1 = translationResult.text;
|
||||||
// В случае ошибки перевода используем исходный промпт
|
usedPrompt = translationResult.text;
|
||||||
workflow['316'].inputs.prompt_1 = userPrompt;
|
} else {
|
||||||
console.log('Используем исходный промпт из-за ошибки перевода:', userPrompt);
|
// Перевод не удался после всех попыток
|
||||||
}
|
console.error('Не удалось перевести промпт после нескольких попыток');
|
||||||
}
|
translationFailed = true;
|
||||||
// Иначе используем предустановленный промпт
|
|
||||||
else if (promptId && prompts[promptId]) {
|
// Не продолжаем генерацию, возвращаем ошибку
|
||||||
workflow['316'].inputs.prompt_1 = prompts[promptId];
|
return {
|
||||||
console.log('Используем предустановленный промпт:', prompts[promptId]);
|
translationFailed: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Иначе используем предустановленный промпт (они уже переведены)
|
||||||
|
else if (promptId && prompts[promptId]) {
|
||||||
|
workflow['316'].inputs.prompt_1 = prompts[promptId];
|
||||||
|
usedPrompt = prompts[promptId];
|
||||||
|
console.log('Используем предустановленный промпт:', prompts[promptId]);
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем строку JSON для workflow ПОСЛЕ того, как все изменения внесены
|
// Если был сбой перевода, не продолжаем генерацию
|
||||||
const workflowJson = JSON.stringify(workflow);
|
if (translationFailed) {
|
||||||
|
return { translationFailed: true };
|
||||||
|
}
|
||||||
|
|
||||||
// Определяем тег на основе выбранного стиля
|
// Создаем строку JSON для workflow ПОСЛЕ того, как все изменения внесены
|
||||||
const tag = styleToTagMap[style || 'chibi'] || 'chibi';
|
const workflowJson = JSON.stringify(workflow);
|
||||||
console.log(`Используем тег "${tag}" для стиля "${style || 'chibi'}"`);
|
|
||||||
|
|
||||||
// Создаем тело запроса как строку JSON вручную
|
// Определяем тег на основе выбранного стиля
|
||||||
const requestBodyJson = `{"tag":"${tag}","user_id":${getCurrentUserId()},"workflow":${workflowJson}}`;
|
const tag = styleToTagMap[style || 'chibi'] || 'chibi';
|
||||||
|
console.log(`Используем тег "${tag}" для стиля "${style || 'chibi'}"`);
|
||||||
|
|
||||||
// Сохраняем JSON для отладки только при локальной разработке
|
// Создаем тело запроса как строку JSON вручную
|
||||||
if (!isTelegramWebAppAvailable()) {
|
const requestBodyJson = `{"tag":"${tag}","user_id":${getCurrentUserId()},"workflow":${workflowJson}}`;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем запрос
|
// Сохраняем JSON для отладки только при локальной разработке
|
||||||
const response = await fetch(`${API_BASE_URL}/generate_image`, {
|
if (!isTelegramWebAppAvailable()) {
|
||||||
method: 'POST',
|
const blob = new Blob([requestBodyJson], { type: 'application/json' });
|
||||||
headers: {
|
const url = URL.createObjectURL(blob);
|
||||||
'Content-Type': 'application/json',
|
const a = document.createElement('a');
|
||||||
},
|
a.href = url;
|
||||||
body: requestBodyJson
|
a.download = 'generation_request.json';
|
||||||
});
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
// Отправляем запрос
|
||||||
const errorData: ApiErrorType = await response.json();
|
const response = await fetch(`${API_BASE_URL}/generate_image`, {
|
||||||
throw new GenerationError(errorData.detail);
|
method: 'POST',
|
||||||
}
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: requestBodyJson
|
||||||
|
});
|
||||||
|
|
||||||
return await response.json() as GenerationResponse;
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof GenerationError) {
|
if (error instanceof GenerationError) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -10,32 +10,46 @@ interface TranslateResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const translateService = {
|
const translateService = {
|
||||||
async translateToEnglish(text: string): Promise<string> {
|
async translateToEnglish(text: string, maxRetries = 3): Promise<{ success: boolean; text: string }> {
|
||||||
try {
|
let retries = 0;
|
||||||
const response = await fetch(TRANSLATE_API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
q: text,
|
|
||||||
source: 'auto',
|
|
||||||
target: 'en',
|
|
||||||
format: 'text',
|
|
||||||
alternatives: 3,
|
|
||||||
api_key: ''
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
while (retries <= maxRetries) {
|
||||||
throw new Error('Translation failed');
|
try {
|
||||||
|
// Добавляем задержку перед повторными попытками
|
||||||
|
if (retries > 0) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 секунда между попытками
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(TRANSLATE_API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
q: text,
|
||||||
|
source: 'auto',
|
||||||
|
target: 'en',
|
||||||
|
format: 'text',
|
||||||
|
alternatives: 3,
|
||||||
|
api_key: ''
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Translation failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: TranslateResponse = await response.json();
|
||||||
|
|
||||||
|
// Успешный перевод
|
||||||
|
return { success: true, text: data.translatedText };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Translation attempt ${retries + 1} failed:`, error);
|
||||||
|
retries++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: TranslateResponse = await response.json();
|
|
||||||
return data.translatedText;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Translation error:', error);
|
|
||||||
// В случае ошибки возвращаем исходный текст
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Все попытки исчерпаны, возвращаем исходный текст с флагом неудачи
|
||||||
|
console.error(`All ${maxRetries} translation attempts failed`);
|
||||||
|
return { success: false, text: text };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -60,3 +60,10 @@ export interface StickerSetResponse {
|
|||||||
set_name: string;
|
set_name: string;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Интерфейс для ответа от apiService.generateImage
|
||||||
|
export interface GenerationResult {
|
||||||
|
result?: GenerationResponse;
|
||||||
|
usedPrompt?: string;
|
||||||
|
translationFailed: boolean;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user