diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index f5abda3..39d4cb8 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useState, useEffect, useCallback } from 'react'; -import { BookText, Plus, Loader2, Lock, Check, AlertCircle } from 'lucide-react'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { BookText, Plus, Loader2, Check, ChevronDown, X } from 'lucide-react'; import { API_BASE } from '@/lib/config'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; @@ -39,6 +39,9 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on const [isLoading, setIsLoading] = useState(true); const [importingId, setImportingId] = useState(null); const [error, setError] = useState(null); + const [isOpen, setIsOpen] = useState(false); + const [showTemplates, setShowTemplates] = useState(false); + const containerRef = useRef(null); const fetchData = useCallback(async () => { try { @@ -68,13 +71,25 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on useEffect(() => { fetchData(); }, [fetchData]); + // Close dropdown on outside click + useEffect(() => { + function handleClick(e: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsOpen(false); + setShowTemplates(false); + } + } + if (isOpen) document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [isOpen]); + const handleImportTemplate = async (template: TemplateOption) => { - // If a glossary with this template's name already exists, just select it const existing = glossaries.find( g => g.name.toLowerCase().includes(template.name.toLowerCase().split('/')[0].trim()) ); if (existing) { onChange(existing.id); + setIsOpen(false); return; } @@ -93,13 +108,14 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on if (res.ok) { const data = await res.json(); const newId = data.data?.id; - await fetchData(); // Refresh glossary list + await fetchData(); if (newId) onChange(newId); + setIsOpen(false); } else { const errData = await res.json().catch(() => null); setError(errData?.message || `Import failed (${res.status})`); } - } catch (e) { + } catch { setError('Network error'); } finally { setImportingId(null); @@ -116,182 +132,171 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on const selected = glossaries.find(g => g.id === glossaryId); return ( -
+
- {!isPro ? ( -
- - - {t('translate.glossary.proOnly')} - - - Pro - -
- ) : ( -
- {/* Error */} - {error && ( -
- - {error} + {/* Dropdown trigger */} + - ); - })} +
+
{selected.name}
+
{selected.terms_count} {t('translate.glossary.terms')}
- )} + + + ) : ( + <> + {sourceFlag} + + {isLoading ? ( + {t('translate.glossary.loading')} + ) : filteredGlossaries.length > 0 ? ( + t('translate.glossary.selectGlossary') || 'Sélectionner un glossaire…' + ) : ( + t('translate.glossary.noGlossaries') || 'Aucun glossaire' + )} + + + )} + + - {/* Also show selected glossary if filtered out */} - {selected && !filteredGlossaries.find(g => g.id === selected.id) && ( -
- - {t('translate.glossary.myGlossaries') || 'Mes glossaires'} - - -
- )} + {/* Error */} + {error && ( +

{error}

+ )} - {/* Templates */} - {!isLoading && templates.length > 0 && ( -
- - {t('translate.glossary.fromTemplate') || 'Créer depuis un template'} - -
- {templates.map(tmpl => { - const isImporting = importingId === tmpl.id; - const existingGlossary = glossaries.find( - g => g.name.toLowerCase().includes(tmpl.name.toLowerCase().split('/')[0].trim()) - ); - const isAlreadySelected = existingGlossary?.id === glossaryId; + {/* Dropdown panel */} + {isOpen && !disabled && ( +
+ {/* Glossary list */} +
+ {filteredGlossaries.length > 0 ? ( +
+ {filteredGlossaries.map(g => { + const flag = SUPPORTED_LANGUAGES.find(l => l.code === g.source_language)?.flag ?? ''; + const isSelected = g.id === glossaryId; return ( ); })}
-
- )} + ) : ( +

+ {sourceLang !== 'auto' + ? `${t('translate.glossary.noGlossaryForPair') || 'Aucun glossaire pour'} ${sourceFlag}→${targetFlag}` + : (t('translate.glossary.noGlossaries') || 'Aucun glossaire') + } +

+ )} +
- {/* Loading */} - {isLoading && ( -
- - {t('translate.glossary.loading') || 'Chargement...'} -
- )} - - {/* Empty */} - {!isLoading && filteredGlossaries.length === 0 && !selected && ( -

- {sourceLang !== 'auto' - ? `${t('translate.glossary.noGlossaryForPair') || 'Aucun glossaire pour'} ${sourceFlag}→${targetFlag}` - : (t('translate.glossary.noGlossaries') || 'Aucun glossaire') - } -

+ {/* Templates section — collapsed by default */} + {templates.length > 0 && ( + <> +
+ +
+ {showTemplates && ( +
+ {templates.map(tmpl => { + const isImporting = importingId === tmpl.id; + const existingGlossary = glossaries.find( + g => g.name.toLowerCase().includes(tmpl.name.toLowerCase().split('/')[0].trim()) + ); + const isAlreadySelected = existingGlossary?.id === glossaryId; + return ( + + ); + })} +
+ )} + )}
)} diff --git a/frontend/src/app/dashboard/translate/page.tsx b/frontend/src/app/dashboard/translate/page.tsx index b049131..ec4be71 100644 --- a/frontend/src/app/dashboard/translate/page.tsx +++ b/frontend/src/app/dashboard/translate/page.tsx @@ -438,116 +438,138 @@ export default function TranslatePage() { {/* ── CONFIG (upload / configuring / failed) ──────────── */} {(showUpload || showConfiguring || showFailed) && ( -
-
-
-

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

+
+ {/* Scrollable config content */} +
+

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

-
- +
+ - + - {/* Glossary — Pro + LLM mode only */} - {config.isPro && config.mode === 'llm' && ( - - )} - - {/* PDF mode selector */} - {isPdf && ( -
- -
- - -
-
- )} -
-
- - {/* ── TRANSLATE BUTTON — sticky at bottom, always visible ── */} -
- - {!upload.file && ( -

↑ Déposez d'abord un fichier

+ {/* Active mode badge */} + {config.provider && ( +
+ + {config.mode === 'llm' ? ( + <> Mode IA + ) : ( + <> Mode Classique + )} + + {config.mode === 'classic' && config.isPro && ( + + Glossaires disponibles en mode IA + + )} +
)} - {upload.file && !config.targetLang && ( -

↑ Sélectionnez une langue cible

+ + {/* Glossary — Pro + LLM mode only */} + {config.isPro && config.mode === 'llm' && ( + + )} + + {/* PDF mode selector */} + {isPdf && ( +
+ +
+ + +
+
)}
-
+ {/* ── TRANSLATE BUTTON — fixed at bottom, never scrolls away ── */} +
+ + {!upload.file && ( +

↑ Déposez d'abord un fichier

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

↑ Sélectionnez une langue cible

+ )} +
+ +
{t('landing.translate.zeroRetention')}