Files
uno-click/site/app/result/page.tsx
T
2026-05-13 14:20:41 +00:00

471 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
interface FileResult {
url: string;
contentType: string;
format?: string;
width?: number;
height?: number;
duration?: number;
}
interface ResultData {
status: string;
scenarioName?: string;
requestPayload?: { prompt?: string };
files?: FileResult[];
}
function ResultContent() {
const router = useRouter();
const searchParams = useSearchParams();
const generationUuid = searchParams.get('generationUuid');
const [result, setResult] = useState<ResultData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [autoRefresh, setAutoRefresh] = useState(false);
useEffect(() => {
if (generationUuid) {
loadResult();
} else {
setError('Не указан generationUuid');
setLoading(false);
}
return () => {
if (autoRefresh) {
// Cleanup если нужно
}
};
}, [generationUuid]);
async function loadResult() {
if (!generationUuid) return;
try {
const response = await fetch(`/api/result/${generationUuid}`, {
credentials: 'same-origin',
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Ошибка загрузки результата');
}
setResult(data.result);
setError('');
setLoading(false);
// Авто-обновление если ещё генерируется
if (data.result.status === 'running' || data.result.status === 'waiting_for_input') {
setAutoRefresh(true);
setTimeout(loadResult, 3000);
} else {
setAutoRefresh(false);
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Ошибка загрузки');
setLoading(false);
}
}
function getStatusText(status: string): string {
const statuses: Record<string, string> = {
running: 'Генерация...',
completed: 'Готово',
failed: 'Ошибка',
waiting_for_input: 'Ожидание...',
created: 'Создано',
};
return statuses[status] || status;
}
function getStatusClass(status: string): string {
return `status-badge status-${status}`;
}
function formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function renderFile(file: FileResult, index: number) {
const isVideo = file.contentType === 'video';
const previewContent = isVideo
? <video src={file.url} controls style={styles.previewMedia} />
: <img src={file.url} alt={`Result ${index + 1}`} style={styles.previewMedia} />;
return (
<div key={index} style={styles.fileCard}>
<div style={styles.filePreview}>{previewContent}</div>
<div style={styles.fileInfo}>
<div style={styles.fileMeta}>
{file.format && (
<span style={styles.fileMetaItem}>
<strong>Формат:</strong> {file.format.toUpperCase()}
</span>
)}
{file.width && file.height && (
<span style={styles.fileMetaItem}>
<strong>Размер:</strong> {file.width}×{file.height}
</span>
)}
{file.duration && (
<span style={styles.fileMetaItem}>
<strong>Длительность:</strong> {formatDuration(file.duration)}
</span>
)}
</div>
<div style={styles.fileActions}>
<a href={file.url} target="_blank" rel="noopener noreferrer" style={styles.btnPrimary}>
👁 Открыть
</a>
<a href={file.url} download style={styles.btnSecondary}>
Скачать
</a>
</div>
</div>
</div>
);
}
if (loading) {
return (
<div style={styles.page}>
<div style={styles.container}>
<div style={styles.header}>
<h1 style={styles.headerTitle}>Результат</h1>
<a href="/dashboard" style={styles.btnSmall}>К сценариям</a>
</div>
<div style={styles.card}>
<div style={styles.emptyState}>
<div style={styles.emptyStateIcon}>
<span style={styles.spinnerLarge}></span>
</div>
<div style={styles.emptyStateText}>Загружаем результат...</div>
</div>
</div>
</div>
</div>
);
}
if (error && !result) {
return (
<div style={styles.page}>
<div style={styles.container}>
<div style={styles.header}>
<h1 style={styles.headerTitle}>Результат</h1>
<a href="/dashboard" style={styles.btnSmall}>К сценариям</a>
</div>
<div style={styles.card}>
<div style={styles.error}>{error}</div>
</div>
</div>
</div>
);
}
return (
<div style={styles.page}>
<div style={styles.container}>
<div style={styles.header}>
<h1 style={styles.headerTitle}>Результат</h1>
<div style={styles.headerActions}>
<a href="/dashboard" style={styles.btnSmall}>К сценариям</a>
</div>
</div>
<div style={styles.card}>
<div style={styles.cardHeader}>
<span style={styles.cardTitle}>{result?.scenarioName || 'Загрузка...'}</span>
<span style={{
...styles.statusBadge,
...(result?.status === 'completed' ? styles.statusCompleted :
result?.status === 'failed' ? styles.statusFailed :
styles.statusRunning),
}}>
{result ? getStatusText(result.status) : 'Загрузка...'}
</span>
</div>
<div style={styles.cardBody}>
{error && <div style={styles.error}>{error}</div>}
{result?.requestPayload?.prompt && (
<div style={styles.promptInfo}>
<div style={styles.promptLabel}>Промпт</div>
<div style={styles.promptText}>{result.requestPayload.prompt}</div>
</div>
)}
{result?.files && result.files.length > 0 ? (
<div style={styles.fileContainer}>
{result.files.map((file, index) => renderFile(file, index))}
</div>
) : result?.status === 'completed' ? (
<div style={styles.emptyState}>
<div style={styles.emptyStateIcon}></div>
<div style={styles.emptyStateText}>Результат получен, но файлы не найдены</div>
</div>
) : result?.status === 'failed' ? (
<div style={styles.emptyState}>
<div style={styles.emptyStateIcon}></div>
<div style={styles.emptyStateText}>Произошла ошибка при генерации</div>
</div>
) : (
<div style={styles.emptyState}>
<div style={styles.emptyStateIcon}></div>
<div style={styles.emptyStateText}>Результат ещё не готов</div>
<button onClick={loadResult} style={styles.refreshBtn}>
<span style={{
...styles.spinner,
display: autoRefresh ? 'inline-block' : 'none',
}}></span>
<span>Проверить снова</span>
</button>
</div>
)}
</div>
</div>
</div>
</div>
);
}
export default function ResultPage() {
return (
<Suspense fallback={<div style={styles.page}><div style={styles.container}><div style={styles.emptyState}>Загрузка...</div></div></div>}>
<ResultContent />
</Suspense>
);
}
const styles: Record<string, React.CSSProperties> = {
page: {
minHeight: '100vh',
padding: '40px 20px',
background: '#f5f5f5',
},
container: {
maxWidth: '900px',
margin: '0 auto',
},
header: {
background: 'white',
padding: '20px 30px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
marginBottom: '20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
headerTitle: {
fontSize: '20px',
fontWeight: 600,
color: '#333',
},
headerActions: {
display: 'flex',
gap: '10px',
},
btnSmall: {
padding: '8px 16px',
borderRadius: '6px',
fontSize: '13px',
cursor: 'pointer',
border: '1px solid #ddd',
background: 'white',
color: '#666',
textDecoration: 'none',
},
card: {
background: 'white',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
overflow: 'hidden',
},
cardHeader: {
padding: '20px 30px',
borderBottom: '1px solid #eee',
display: 'flex',
alignItems: 'center',
},
cardTitle: {
fontSize: '16px',
fontWeight: 600,
color: '#333',
},
cardBody: {
padding: '30px',
},
statusBadge: {
display: 'inline-block',
padding: '4px 12px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 500,
marginLeft: '10px',
},
statusRunning: {
background: '#fff3cd',
color: '#856404',
},
statusCompleted: {
background: '#d4edda',
color: '#155724',
},
statusFailed: {
background: '#f8d7da',
color: '#721c24',
},
emptyState: {
textAlign: 'center',
padding: '60px 20px',
color: '#666',
},
emptyStateIcon: {
fontSize: '48px',
marginBottom: '16px',
},
emptyStateText: {
fontSize: '16px',
marginBottom: '24px',
},
refreshBtn: {
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
padding: '12px 24px',
background: '#007bff',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: 500,
cursor: 'pointer',
},
spinner: {
display: 'inline-block',
width: '16px',
height: '16px',
border: '2px solid rgba(255,255,255,0.3)',
borderTopColor: 'white',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
},
spinnerLarge: {
width: '32px',
height: '32px',
border: '2px solid rgba(0,123,255,0.3)',
borderTopColor: '#007bff',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
},
error: {
background: '#fee',
color: '#c00',
padding: '12px',
borderRadius: '6px',
marginBottom: '20px',
fontSize: '14px',
},
promptInfo: {
background: '#f8f9fa',
padding: '16px 20px',
borderRadius: '6px',
marginBottom: '20px',
},
promptLabel: {
fontSize: '12px',
color: '#666',
marginBottom: '4px',
},
promptText: {
fontSize: '14px',
color: '#333',
},
fileContainer: {
display: 'flex',
flexDirection: 'column',
gap: '20px',
},
fileCard: {
border: '1px solid #eee',
borderRadius: '8px',
overflow: 'hidden',
},
filePreview: {
width: '100%',
background: '#f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '300px',
maxHeight: '600px',
},
previewMedia: {
maxWidth: '100%',
maxHeight: '600px',
objectFit: 'contain',
},
fileInfo: {
padding: '16px 20px',
background: '#fafafa',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '12px',
},
fileMeta: {
display: 'flex',
gap: '16px',
flexWrap: 'wrap',
},
fileMetaItem: {
fontSize: '13px',
color: '#666',
},
fileActions: {
display: 'flex',
gap: '10px',
},
btnPrimary: {
padding: '10px 20px',
borderRadius: '6px',
fontSize: '13px',
fontWeight: 500,
cursor: 'pointer',
textDecoration: 'none',
border: 'none',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
background: '#007bff',
color: 'white',
},
btnSecondary: {
padding: '10px 20px',
borderRadius: '6px',
fontSize: '13px',
fontWeight: 500,
cursor: 'pointer',
textDecoration: 'none',
border: '1px solid #ddd',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
background: '#f5f5f5',
color: '#333',
},
};