'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, Image as ImageIcon, } 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 { Switch } from '@/components/ui/switch'; import { useNotification } from '@/components/ui/notification'; import { useI18n } from '@/lib/i18n'; import { API_BASE } from '@/lib/config'; import { cn } from '@/lib/utils'; import { useUser } from '../useUser'; /* ── 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 { data: currentUser } = useUser(); const isPaid = !!currentUser?.tier && currentUser.tier !== 'free'; 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; /* ── Human-friendly error messages ──────────────────────────── */ const humanFriendlyError = (raw: string | null): string => { if (!raw) return t('dashboard.translate.error.unexpected'); const r = raw.toLowerCase(); if (r.includes('0 out of') || r.includes('0 textes') || r.includes('0 texts')) return t('dashboard.translate.error.noResult'); if (r.includes('api key') || r.includes('api_key') || r.includes('unauthorized') || r.includes('401')) return t('dashboard.translate.error.apiKey'); if (r.includes('quota') || r.includes('rate limit') || r.includes('429')) return t('dashboard.translate.error.quota'); if (r.includes('timeout') || r.includes('timed out') || r.includes('connexion')) return t('dashboard.translate.error.timeout'); if (r.includes('not found') || r.includes('404')) return t('dashboard.translate.error.sessionExpired'); if (r.includes('empty') || r.includes('vide') || r.includes('no translatable')) return t('dashboard.translate.error.empty'); if (r.includes('unsupported') || r.includes('format')) return t('dashboard.translate.error.unsupported'); if (r.includes('lost connection') || r.includes('internet')) return t('dashboard.translate.error.connection'); // Generic fallback — show raw but readable return t('dashboard.translate.error.generic', { detail: raw.charAt(0).toUpperCase() + raw.slice(1) }); }; useEffect(() => { if (submit.error && submit.error !== lastErrorRef.current) { lastErrorRef.current = submit.error; showError({ title: t('dashboard.translate.error.title'), description: humanFriendlyError(submit.error) }); } }, [submit.error, showError]); // 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 handleRetry = async () => { submit.reset(); // Small delay to ensure reset completes await new Promise(r => setTimeout(r, 50)); await handleTranslate(); }; 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]); const fileTypeButtons = [ { label: t('translate.fileType.word'), type: 'word' as const, icon: }, { label: t('translate.fileType.excel'), type: 'excel' as const, icon: }, { label: t('translate.fileType.slides'), type: 'slides' as const, icon: }, { label: t('translate.fileType.pdf'), type: 'pdf' as const, icon: }, ]; return (
{/* ── HEADER (Landing Page Style) ───────────────────────── */}
{showProcessing ? ( <> {t('translate.header.processing')}

{(() => { const full = t('translate.header.aiActive'); const i = full.lastIndexOf(' '); return <>{i === -1 ? full : <>{full.slice(0, i)} {full.slice(i + 1)}}; })()}

{t('translate.header.aiActiveDesc')}

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

{(() => { const full = t('translate.header.completedTitle'); const i = full.lastIndexOf(' '); return <>{i === -1 ? full : <>{full.slice(0, i)} {full.slice(i + 1)}}; })()}

{submit.fileName}

) : ( <> {t('translate.header.proSpace')}

{(() => { const full = t('translate.header.translateDoc'); const i = full.lastIndexOf(' '); return <>{i === -1 ? full : <>{full.slice(0, i)} {full.slice(i + 1)}}; })()}

{t('translate.header.translateDocDesc')}

)}
{/* ── GRID: 7/5 SPLIT ───────────────────────────────────── */}
{/* ═══════════════════════════════════════════════════════ */} {/* LEFT (7 cols) — content swaps based on state */} {/* ═══════════════════════════════════════════════════════ */}
{/* ── UPLOAD STATE: Editorial Dropzone ──────────────── */} {showUpload && (
dropzoneInputRef.current?.click()} >
{t('translate.upload.nativeFormat')}

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

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

{/* Simulated file triggers */}
e.stopPropagation()}> {fileTypeButtons.map(f => ( ))}
{/* Hidden file input for click-to-upload */}
)} {/* ── CONFIGURING STATE: File indicator ──────────────── */} {showConfiguring && (

{t('landing.translate.sourceDocument') || 'Document Source'}

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

{upload.error}

}
)} {/* ── Desktop Submit Button & Actions (shown when not processing/complete) ── */} {(showUpload || showConfiguring) && (
{!upload.file && (

{t('translate.pleaseLoadFile')}

)} {upload.file && !config.targetLang && (

{t('translate.chooseTargetLang')}

)}
{t('landing.translate.zeroRetention') || 'Rétention Zéro'} {t('landing.translate.filesDeleted') || 'Fichiers supprimés post-traitement'}
)} {/* ── PROCESSING STATE: Rich progress ───────────────── */} {showProcessing && (

{t('translate.contextEngineActive')}

{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 dark:text-brand-dark font-bold' : 'bg-brand-muted text-brand-dark/25 dark:bg-[#1f1f1f] dark:text-white/20' )} >
))}
{activeStepIdx < 2 ? t('translate.phase1') : t('translate.phase2')} {Math.round(submit.progress)}%
} value={`${Math.round(submit.progress)}%`} label={t('translate.stat.segments')} /> } value="99.9%" label={t('translate.stat.precision')} /> } value="Turbo" label={t('translate.stat.speedLabel')} /> } value={formatElapsed(elapsed)} label={t('translate.stat.time')} />
)} {/* ── COMPLETE STATE: Success with download ─────────── */} {showComplete && (

{t('translate.header.completedTitle')}

{submit.fileName}

{t('translate.complete.masterQuality')}
)} {/* ── FAILED STATE ───────────────────────────────────── */} {showFailed && (

{t('translate.failedTitle')}

{humanFriendlyError(submit.error)}

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

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

{/* Active mode badge */} {config.provider && (
{config.mode === 'llm' ? ( <> {t('dashboard.translate.modeAI')} ) : ( <> {t('dashboard.translate.modeClassic')} )} {config.mode === 'classic' && config.isPro && ( {t('dashboard.translate.glossaryLLMHint')} )}
)} {/* Glossary selector */} {/* Translate Images */}
{t('dashboard.translate.translateImages') || "Traduire les images"}
{config.mode === 'classic' ? (
{t('translate.unavailableStandard')}
) : (
{t('dashboard.translate.translateImagesDesc') || "Détecter et traduire automatiquement les textes incrustés dans vos images."}
)}
{/* PDF mode selector */} {isPdf && (
)}
)} {/* ── MONITOR (processing) ────────────────────────────── */} {showProcessing && (

{t('translate.monitor')}

{/* 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 */}
Source {srcLangName.toUpperCase()}
Cible {tgtLangName.toUpperCase()}
{currentProvider && (
Moteur {currentProvider.label.toUpperCase()}
)}
{/* Quality progress */}
{t('translate.layoutIntegrity')} {t('translate.secureHundred')}
)} {/* ── SUMMARY (complete) ──────────────────────────────── */} {showComplete && (

{t('translate.summary')}

Source {srcLangName.toUpperCase()}
Cible {tgtLangName.toUpperCase()}
{currentProvider && (
Moteur {currentProvider.label.toUpperCase()}
)}
{t('translate.layoutIntegrity')} {t('translate.okHundred')}
)}
{/* ── MEMENTO PROMO BANNER — hidden for paying users ────── */} {!isPaid && (showUpload || showConfiguring || showFailed) && (
M
Ecosystème Wordly

{t('memento.title')}

{t('memento.slogan')}

{t('memento.ctaFree')} {t('memento.ctaMore')}
)} {/* Mobile Sticky Action Bar (visible on mobile, hidden on lg) */} {(showUpload || showConfiguring) && (
)}
); } /* ═══ 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}

); }