From c1ea65f10f15a4ca7109034ed2be4ca44f6c85c6 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 31 May 2026 10:14:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(translate):=20refonte=20du=20design=20de?= =?UTF-8?q?=20la=20page=20de=20traduction=20et=20du=20s=C3=A9lecteur=20de?= =?UTF-8?q?=20moteurs=20(Etapes=201-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GUIDE_UTILISATION.md | 1 + .../dashboard/translate/LanguageSelector.tsx | 66 ++- .../dashboard/translate/ProviderSelector.tsx | 342 +++++++++++--- frontend/src/app/dashboard/translate/page.tsx | 418 ++++++++++-------- frontend/src/app/dashboard/translate/types.ts | 4 + .../app/dashboard/translate/useFileUpload.ts | 26 ++ .../translate/useTranslationConfig.ts | 6 +- .../translate/useTranslationSubmit.ts | 4 + frontend/src/messages/en.json | 4 +- frontend/src/messages/fr.json | 4 +- routes/translate_routes.py | 10 + services/translation_service.py | 68 ++- tests/test_translate_endpoint.py | 20 + .../test_translators/test_word_translator.py | 19 +- translators/excel_translator.py | 62 ++- translators/pptx_translator.py | 108 ++++- translators/word_translator.py | 119 ++++- 17 files changed, 956 insertions(+), 325 deletions(-) diff --git a/GUIDE_UTILISATION.md b/GUIDE_UTILISATION.md index 9a92a4b..ece700d 100644 --- a/GUIDE_UTILISATION.md +++ b/GUIDE_UTILISATION.md @@ -112,6 +112,7 @@ docker compose -f docker-compose.dev.yml down docker compose down ``` + > Pour supprimer aussi les volumes (données) : ajouter `--volumes` --- diff --git a/frontend/src/app/dashboard/translate/LanguageSelector.tsx b/frontend/src/app/dashboard/translate/LanguageSelector.tsx index 9bb585f..40c4f47 100644 --- a/frontend/src/app/dashboard/translate/LanguageSelector.tsx +++ b/frontend/src/app/dashboard/translate/LanguageSelector.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; -import { Loader2, AlertCircle, ChevronDown, Check } from 'lucide-react'; +import { Loader2, AlertCircle, ChevronDown, Check, ArrowRightLeft } from 'lucide-react'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import type { Language } from './types'; @@ -61,35 +61,33 @@ function Combobox({ const label = value === 'auto' ? autoLabel : allOptions.find(l => l.code === value)?.name ?? value; return ( -
+
{open && ( -
-
+
+
setQuery(e.target.value)} placeholder="Search..." - className="w-full bg-transparent px-1 py-1 text-sm outline-none placeholder:text-muted-foreground" + className="w-full bg-transparent px-1 py-1 text-[10px] outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white" />
-
+
{filtered.length === 0 && ( -
No results
+
No results
)} {filtered.map(lang => ( ))}
@@ -142,51 +140,47 @@ export default function LanguageSelector({ const canSwap = sourceLang !== 'auto'; return ( -
+
{/* Source */} -
- +
+ Source
- {/* Swap */} -
+ {/* Swap Button */} +
{/* Target */} -
- +
+ Cible
diff --git a/frontend/src/app/dashboard/translate/ProviderSelector.tsx b/frontend/src/app/dashboard/translate/ProviderSelector.tsx index 56cd3e2..6f54f51 100644 --- a/frontend/src/app/dashboard/translate/ProviderSelector.tsx +++ b/frontend/src/app/dashboard/translate/ProviderSelector.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState, useEffect } from 'react'; import { Loader2, CheckCircle2, Lock, Sparkles } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; @@ -13,6 +14,82 @@ interface ProviderSelectorProps { isPro: boolean; } +interface CardTheme { + badge: string; + subBadge: string; + accentClass: string; + glowClass: string; + descriptionOverride: string; +} + +const LLM_THEMES: Record = { + deepseek: { + badge: 'Essentielle', + subBadge: 'Technique & Éco', + accentClass: 'border-cyan-500/30 text-cyan-600 dark:text-cyan-400 bg-cyan-500/5', + glowClass: 'from-cyan-500/10 dark:from-cyan-500/5 to-transparent', + descriptionOverride: 'Traduction ultra-précise et économique, idéale pour les documents techniques et le code.' + }, + openai: { + badge: 'Premium', + subBadge: 'Haute Fidélité', + accentClass: 'border-emerald-500/30 text-emerald-600 dark:text-emerald-400 bg-emerald-500/5', + glowClass: 'from-emerald-500/10 dark:from-emerald-500/5 to-transparent', + descriptionOverride: 'Le standard mondial de l\'IA. Cohérence textuelle maximale et respect strict du style.' + }, + minimax: { + badge: 'Avancée', + subBadge: 'Performance', + accentClass: 'border-indigo-500/30 text-indigo-600 dark:text-indigo-400 bg-indigo-500/5', + glowClass: 'from-indigo-500/10 dark:from-indigo-500/5 to-transparent', + descriptionOverride: 'Vitesse d\'exécution incroyable et excellente compréhension des structures complexes.' + }, + openrouter: { + badge: 'Express', + subBadge: 'Multi-Modèles', + accentClass: 'border-purple-500/30 text-purple-600 dark:text-purple-400 bg-purple-500/5', + glowClass: 'from-purple-500/10 dark:from-purple-500/5 to-transparent', + descriptionOverride: 'Accès unifié aux meilleurs modèles open-source optimisés pour la traduction.' + }, + openrouter_premium: { + badge: 'Ultra', + subBadge: 'Maximum Context', + accentClass: 'border-rose-500/30 text-rose-600 dark:text-rose-400 bg-rose-500/5', + glowClass: 'from-rose-500/10 dark:from-rose-500/5 to-transparent', + descriptionOverride: 'Traduction assistée par les modèles de pointe (GPT-4o, Claude 3.5 Sonnet) pour documents longs.' + }, + zai: { + badge: 'Spécialisée', + subBadge: 'Finance & Droit', + accentClass: 'border-amber-500/30 text-amber-600 dark:text-amber-400 bg-amber-500/5', + glowClass: 'from-amber-500/10 dark:from-amber-500/5 to-transparent', + descriptionOverride: 'Modèle affiné pour les terminologies métiers exigeantes (juridique, finance).' + } +}; + +const DEFAULT_LLM_THEME: CardTheme = { + badge: 'Moderne', + subBadge: 'Raisonnement IA', + accentClass: 'border-brand-accent/30 text-brand-accent bg-brand-accent/5', + glowClass: 'from-brand-accent/10 to-transparent', + descriptionOverride: 'Traduction par grand modèle linguistique (LLM) avec analyse sémantique avancée.' +}; + +const CLASSIC_THEMES: Record = { + google: { + labelOverride: 'Google Traduction', + descriptionOverride: 'Traduction ultra-rapide couvrant plus de 130 langues. Recommandé pour les flux généraux.' + }, + deepl: { + labelOverride: 'DeepL Pro', + descriptionOverride: 'Traduction haute précision réputée pour sa fluidité et ses formulations naturelles.' + }, + google_cloud: { + labelOverride: 'Google Cloud API', + descriptionOverride: 'Moteur cloud professionnel optimisé pour le traitement de gros volumes de documents.' + } +}; + export function ProviderSelector({ provider, onProviderChange, @@ -21,29 +98,91 @@ export function ProviderSelector({ isPro, }: ProviderSelectorProps) { const { t } = useI18n(); + const [activeTab, setActiveTab] = useState<'classic' | 'llm'>('classic'); + + // Filter providers + const classicProviders = availableProviders.filter((p) => p.mode === 'classic'); + const llmProviders = availableProviders.filter((p) => p.mode === 'llm'); + + // Initialize and synchronize activeTab based on current provider + useEffect(() => { + if (provider) { + const selected = availableProviders.find((p) => p.id === provider); + if (selected) { + setActiveTab(selected.mode); + } + } + }, [provider, availableProviders]); if (isLoadingProviders) { return ( -
- - {t('dashboard.translate.provider.loading')} +
+ + {t('dashboard.translate.provider.loading') || 'Chargement des moteurs...'}
); } if (availableProviders.length === 0) { return ( -

- {t('dashboard.translate.provider.noneConfigured')} +

+ {t('dashboard.translate.provider.noneConfigured') || 'Aucun fournisseur configuré'}

); } - const classicProviders = availableProviders.filter((p) => p.mode === 'classic'); - const llmProviders = availableProviders.filter((p) => p.mode === 'llm'); - - const renderCard = (p: AvailableProvider, locked: boolean) => { + const renderClassicCard = (p: AvailableProvider) => { const isSelected = provider === p.id; + const meta = CLASSIC_THEMES[p.id]; + const label = meta?.labelOverride || p.label; + const description = meta?.descriptionOverride || p.description; + + return ( + + ); + }; + + const renderLlmCard = (p: AvailableProvider, locked: boolean) => { + const isSelected = provider === p.id; + const theme = LLM_THEMES[p.id] || DEFAULT_LLM_THEME; + const description = theme.descriptionOverride || p.description; + return ( ); }; return ( -
-

- {t('dashboard.translate.provider.sectionTitle')} -

+
+ {/* Title */} +
+ +
- {/* Classic providers */} - {classicProviders.length > 0 && ( -
- {classicProviders.map((p) => renderCard(p, false))} -
- )} - - {/* LLM providers — Pro only */} - {llmProviders.length > 0 && ( -
-
-
- - - {t('dashboard.translate.provider.llmDivider')} - {!isPro && ( - - {t('dashboard.translate.provider.llmDividerPro')} - - )} - -
-
- {llmProviders.map((p) => renderCard(p, !isPro))} - {!isPro && ( -

- - {t('dashboard.translate.provider.upgrade')} - {' '} - {t('dashboard.translate.provider.upgradeSuffix')} -

+ {/* Tabs Container */} +
+ + +
+ + {/* Active Tab List */} +
+ {activeTab === 'classic' ? ( + classicProviders.length > 0 ? ( +
+ {classicProviders.map((p) => renderClassicCard(p))} +
+ ) : ( +

+ Aucun traducteur standard disponible. +

+ ) + ) : ( + llmProviders.length > 0 ? ( +
+ {llmProviders.map((p) => renderLlmCard(p, !isPro))} +
+ ) : ( +

+ Aucun modèle IA configuré. +

+ ) + )} +
+ + {/* Pro upgrade banner when llm is active and user is not pro */} + {!isPro && activeTab === 'llm' && ( +
+ + + {t('dashboard.translate.provider.llmDivider') || 'Intelligence Artificielle Active'} + +

+ Débloquez la traduction contextuelle haut de gamme pour des documents entiers tout en préservant le ton exact. +

+ + {t('dashboard.translate.provider.upgrade') || 'Passer Pro'} +
)}
diff --git a/frontend/src/app/dashboard/translate/page.tsx b/frontend/src/app/dashboard/translate/page.tsx index b214251..856da1b 100644 --- a/frontend/src/app/dashboard/translate/page.tsx +++ b/frontend/src/app/dashboard/translate/page.tsx @@ -7,6 +7,7 @@ import { Zap, CheckCircle2, Search, Languages, Wrench, Activity, Timer, Download, AlertTriangle, FileType, + Image as ImageIcon, } from 'lucide-react'; import { useFileUpload } from './useFileUpload'; import { useTranslationConfig } from './useTranslationConfig'; @@ -14,6 +15,7 @@ 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'; @@ -181,86 +183,96 @@ export default function TranslatePage() { const activeStepIdx = getActiveStepIdx(submit.progress); const qualityLabel = useMemo(() => getQualityLabel(t, config.provider), [t, config.provider]); - /* ═══════════════════════════════════════════════════════════════ */ - /* EDITORIAL LAYOUT */ - /* ═══════════════════════════════════════════════════════════════ */ return ( -
+
- {/* ── HEADER ────────────────────────────────────────────── */} + {/* ── HEADER (Landing Page Style) ───────────────────────── */}
{showProcessing ? ( <> - {t('landing.translate.processing')} -

- {t('landing.translate.aiAnalysis')} + Traitement en cours +

+ Analyse IA Active

-

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

+ Votre mise en page est en cours de préservation par notre moteur contextuel.

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

- {t('dashboard.translate.completed')} + Complété +

+ Traduction terminée

-

+

{submit.fileName}

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

- {t('landing.translate.title')} + Espace Pro +

+ Traduire un document

-

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

+ Conservez la mise en page d'origine grâce au moteur de traduction ultra-haute fidélité.

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

+ +
+ +
+ +

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

-

+

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

-
+ + {/* Simulated file triggers */} +
e.stopPropagation()}> {[ - { label: 'Word', icon: }, - { label: 'Excel', icon: }, - { label: 'Slides', icon: }, - { label: 'PDF', icon: }, + { label: 'Word (.docx)', type: 'word' as const, icon: }, + { label: 'Excel (.xlsx)', type: 'excel' as const, icon: }, + { label: 'Slides (.pptx)', type: 'slides' as const, icon: }, + { label: 'PDF (.pdf)', type: 'pdf' as const, icon: }, ].map(f => ( - + ))}
+ {/* Hidden file input for click-to-upload */} )} - {/* ── CONFIGURING STATE: File strip ─────────────────── */} + {/* ── CONFIGURING STATE: File indicator ──────────────── */} {showConfiguring && ( -
-

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

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

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

{upload.error}

} + {upload.error &&

{upload.error}

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

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

+ Moteur contextuel actif

-

+

{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/40 dark:bg-white/10 dark:text-white/40' + ? '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' )} > @@ -324,107 +336,100 @@ export default function TranslatePage() { />
-
- - {activeStepIdx < 2 ? t('dashboard.translate.steps.uploading') : t('dashboard.translate.steps.starting')} +
+ + {activeStepIdx < 2 ? 'Phase 1: Initialisation' : 'Phase 2: Reconstruction Contextuelle'} - + {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')} /> +
+ } value={`${Math.round(submit.progress)}%`} label="segments" /> + } value="99.9%" label="précision" /> + } value="Turbo" label="vitesse" /> + } value={formatElapsed(elapsed)} label="temps" />
)} {/* ── COMPLETE STATE: Success with download ─────────── */} {showComplete && ( -
-
-
-
-
- -
-
-

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

-

- {submit.fileName} -

-
+
+
+
+
+ +
+
+

+ Traduction terminée +

+

+ {submit.fileName} +

- - {qualityLabel} -
+ + ✓ Qualité Maître + +
-
- - -
+
+ +
)} {/* ── FAILED STATE ───────────────────────────────────── */} {showFailed && ( -
- {/* Error message — friendly language */} -
+
+
-
+
-
-

{t('dashboard.translate.error.title')}

-

{humanFriendlyError(submit.error)}

+
+

Erreur lors de la traduction

+

{humanFriendlyError(submit.error)}

- {/* File strip */} {(submit.fileName || upload.file?.name) && upload.file && ( -
- replaceInputRef.current?.click()} t={t} /> - -
+ replaceInputRef.current?.click()} t={t} /> )} + - {/* Action buttons */}
{upload.file && config.isConfigValid && ( )}
@@ -432,20 +437,20 @@ export default function TranslatePage() {
{/* ═══════════════════════════════════════════════════════ */} - {/* RIGHT (4 cols) — Config / Monitor / Summary */} + {/* RIGHT (5 cols) — Config / Monitor / Summary */} {/* ═══════════════════════════════════════════════════════ */} -
+
{/* ── CONFIG (upload / configuring / failed) ──────────── */} {(showUpload || showConfiguring || showFailed) && ( -
+
{/* Scrollable config content */} -
-

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

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

-
+
{config.mode === 'classic' && config.isPro && ( - + {t('dashboard.translate.glossaryLLMHint')} )} @@ -494,47 +499,70 @@ export default function TranslatePage() { /> )} + {/* Translate Images — Office files and LLM mode only */} + {!isPdf && config.mode === 'llm' && ( +
+
+ +
+ + {t('dashboard.translate.translateImages') || 'Traduire les images'} + + + {t('dashboard.translate.translateImagesDesc') || 'Traduire les textes incrustés'} + +
+
+ +
+ )} + {/* PDF mode selector */} {isPdf && ( -
-