initial commit
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
'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',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user