diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index 4d4fb30..ccb6cc3 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -1,10 +1,11 @@ 'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; -import { BookText, Plus, Loader2, Check, ChevronDown, X } from 'lucide-react'; +import { BookText, Plus, Loader2, Check, ChevronDown, X, Globe } from 'lucide-react'; import { API_BASE } from '@/lib/config'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; +import { Switch } from '@/components/ui/switch'; import { SUPPORTED_LANGUAGES } from '../glossaries/types'; interface GlossaryOption { @@ -37,12 +38,20 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on const [glossaries, setGlossaries] = useState([]); const [templates, setTemplates] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [isGlossaryEnabled, setIsGlossaryEnabled] = useState(!!glossaryId); + const [selectedGlossaryDetail, setSelectedGlossaryDetail] = useState(null); + const [isLoadingDetail, setIsLoadingDetail] = useState(false); const [importingId, setImportingId] = useState(null); const [error, setError] = useState(null); const [isOpen, setIsOpen] = useState(false); - const [showTemplates, setShowTemplates] = useState(true); + const [showTemplates, setShowTemplates] = useState(false); const containerRef = useRef(null); + // Form states for adding term + const [newSource, setNewSource] = useState(''); + const [newTarget, setNewTarget] = useState(''); + const [isAddingTerm, setIsAddingTerm] = useState(false); + const fetchData = useCallback(async () => { try { const token = localStorage.getItem('token'); @@ -69,14 +78,42 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on } }, []); + const fetchGlossaryDetail = useCallback(async (id: string) => { + setIsLoadingDetail(true); + try { + const token = localStorage.getItem('token'); + const headers: Record = {}; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const res = await fetch(`${API_BASE}/api/v1/glossaries/${id}`, { headers }); + if (res.ok) { + const data = await res.json(); + setSelectedGlossaryDetail(data.data || null); + } + } catch { + // ignore + } finally { + setIsLoadingDetail(false); + } + }, []); + useEffect(() => { fetchData(); }, [fetchData]); + // Synchronize glossary detail and enablement state with props + useEffect(() => { + if (glossaryId) { + setIsGlossaryEnabled(true); + fetchGlossaryDetail(glossaryId); + } else { + setSelectedGlossaryDetail(null); + } + }, [glossaryId, fetchGlossaryDetail]); + // 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); @@ -89,6 +126,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on ); if (existing) { onChange(existing.id); + setIsGlossaryEnabled(true); setIsOpen(false); return; } @@ -109,7 +147,10 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on const data = await res.json(); const newId = data.data?.id; await fetchData(); - if (newId) onChange(newId); + if (newId) { + onChange(newId); + setIsGlossaryEnabled(true); + } setIsOpen(false); } else { const errData = await res.json().catch(() => null); @@ -122,6 +163,61 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on } }; + const handleAddTerm = async (e: React.FormEvent) => { + e.preventDefault(); + if (!glossaryId || !newSource.trim() || !newTarget.trim()) return; + + setIsAddingTerm(true); + setError(null); + try { + const token = localStorage.getItem('token'); + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (token) headers['Authorization'] = `Bearer ${token}`; + + // Fetch latest terms + const detailRes = await fetch(`${API_BASE}/api/v1/glossaries/${glossaryId}`, { headers }); + let currentTerms = []; + if (detailRes.ok) { + const detailData = await detailRes.json(); + currentTerms = detailData.data?.terms || []; + } + + const mappedTerms = currentTerms.map((t: any) => ({ + source: t.source, + target: t.target, + translations: t.translations || {} + })); + + // Append + const updatedTerms = [...mappedTerms, { source: newSource.trim(), target: newTarget.trim(), translations: {} }]; + + const res = await fetch(`${API_BASE}/api/v1/glossaries/${glossaryId}`, { + method: 'PATCH', + headers, + body: JSON.stringify({ + terms: updatedTerms + }) + }); + + if (res.ok) { + setNewSource(''); + setNewTarget(''); + // Refresh details + fetchGlossaryDetail(glossaryId); + fetchData(); + } else { + const errData = await res.json().catch(() => null); + setError(errData?.message || 'Erreur lors de l\'ajout du terme'); + } + } catch { + setError('Erreur réseau'); + } finally { + setIsAddingTerm(false); + } + }; + const sourceFlag = SUPPORTED_LANGUAGES.find(l => l.code === sourceLang)?.flag ?? ''; const targetFlag = SUPPORTED_LANGUAGES.find(l => l.code === targetLang)?.flag ?? ''; @@ -129,177 +225,280 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on ? glossaries : glossaries.filter(g => g.source_language === sourceLang); - // If filtering by source language yields nothing, show all glossaries const filteredGlossaries = langFiltered.length > 0 ? langFiltered : glossaries; - const selected = glossaries.find(g => g.id === glossaryId); return ( -
- - - {/* Dropdown trigger */} - - - ) : ( - <> - {sourceFlag} - - {isLoading ? ( - {t('translate.glossary.loading')} - ) : filteredGlossaries.length > 0 ? ( - t('translate.glossary.selectGlossary') || 'Sélectionner un glossaire…' - ) : ( - t('translate.glossary.noGlossaries') || 'Aucun glossaire' - )} - - - )} - - +
- {/* Error */} - {error && ( -

{error}

+ {/* Pro limitation message */} + {!isPro && ( +
+

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

+
)} - {/* 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 ( + {/* Enabled glossary selector details */} + {isPro && isGlossaryEnabled && ( +
+ {/* Dropdown trigger */} +
+ + + ) : ( + <> + + + {isLoading ? ( + {t('translate.glossary.loading') || 'Chargement...'} + ) : filteredGlossaries.length > 0 ? ( + t('translate.glossary.selectGlossary') || 'Sélectionner un glossaire…' + ) : ( + t('translate.glossary.noGlossaries') || 'Aucun glossaire disponible' + )} + + + )} + + + + {/* Error panel */} + {error && ( +

{error}

+ )} + + {/* 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') + } +

+ )} +
+ + {/* 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 ( + + ); + })} +
+ )} +
+ )}
- ) : ( -

- {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 && ( - <> -
- + {/* Active Glossary detail section (Preview & Add inline) */} + {selected && ( +
+ {/* Preview header */} +
+ Aperçu des termes + {selectedGlossaryDetail?.terms?.length || selected.terms_count} au total
- {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 ( - - ); - })} -
- )} - + + {/* 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} + +
+ )) + ) : ( +

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" + /> + +
+
)}
)}