diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index ccb6cc3..feb7c98 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, useRef } from 'react'; -import { BookText, Plus, Loader2, Check, ChevronDown, X, Globe } from 'lucide-react'; +import { BookOpen, Plus, Loader2, Check, ChevronDown, X, Globe, ChevronRight } from 'lucide-react'; import { API_BASE } from '@/lib/config'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; @@ -28,12 +28,13 @@ interface GlossarySelectorProps { sourceLang: string; targetLang: string; isPro: boolean; + mode: string; glossaryId: string | null; onChange: (id: string | null) => void; disabled?: boolean; } -export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, onChange, disabled }: GlossarySelectorProps) { +export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossaryId, onChange, disabled }: GlossarySelectorProps) { const { t } = useI18n(); const [glossaries, setGlossaries] = useState([]); const [templates, setTemplates] = useState([]); @@ -227,279 +228,271 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on const filteredGlossaries = langFiltered.length > 0 ? langFiltered : glossaries; const selected = glossaries.find(g => g.id === glossaryId); - return ( -
- {/* Header with Switch */} -
-
- - - {t('translate.glossary.title') || 'Glossaire personnel'} +
+
+
+ + + {t('translate.glossary.title') || 'Glossaire & Terminologie'} - + ({sourceFlag || 'AUTO'}➔{targetFlag})
- - {isPro && ( - { - setIsGlossaryEnabled(checked); - if (!checked) { + + {/* Active switch slider */} + {isPro && mode === 'llm' && ( + )}
- {/* Pro limitation message */} - {!isPro && ( -
-

+ {mode === 'classic' ? ( +

+ + Moteur neutre sans glossaire (IA uniquement) + +
+ ) : !isPro ? ( +
+

{t('translate.glossary.proOnly') || 'Passez Pro pour appliquer vos glossaires terminologiques.'}

- )} - - {/* Enabled glossary selector details */} - {isPro && isGlossaryEnabled && ( -
- {/* Dropdown trigger */} + ) : isGlossaryEnabled ? ( +
+ + {/* Select Glossary Trigger button */}
- - ) : ( - <> - - - {isLoading ? ( - {t('translate.glossary.loading') || 'Chargement...'} - ) : filteredGlossaries.length > 0 ? ( - t('translate.glossary.selectGlossary') || 'Sélectionner un glossaire…' - ) : ( - t('translate.glossary.noGlossaries') || 'Aucun glossaire disponible' - )} - - - )} - +
+ + {selected ? selected.name : (isLoading ? "Chargement..." : "Sélectionner un glossaire...")} + + + {selected + ? `${SUPPORTED_LANGUAGES.find(l => l.code === selected.source_language)?.flag || '🌐'} ➜ ${targetFlag} • ${selected.terms_count} termes` + : (filteredGlossaries.length > 0 ? "Aucun glossaire sélectionné" : "Aucun glossaire disponible") + } + +
+ - {/* Error panel */} + {/* Error message */} {error && ( -

{error}

+

{error}

)} - {/* Dropdown panel */} + {/* Selector Dropdown list */} {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') - } -

- )} -
- - {/* Templates shortcut button */} - {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 ( - - ); - })} -
- )} -
+
+ {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' + ? `Aucun glossaire pour ${sourceFlag}➔${targetFlag}` + : "Aucun glossaire disponible" + } +

)}
)}
- {/* Active Glossary detail section (Preview & Add inline) */} + {/* Dynamic Terms Preview block */} {selected && ( -
- {/* Preview header */} -
- Aperçu des termes - {selectedGlossaryDetail?.terms?.length || selected.terms_count} au total +
+
+ + Aperçu des correspondances actives : + + + {selectedGlossaryDetail?.terms?.length || selected.terms_count} au total +
- - {/* Dynamic scrollable terms preview */} -
- {isLoadingDetail ? ( -
- Chargement des termes... -
- ) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? ( - selectedGlossaryDetail.terms.map((t: any, i: number) => ( -
- - {t.source} - - - - {t.target} + + {isLoadingDetail ? ( +
+ Chargement... +
+ ) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? ( +
+ {selectedGlossaryDetail.terms.slice(0, 4).map((t: any, i: number) => ( +
+ + {t.source} ➔ {t.target} +
- )) - ) : ( -

Aucun terme dans ce glossaire.

- )} -
- - {/* Form to add a new term inline */} -
- setNewSource(e.target.value)} - disabled={isAddingTerm} - className="flex-1 min-w-0 bg-white dark:bg-[#141414] border border-brand-dark/10 dark:border-white/10 rounded-xl px-2.5 py-1.5 text-[10px] placeholder:text-brand-dark/30 dark:placeholder:text-white/30 focus:border-brand-accent focus:outline-none transition-colors" - /> - setNewTarget(e.target.value)} - disabled={isAddingTerm} - className="flex-1 min-w-0 bg-white dark:bg-[#141414] border border-brand-dark/10 dark:border-white/10 rounded-xl px-2.5 py-1.5 text-[10px] placeholder:text-brand-dark/30 dark:placeholder:text-white/30 focus:border-brand-accent focus:outline-none transition-colors" - /> - -
+
+ ) : ( +

Aucun terme dans ce glossaire.

+ )}
)} + + {/* Ultra-neat Quick Term Adder */} + {selected && ( +
+ setNewSource(e.target.value)} + disabled={isAddingTerm || disabled} + className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2 py-1 text-[8px] font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent" + /> + setNewTarget(e.target.value)} + disabled={isAddingTerm || disabled} + className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2 py-1 text-[8px] font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent" + /> + +
+ )} + + {/* Interactive Accordion for Templates Generator */} + {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 ( + + ); + })} +
+ )} +
+ )} +
+ ) : ( +
+ Moteur neutre sans glossaire appliqué
)}
diff --git a/frontend/src/app/dashboard/translate/page.tsx b/frontend/src/app/dashboard/translate/page.tsx index c9ebbd0..18edeaa 100644 --- a/frontend/src/app/dashboard/translate/page.tsx +++ b/frontend/src/app/dashboard/translate/page.tsx @@ -525,40 +525,48 @@ export default function TranslatePage() {
)} - {/* Glossary — Pro + LLM mode only */} - {config.isPro && config.mode === 'llm' && ( - - )} + {/* Glossary selector */} + - {/* 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'} - -
+ {/* Translate Images */} +
+
+
+ + + {t('dashboard.translate.translateImages') || "Traduire les images"} +
- )} + + {config.mode === 'classic' ? ( +
+ + Indisponible en mode Standard (IA uniquement) + +
+ ) : ( +
+ + {t('dashboard.translate.translateImagesDesc') || "Détecter et traduire automatiquement les textes incrustés dans vos images."} + +
+ )} +
{/* PDF mode selector */} {isPdf && ( diff --git a/frontend/src/test/constants.test.ts b/frontend/src/test/constants.test.ts index 6160953..128dba4 100644 --- a/frontend/src/test/constants.test.ts +++ b/frontend/src/test/constants.test.ts @@ -1,28 +1,19 @@ import { describe, it, expect } from 'vitest'; -import { baseNavItems, proNavItem, getNavItems } from '../app/dashboard/constants'; - -describe('getNavItems', () => { - it('should return only base items for free users', () => { - const items = getNavItems(false); - expect(items).toHaveLength(2); - expect(items).toEqual(baseNavItems); - }); - - it('should include pro item for pro users', () => { - const items = getNavItems(true); - expect(items).toHaveLength(3); - expect(items).toContain(proNavItem); - }); +import { baseNavItems } from '../app/dashboard/constants'; +describe('baseNavItems', () => { it('should have correct structure for base items', () => { baseNavItems.forEach(item => { - expect(item).toHaveProperty('label'); + expect(item).toHaveProperty('labelKey'); expect(item).toHaveProperty('href'); expect(item).toHaveProperty('icon'); }); }); - it('should have proOnly flag on proNavItem', () => { - expect(proNavItem.proOnly).toBe(true); + it('should flag glossaries and apiKeys as proOnly', () => { + const glossaries = baseNavItems.find(item => item.href === '/dashboard/glossaries'); + const apiKeys = baseNavItems.find(item => item.href === '/dashboard/api-keys'); + expect(glossaries?.proOnly).toBe(true); + expect(apiKeys?.proOnly).toBe(true); }); });