обавлены улучшения: 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} />;
|
||||
case 'uploadPhoto':
|
||||
return <UploadPhotoBlock
|
||||
previewUrl={window.history.state?.usr?.previewUrl}
|
||||
previewUrl={window.history.state?.usr?.previewUrl || localStorage.getItem('stickerPreviewUrl')}
|
||||
onPhotoSelect={(file) => {
|
||||
const tempUrl = URL.createObjectURL(file);
|
||||
window.history.replaceState(
|
||||
|
||||
@ -14,6 +14,10 @@ const UploadPhotoBlock: React.FC<UploadPhotoBlockProps> = ({ onPhotoSelect, prev
|
||||
|
||||
const handleFileSelect = (file: File) => {
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
// Очищаем предыдущие данные изображения при загрузке нового
|
||||
localStorage.removeItem('stickerPreviewUrl');
|
||||
localStorage.removeItem('stickerImageData');
|
||||
|
||||
onPhotoSelect?.(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 данные
|
||||
// Убираем префикс data:image/jpeg;base64, оставляем только данные
|
||||
const imageData = previewUrl.split(',')[1];
|
||||
|
||||
// Сохраняем данные в localStorage для сохранения между сеансами навигации
|
||||
localStorage.setItem('stickerPreviewUrl', previewUrl);
|
||||
localStorage.setItem('stickerImageData', imageData);
|
||||
|
||||
navigate('/', {
|
||||
state: {
|
||||
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 BlockRenderer from '../components/blocks/BlockRenderer';
|
||||
// import UploadPhotoBlock from '../components/blocks/UploadPhotoBlock'; // Не используется
|
||||
@ -6,26 +6,44 @@ 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';
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 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;
|
||||
return state?.previewUrl || localStorage.getItem('stickerPreviewUrl') || undefined;
|
||||
});
|
||||
|
||||
const [imageData, _setImageData] = useState<string | undefined>(() => {
|
||||
const state = window.history.state?.usr;
|
||||
return state?.imageData;
|
||||
return state?.imageData || localStorage.getItem('stickerImageData') || undefined;
|
||||
});
|
||||
const [isInputVisible, setIsInputVisible] = useState(false);
|
||||
const [selectedStyle, setSelectedStyle] = useState<string>('chibi'); // По умолчанию выбран первый стиль
|
||||
const [selectedButtonId, setSelectedButtonId] = 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 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 === 'function') {
|
||||
if (actionValue === 'startGeneration') {
|
||||
@ -35,30 +53,58 @@ const Home: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Показываем уведомление о начале генерации
|
||||
setNotificationTitle('Генерация стикера');
|
||||
setNotificationMessage('Отправка запроса...');
|
||||
setIsLoading(true);
|
||||
setPromptText('');
|
||||
setIsNotificationVisible(true);
|
||||
|
||||
// Если выбран "Свой промпт" и введен текст, используем его
|
||||
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 estimatedTime = apiService.calculateEstimatedWaitTime(result.queue_position);
|
||||
const minutes = Math.floor(estimatedTime / 60);
|
||||
const seconds = estimatedTime % 60;
|
||||
const timeString = minutes > 0
|
||||
? `${minutes} мин ${seconds} сек`
|
||||
: `${seconds} сек`;
|
||||
// Отправляем запрос на генерацию
|
||||
const response = await apiService.generateImage(imageData, selectedStyle, selectedButtonId, userPrompt);
|
||||
console.log('Generation response:', response);
|
||||
|
||||
alert(`Ваша задача отправлена на генерацию!\nПозиция в очереди: ${result.queue_position}\nПримерное время ожидания: ${timeString}`);
|
||||
} else {
|
||||
alert('Ваша задача отправлена на генерацию!');
|
||||
// Проверяем, была ли ошибка перевода
|
||||
if (response.translationFailed) {
|
||||
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) {
|
||||
console.error('Generation failed:', error);
|
||||
alert('Не удалось начать генерацию');
|
||||
setNotificationTitle('Ошибка');
|
||||
setNotificationMessage('Не удалось начать генерацию');
|
||||
setIsLoading(false);
|
||||
setIsNotificationVisible(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -101,6 +147,27 @@ const Home: React.FC = () => {
|
||||
setIsInputVisible(false);
|
||||
}, [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) => {
|
||||
if (block.id === 'quickActions') {
|
||||
@ -114,6 +181,17 @@ const Home: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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.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 { prompts } from '../assets/prompts';
|
||||
import translateService from './translateService';
|
||||
@ -133,79 +133,98 @@ const apiService = {
|
||||
}
|
||||
},
|
||||
|
||||
async generateImage(imageData: string, style?: string, promptId?: string, userPrompt?: string) {
|
||||
try {
|
||||
// Создаем копию базового воркфлоу
|
||||
const workflow = JSON.parse(JSON.stringify(baseWorkflow));
|
||||
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;
|
||||
// Вставляем изображение в base64 формате в узел 563
|
||||
workflow['563'].inputs.image = imageData;
|
||||
|
||||
// Если указан пользовательский промпт и выбрана кнопка "Свой промпт"
|
||||
if (userPrompt && promptId === 'customPrompt') {
|
||||
console.log('Переводим пользовательский промпт:', userPrompt);
|
||||
// Переменная для хранения использованного промпта
|
||||
let usedPrompt = '';
|
||||
let translationFailed = false;
|
||||
|
||||
try {
|
||||
// Переводим промпт и ждем результата
|
||||
const translatedPrompt = await translateService.translateToEnglish(userPrompt);
|
||||
console.log('Переведенный промпт:', translatedPrompt);
|
||||
// Если указан пользовательский промпт и выбрана кнопка "Свой промпт"
|
||||
if (userPrompt && promptId === 'customPrompt') {
|
||||
console.log('Переводим пользовательский промпт:', userPrompt);
|
||||
|
||||
// Явно заменяем промпт в воркфлоу
|
||||
workflow['316'].inputs.prompt_1 = translatedPrompt;
|
||||
// Переводим промпт с 3 попытками
|
||||
const translationResult = await translateService.translateToEnglish(userPrompt, 3);
|
||||
|
||||
// Проверяем, что промпт действительно заменен
|
||||
console.log('Промпт в воркфлоу после замены:', workflow['316'].inputs.prompt_1);
|
||||
} catch (translationError) {
|
||||
console.error('Ошибка при переводе:', translationError);
|
||||
// В случае ошибки перевода используем исходный промпт
|
||||
workflow['316'].inputs.prompt_1 = userPrompt;
|
||||
console.log('Используем исходный промпт из-за ошибки перевода:', userPrompt);
|
||||
}
|
||||
}
|
||||
// Иначе используем предустановленный промпт
|
||||
else if (promptId && prompts[promptId]) {
|
||||
workflow['316'].inputs.prompt_1 = prompts[promptId];
|
||||
console.log('Используем предустановленный промпт:', prompts[promptId]);
|
||||
if (translationResult.success) {
|
||||
// Успешный перевод
|
||||
console.log('Переведенный промпт:', translationResult.text);
|
||||
workflow['316'].inputs.prompt_1 = translationResult.text;
|
||||
usedPrompt = translationResult.text;
|
||||
} else {
|
||||
// Перевод не удался после всех попыток
|
||||
console.error('Не удалось перевести промпт после нескольких попыток');
|
||||
translationFailed = true;
|
||||
|
||||
// Не продолжаем генерацию, возвращаем ошибку
|
||||
return {
|
||||
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 };
|
||||
}
|
||||
|
||||
// Определяем тег на основе выбранного стиля
|
||||
const tag = styleToTagMap[style || 'chibi'] || 'chibi';
|
||||
console.log(`Используем тег "${tag}" для стиля "${style || 'chibi'}"`);
|
||||
// Создаем строку JSON для workflow ПОСЛЕ того, как все изменения внесены
|
||||
const workflowJson = JSON.stringify(workflow);
|
||||
|
||||
// Создаем тело запроса как строку JSON вручную
|
||||
const requestBodyJson = `{"tag":"${tag}","user_id":${getCurrentUserId()},"workflow":${workflowJson}}`;
|
||||
// Определяем тег на основе выбранного стиля
|
||||
const tag = styleToTagMap[style || 'chibi'] || 'chibi';
|
||||
console.log(`Используем тег "${tag}" для стиля "${style || 'chibi'}"`);
|
||||
|
||||
// Сохраняем 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);
|
||||
}
|
||||
// Создаем тело запроса как строку JSON вручную
|
||||
const requestBodyJson = `{"tag":"${tag}","user_id":${getCurrentUserId()},"workflow":${workflowJson}}`;
|
||||
|
||||
// Отправляем запрос
|
||||
const response = await fetch(`${API_BASE_URL}/generate_image`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: requestBodyJson
|
||||
});
|
||||
// Сохраняем 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);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData: ApiErrorType = await response.json();
|
||||
throw new GenerationError(errorData.detail);
|
||||
}
|
||||
// Отправляем запрос
|
||||
const response = await fetch(`${API_BASE_URL}/generate_image`, {
|
||||
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) {
|
||||
if (error instanceof GenerationError) {
|
||||
throw error;
|
||||
|
||||
@ -10,32 +10,46 @@ interface TranslateResponse {
|
||||
}
|
||||
|
||||
const translateService = {
|
||||
async translateToEnglish(text: string): Promise<string> {
|
||||
try {
|
||||
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' }
|
||||
});
|
||||
async translateToEnglish(text: string, maxRetries = 3): Promise<{ success: boolean; text: string }> {
|
||||
let retries = 0;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Translation failed');
|
||||
while (retries <= maxRetries) {
|
||||
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;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
// Интерфейс для ответа от apiService.generateImage
|
||||
export interface GenerationResult {
|
||||
result?: GenerationResponse;
|
||||
usedPrompt?: string;
|
||||
translationFailed: boolean;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user