'use client'; import { useEffect, useRef, useState, useMemo } from 'react'; import { ShieldCheck, Clock, ArrowRight, RotateCcw, Loader2, FileSpreadsheet, FileText, Presentation, Upload, X, Zap, CheckCircle2, Search, Languages, Wrench, Activity, Timer, Download, AlertTriangle, FileType, } from 'lucide-react'; import { useFileUpload } from './useFileUpload'; import { useTranslationConfig } from './useTranslationConfig'; import { useTranslationSubmit } from './useTranslationSubmit'; import LanguageSelector from './LanguageSelector'; import { ProviderSelector } from './ProviderSelector'; import { GlossarySelector } from './GlossarySelector'; import { useNotification } from '@/components/ui/notification'; import { useI18n } from '@/lib/i18n'; import { API_BASE } from '@/lib/config'; import { cn } from '@/lib/utils'; /* ── helpers ─────────────────────────────────────────────────────── */ const FILE_ICONS: Record = { xlsx: FileSpreadsheet, docx: FileText, pptx: Presentation, pdf: FileType, }; const FILE_COLORS: Record = { xlsx: 'text-green-500', docx: 'text-blue-500', pptx: 'text-orange-500', pdf: 'text-red-500', }; function fmt(bytes: number) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1048576).toFixed(1)} MB`; } /* ── Quality label based on provider ────────────────────────────── */ function getQualityLabel(t: (key: string) => string, provider: string | null | undefined): string { if (!provider) return t('dashboard.translate.highQuality'); if (['openai', 'openrouter', 'openrouter_premium'].includes(provider)) return t('dashboard.translate.highQuality'); if (provider === 'deepl') return t('dashboard.translate.highQuality'); return t('dashboard.translate.quality'); } /* ── Pipeline step keys ─────────────────────────────────────────── */ const PIPELINE_STEP_KEYS = [ 'dashboard.translate.pipeline.upload', 'dashboard.translate.pipeline.analyze', 'dashboard.translate.pipeline.translate', 'dashboard.translate.pipeline.rebuild', 'dashboard.translate.pipeline.finalize', ] as const; const PIPELINE_ICONS = [Upload, Search, Languages, Wrench, CheckCircle2] as const; const PIPELINE_STARTS = [0, 10, 25, 75, 92]; function getActiveStepIdx(progress: number) { for (let i = PIPELINE_STARTS.length - 1; i >= 0; i--) { if (progress >= PIPELINE_STARTS[i]) return i; } return 0; } function formatElapsed(totalSeconds: number) { const m = Math.floor(totalSeconds / 60); const s = totalSeconds % 60; return `${m}:${s.toString().padStart(2, '0')}`; } /* ── Page ────────────────────────────────────────────────────────── */ export default function TranslatePage() { const upload = useFileUpload(); const config = useTranslationConfig(!!upload.file); const submit = useTranslationSubmit(); const { error: showError } = useNotification(); const { t } = useI18n(); const lastErrorRef = useRef(null); const replaceInputRef = useRef(null); const dropzoneInputRef = useRef(null); const [pdfMode, setPdfMode] = useState<'layout' | 'text_only'>('layout'); const [elapsed, setElapsed] = useState(0); const timerRef = useRef | null>(null); const isPdf = upload.file?.name.toLowerCase().endsWith('.pdf') ?? false; useEffect(() => { if (submit.error && submit.error !== lastErrorRef.current) { lastErrorRef.current = submit.error; showError({ title: t('dashboard.translate.errorNotificationTitle'), description: submit.error }); } }, [submit.error, showError, t]); // Elapsed timer useEffect(() => { if ((submit.status === 'processing' || submit.isSubmitting) && submit.status !== 'completed') { setElapsed(0); timerRef.current = setInterval(() => setElapsed((s) => s + 1), 1000); } else { if (timerRef.current) clearInterval(timerRef.current); } return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [submit.status, submit.isSubmitting]); const handleTranslate = async () => { if (!upload.file || !config.isConfigValid) return; const cfg = config.getConfig(); if (isPdf) cfg.pdfMode = pdfMode; await submit.submitTranslation(upload.file, cfg); }; const handleNewTranslation = () => { submit.reset(); upload.removeFile(); setElapsed(0); }; const handleDownload = async () => { if (!submit.jobId) return; const token = localStorage.getItem('token'); const headers: Record = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const response = await fetch(`${API_BASE}/api/v1/download/${submit.jobId}`, { headers }); if (!response.ok) return; const contentDisposition = response.headers.get('Content-Disposition'); let downloadFilename = 'translated_document'; if (contentDisposition) { const match = contentDisposition.match(/filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']+)/i); if (match?.[1]) downloadFilename = match[1]; } else if (submit.fileName) { const ext = submit.fileName.split('.').pop() || ''; const base = submit.fileName.replace(/\.[^.]+$/, ''); downloadFilename = `${base}_translated.${ext}`; } const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = downloadFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); }; /* ── Derived states ──────────────────────────────────────────── */ const isConfiguring = !!upload.file && submit.status === 'idle' && !submit.isSubmitting; const isProcessing = (submit.status === 'processing' || submit.isSubmitting) && submit.status !== 'completed'; const isCompleted = submit.status === 'completed'; const isFailed = submit.status === 'failed'; const showUpload = !upload.file && !isProcessing && !isCompleted && !isFailed; const showConfiguring = isConfiguring; const showProcessing = isProcessing; const showComplete = isCompleted && !!submit.jobId; const showFailed = isFailed; const currentProvider = config.availableProviders.find(p => p.id === config.provider); const srcLangName = config.languages.find(l => l.code === config.sourceLang)?.name || config.sourceLang; const tgtLangName = config.languages.find(l => l.code === config.targetLang)?.name || config.targetLang; const activeStepIdx = getActiveStepIdx(submit.progress); const qualityLabel = useMemo(() => getQualityLabel(t, config.provider), [t, config.provider]); /* ═══════════════════════════════════════════════════════════════ */ /* EDITORIAL LAYOUT */ /* ═══════════════════════════════════════════════════════════════ */ return (
{/* ── HEADER ────────────────────────────────────────────── */}
{showProcessing ? ( <> {t('landing.translate.processing')}

{t('landing.translate.aiAnalysis')}

{t('landing.translate.preservingLayout')}

) : showComplete ? ( <> {t('dashboard.translate.completed')}

{t('dashboard.translate.completed')}

{submit.fileName}

) : ( <> {t('landing.translate.newProject')}

{t('landing.translate.title')}

{t('landing.translate.subtitle')}

)}
{/* ── GRID: 8/4 SPLIT ───────────────────────────────────── */}
{/* ═══════════════════════════════════════════════════════ */} {/* LEFT (8 cols) — content swaps based on state */} {/* ═══════════════════════════════════════════════════════ */}
{/* ── UPLOAD STATE: Editorial Dropzone ──────────────── */} {showUpload && (
dropzoneInputRef.current?.click()} >

{t('landing.translate.dropHere')}

{t('landing.translate.supportedFormats')}

{[ { label: 'Word', icon: }, { label: 'Excel', icon: }, { label: 'Slides', icon: }, { label: 'PDF', icon: }, ].map(f => ( {f.icon} {f.label} ))}
{/* Hidden file input for click-to-upload */}
)} {/* ── CONFIGURING STATE: File strip ─────────────────── */} {showConfiguring && (

{t('landing.translate.sourceDocument')}

replaceInputRef.current?.click()} t={t} /> {upload.error &&

{upload.error}

}
)} {/* ── PROCESSING STATE: Rich progress ───────────────── */} {showProcessing && (

{t('dashboard.translate.translating')}

{submit.fileName || upload.file?.name}

{/* Progress Line with step icons */}
{PIPELINE_ICONS.map((Icon, i) => (
(i * 25) ? 'bg-brand-dark text-white scale-110 dark:bg-brand-accent' : 'bg-brand-muted text-brand-dark/20 dark:bg-white/10 dark:text-white/20' )} >
))}
{activeStepIdx < 2 ? t('dashboard.translate.steps.uploading') : t('dashboard.translate.steps.starting')} {Math.round(submit.progress)}%
} value={`${Math.round(submit.progress)}%`} label={t('dashboard.translate.segments')} /> } value="99.9%" label={t('dashboard.translate.quality')} /> } value="Turbo" label={t('dashboard.translate.segPerMin')} /> } value={formatElapsed(elapsed)} label={t('dashboard.translate.elapsed')} />
)} {/* ── COMPLETE STATE: Success with download ─────────── */} {showComplete && (

{t('dashboard.translate.completed')}

{submit.fileName}

{qualityLabel}
)} {/* ── FAILED STATE ───────────────────────────────────── */} {showFailed && (

{t('dashboard.translate.progress.failedTitle')}

{submit.error}

{(submit.fileName || upload.file?.name) && upload.file && (
replaceInputRef.current?.click()} t={t} />
)}
)}
{/* ═══════════════════════════════════════════════════════ */} {/* RIGHT (4 cols) — Config / Monitor / Summary */} {/* ═══════════════════════════════════════════════════════ */}
{/* ── CONFIG (upload / configuring / failed) ──────────── */} {(showUpload || showConfiguring || showFailed) && (

{t('landing.translate.configuration')}

{config.isPro && config.mode === 'llm' && ( )} {/* PDF mode selector */} {isPdf && (
)}
{t('landing.translate.zeroRetention')} {t('landing.translate.filesDeleted')}
)} {/* ── MONITOR (processing) ────────────────────────────── */} {showProcessing && (

{t('dashboard.translate.liveMonitor')}

{/* File summary */} {(submit.fileName || upload.file?.name) && (
{(() => { const name = submit.fileName || upload.file?.name || ''; const ext = name.split('.').pop()?.toLowerCase() ?? ''; const FileIcon = FILE_ICONS[ext] ?? FileText; return ; })()}

{submit.fileName || upload.file?.name}

{upload.file ? `${fmt(upload.file.size)} ` : ''}{(submit.fileName || upload.file?.name || '').split('.').pop()?.toUpperCase()}

)} {/* Config summary */}
{t('dashboard.translate.language.source')} {srcLangName.toUpperCase()}
{t('dashboard.translate.language.target')} {tgtLangName.toUpperCase()}
{currentProvider && (
{t('dashboard.translate.engine')} {currentProvider.label.toUpperCase()}
)}
{/* Quality progress */}
{t('dashboard.translate.quality')} {qualityLabel}
)} {/* ── SUMMARY (complete) ──────────────────────────────── */} {showComplete && (

{t('dashboard.translate.summary')}

{t('dashboard.translate.language.source')} {srcLangName.toUpperCase()}
{t('dashboard.translate.language.target')} {tgtLangName.toUpperCase()}
{currentProvider && (
{t('dashboard.translate.engine')} {currentProvider.label.toUpperCase()}
)}
{t('dashboard.translate.quality')} {qualityLabel}
)}
{/* ── MOMENTO PROMO BANNER ──────────────────────────────── */} {(showUpload || showConfiguring || showFailed) && (
M
{t('memento.title')}

{t('memento.title')}

{t('memento.slogan')}

)}
); } /* ═══ Sub-components ═══════════════════════════════════════════════ */ /** Compact file strip */ function FileStrip({ file, onRemove, onReplace, t }: { file: File; onRemove: () => void; onReplace: () => void; t: (key: string) => string }) { const ext = file.name.split('.').pop()?.toLowerCase() ?? ''; const FileIcon = FILE_ICONS[ext] ?? FileText; const color = FILE_COLORS[ext] ?? 'text-muted-foreground'; return (
{file.name} {fmt(file.size)} .{ext.toUpperCase()}
); } /** Pipeline stepper */ function PipelineStepper({ activeIdx, t }: { activeIdx: number; t: (key: string) => string }) { return (
{PIPELINE_STEP_KEYS.map((stepKey, i) => { const isActive = i === activeIdx; const isDone = i < activeIdx; const Icon = PIPELINE_ICONS[i]; return (
{isDone ? : }
{i < PIPELINE_STEP_KEYS.length - 1 && (
)}
{t(stepKey)}
); })}
); } /** Small stat box */ function StatBox({ icon, value, label }: { icon: React.ReactNode; value: string; label: string }) { return (
{icon}

{value}

{label}

); }