fix(frontend): increase translation config legibility and integrate templates in glossary selector
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m31s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m31s
This commit is contained in:
@@ -7,6 +7,7 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { SUPPORTED_LANGUAGES } from '../glossaries/types';
|
||||
import { languages as API_LANGUAGES } from '@/lib/api';
|
||||
|
||||
interface GlossaryOption {
|
||||
id: string;
|
||||
@@ -45,7 +46,6 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
const [importingId, setImportingId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showTemplates, setShowTemplates] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [filterByLang, setFilterByLang] = useState(sourceLang !== 'auto');
|
||||
@@ -225,8 +225,14 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
}
|
||||
};
|
||||
|
||||
const sourceFlag = SUPPORTED_LANGUAGES.find(l => l.code === sourceLang)?.flag ?? '';
|
||||
const targetFlag = SUPPORTED_LANGUAGES.find(l => l.code === targetLang)?.flag ?? '';
|
||||
const getFlag = useCallback((code: string) => {
|
||||
return API_LANGUAGES.find(l => l.code === code)?.flag ??
|
||||
SUPPORTED_LANGUAGES.find(l => l.code === code)?.flag ??
|
||||
code.toUpperCase();
|
||||
}, []);
|
||||
|
||||
const sourceFlag = useMemo(() => sourceLang === 'auto' ? '' : getFlag(sourceLang), [sourceLang, getFlag]);
|
||||
const targetFlag = useMemo(() => getFlag(targetLang), [targetLang, getFlag]);
|
||||
|
||||
const filteredGlossaries = useMemo(() => {
|
||||
if (!filterByLang || sourceLang === 'auto') {
|
||||
@@ -235,7 +241,15 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
return glossaries.filter(g => g.source_language === sourceLang);
|
||||
}, [glossaries, filterByLang, sourceLang]);
|
||||
|
||||
const filteredTemplates = useMemo(() => {
|
||||
if (!filterByLang || sourceLang === 'auto') {
|
||||
return templates;
|
||||
}
|
||||
return templates.filter(t => t.source_lang === sourceLang);
|
||||
}, [templates, filterByLang, sourceLang]);
|
||||
|
||||
const selected = glossaries.find(g => g.id === glossaryId);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-brand-muted/30 dark:bg-white/[0.02] border border-black/[0.03] dark:border-white/[0.03] p-4 rounded-xl space-y-3 text-left"
|
||||
@@ -243,11 +257,11 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen size={12} className="text-brand-accent shrink-0" />
|
||||
<span className="text-[9px] font-black uppercase tracking-[0.1em] text-brand-dark dark:text-white truncate">
|
||||
<BookOpen size={14} className="text-brand-accent shrink-0" />
|
||||
<span className="text-xs font-black uppercase tracking-[0.1em] text-brand-dark dark:text-white truncate">
|
||||
{t('translate.glossary.title') || 'Glossaire & Terminologie'}
|
||||
</span>
|
||||
<span className="text-[7.5px] text-brand-dark/40 dark:text-white/40 shrink-0 font-medium font-mono">
|
||||
<span className="text-[10px] text-brand-dark/40 dark:text-white/40 shrink-0 font-medium font-mono">
|
||||
({sourceFlag || 'AUTO'}➔{targetFlag})
|
||||
</span>
|
||||
</div>
|
||||
@@ -282,13 +296,13 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
|
||||
{mode === 'classic' ? (
|
||||
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center">
|
||||
<span className="text-[7.5px] font-black uppercase opacity-45 block">
|
||||
<span className="text-xs font-black uppercase opacity-45 block">
|
||||
Moteur neutre sans glossaire (IA uniquement)
|
||||
</span>
|
||||
</div>
|
||||
) : !isPro ? (
|
||||
<div className="p-2.5 rounded-lg bg-brand-dark/5 dark:bg-white/5 text-center">
|
||||
<p className="text-[7.5px] text-brand-dark/50 dark:text-white/45 leading-relaxed font-light">
|
||||
<p className="text-xs text-brand-dark/50 dark:text-white/45 leading-relaxed font-light">
|
||||
{t('translate.glossary.proOnly') || 'Passez Pro pour appliquer vos glossaires terminologiques.'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -302,44 +316,44 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
"w-full bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 py-2 px-3 rounded-lg flex items-center justify-between shadow-sm transition-all",
|
||||
"w-full bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 py-2.5 px-3 rounded-lg flex items-center justify-between shadow-sm transition-all cursor-pointer",
|
||||
isOpen && "border-brand-accent dark:border-brand-accent",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className="text-left min-w-0 flex-1 pr-2">
|
||||
<span className="text-[9px] font-black text-brand-dark dark:text-white tracking-tight block truncate">
|
||||
<span className="text-xs font-bold text-brand-dark dark:text-white tracking-tight block truncate">
|
||||
{selected ? selected.name : (isLoading ? "Chargement..." : "Sélectionner un glossaire...")}
|
||||
</span>
|
||||
<span className="text-[7px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
|
||||
<span className="text-[10px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
|
||||
{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")
|
||||
? `${getFlag(selected.source_language)} ➜ ${targetFlag} • ${selected.terms_count} termes`
|
||||
: (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? "Sélectionnez un glossaire" : "Aucun glossaire disponible")
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown size={11} className={cn("text-brand-dark/30 dark:text-white/30 shrink-0 transition-transform duration-200", isOpen && "rotate-180")} />
|
||||
<ChevronDown size={14} className={cn("text-brand-dark/30 dark:text-white/30 shrink-0 transition-transform duration-200", isOpen && "rotate-180")} />
|
||||
</button>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<p className="text-[7.5px] text-red-500 pl-1 mt-1 font-medium">{error}</p>
|
||||
<p className="text-xs text-red-500 pl-1 mt-1 font-medium">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Selector Dropdown list */}
|
||||
{isOpen && !disabled && (
|
||||
<div className="absolute top-[102%] left-0 right-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-1.5 z-40 max-h-48 overflow-y-auto animate-fade-in">
|
||||
<div className="absolute top-[102%] left-0 right-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-1.5 z-40 max-h-64 overflow-y-auto animate-fade-in animate-in fade-in zoom-in-95 duration-100">
|
||||
{/* Filter toggle header */}
|
||||
{sourceLang !== 'auto' && glossaries.length > 0 && (
|
||||
<div className="px-2 py-1 border-b border-black/[0.03] dark:border-white/[0.03] flex justify-between items-center mb-1">
|
||||
<span className="text-[8px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
|
||||
{sourceLang !== 'auto' && (glossaries.length > 0 || templates.length > 0) && (
|
||||
<div className="px-2 py-1.5 border-b border-black/[0.03] dark:border-white/[0.03] flex justify-between items-center mb-1">
|
||||
<span className="text-[10px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
|
||||
Filtrer par langue ({sourceFlag})
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilterByLang(!filterByLang)}
|
||||
className={cn(
|
||||
"text-[8px] font-bold px-1.5 py-0.5 rounded transition-colors",
|
||||
"text-[10px] font-bold px-2 py-0.5 rounded transition-colors cursor-pointer",
|
||||
filterByLang ? "bg-brand-accent/10 text-brand-accent" : "bg-brand-dark/10 dark:bg-white/10 text-brand-dark/50 dark:text-white/50"
|
||||
)}
|
||||
>
|
||||
@@ -348,53 +362,118 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredGlossaries.length > 0 ? (
|
||||
filteredGlossaries.map(g => {
|
||||
const flag = SUPPORTED_LANGUAGES.find(l => l.code === g.source_language)?.flag ?? '';
|
||||
const isSelected = g.id === glossaryId;
|
||||
return (
|
||||
<button
|
||||
key={g.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(isSelected ? null : g.id);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center justify-between",
|
||||
isSelected
|
||||
? 'bg-brand-accent/5 text-brand-accent'
|
||||
: 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70'
|
||||
)}
|
||||
>
|
||||
<div className="text-left min-w-0 flex-1 pr-2">
|
||||
<span className="text-[9px] font-black block leading-none truncate">{g.name}</span>
|
||||
<span className="text-[7.5px] uppercase tracking-wider text-brand-dark/40 dark:text-white/40 font-bold block mt-1">
|
||||
{flag || '🌐'} ➜ {targetFlag} • {g.terms_count} termes
|
||||
</span>
|
||||
</div>
|
||||
{isSelected && <Check size={10} className="text-brand-accent shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
{/* Custom Glossaries Section */}
|
||||
{filteredGlossaries.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<div className="px-2 py-1 text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
|
||||
Mes Glossaires
|
||||
</div>
|
||||
{filteredGlossaries.map(g => {
|
||||
const flag = getFlag(g.source_language);
|
||||
const isSelected = g.id === glossaryId;
|
||||
return (
|
||||
<button
|
||||
key={g.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(isSelected ? null : g.id);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"w-full text-left px-2.5 py-2 rounded-lg transition-colors flex items-center justify-between cursor-pointer",
|
||||
isSelected
|
||||
? 'bg-brand-accent/5 text-brand-accent'
|
||||
: 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70'
|
||||
)}
|
||||
>
|
||||
<div className="text-left min-w-0 flex-1 pr-2">
|
||||
<span className="text-xs font-bold block leading-none truncate">{g.name}</span>
|
||||
<span className="text-[10px] uppercase tracking-wider text-brand-dark/40 dark:text-white/45 font-bold block mt-1">
|
||||
{flag} ➜ {targetFlag} • {g.terms_count} termes
|
||||
</span>
|
||||
</div>
|
||||
{isSelected && <Check size={12} className="text-brand-accent shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Templates Section */}
|
||||
{filteredTemplates.length > 0 && (
|
||||
<div>
|
||||
<div className="px-2 py-1 text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest border-t border-black/[0.03] dark:border-white/[0.03] pt-2 mt-1">
|
||||
Modèles disponibles
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-0.5">
|
||||
{filteredTemplates.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;
|
||||
const flag = getFlag(tmpl.source_lang);
|
||||
const tFlag = getFlag(tmpl.target_lang);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tmpl.id}
|
||||
type="button"
|
||||
disabled={isImporting || disabled}
|
||||
onClick={() => {
|
||||
if (existingGlossary) {
|
||||
onChange(isAlreadySelected ? null : existingGlossary.id);
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
handleImportTemplate(tmpl);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"w-full text-left px-2.5 py-2 rounded-lg transition-colors flex items-center justify-between cursor-pointer",
|
||||
isAlreadySelected
|
||||
? 'bg-brand-accent/5 text-brand-accent'
|
||||
: 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70'
|
||||
)}
|
||||
>
|
||||
<div className="text-left min-w-0 flex-1 pr-2">
|
||||
<span className="text-xs font-bold block leading-none truncate">
|
||||
{isImporting ? 'Importation...' : tmpl.name.split('/')[0].trim()}
|
||||
</span>
|
||||
<span className="text-[10px] uppercase tracking-wider text-brand-dark/40 dark:text-white/45 font-bold block mt-1">
|
||||
{flag} ➜ {tFlag} • {tmpl.terms_count} termes {existingGlossary ? '(Importé)' : ''}
|
||||
</span>
|
||||
</div>
|
||||
{isImporting ? (
|
||||
<Loader2 size={12} className="animate-spin text-brand-accent shrink-0" />
|
||||
) : isAlreadySelected ? (
|
||||
<Check size={12} className="text-brand-accent shrink-0" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{filteredGlossaries.length === 0 && filteredTemplates.length === 0 && (
|
||||
<div className="px-2.5 py-4 text-center">
|
||||
<p className="text-[9px] text-brand-dark/45 dark:text-white/45 italic mb-3">
|
||||
Aucun glossaire pour la langue {sourceFlag || sourceLang.toUpperCase()}.
|
||||
<p className="text-xs text-brand-dark/45 dark:text-white/45 italic mb-3">
|
||||
Aucun glossaire ni modèle disponible pour la langue {sourceFlag || sourceLang.toUpperCase()}.
|
||||
</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{filterByLang && glossaries.length > 0 && (
|
||||
{filterByLang && (glossaries.length > 0 || templates.length > 0) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilterByLang(false)}
|
||||
className="w-full py-1.5 px-2 bg-brand-muted dark:bg-white/5 hover:bg-brand-muted/70 text-brand-dark dark:text-white rounded-lg text-[8px] font-bold uppercase tracking-wider transition-colors"
|
||||
className="w-full py-2 px-2 bg-brand-muted dark:bg-white/5 hover:bg-brand-muted/70 text-brand-dark dark:text-white rounded-lg text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
|
||||
>
|
||||
Afficher tous les glossaires
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href="/dashboard/glossaries"
|
||||
className="w-full py-1.5 px-2 bg-brand-dark dark:bg-white text-white dark:text-brand-dark hover:opacity-90 rounded-lg text-[8px] font-bold uppercase tracking-wider block text-center transition-opacity"
|
||||
className="w-full py-2 px-2 bg-brand-dark dark:bg-white text-white dark:text-brand-dark hover:opacity-90 rounded-lg text-xs font-bold uppercase tracking-wider block text-center transition-opacity cursor-pointer"
|
||||
>
|
||||
Créer un glossaire ➔
|
||||
</a>
|
||||
@@ -409,43 +488,43 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
{selected && (
|
||||
<div className="bg-white/80 dark:bg-[#1a1a1a]/40 rounded-lg p-2.5 border border-black/[0.02] dark:border-white/[0.02] space-y-2">
|
||||
<div className="flex justify-between items-center border-b border-black/[0.02] dark:border-white/[0.02] pb-1">
|
||||
<span className="text-[7.5px] font-black uppercase tracking-wider text-brand-dark/30 dark:text-white/30 block">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-brand-dark/30 dark:text-white/30 block">
|
||||
Aperçu des correspondances actives :
|
||||
</span>
|
||||
<span className="text-[7px] font-bold text-brand-dark/40 dark:text-white/40">
|
||||
<span className="text-xs font-bold text-brand-dark/40 dark:text-white/40">
|
||||
{selectedGlossaryDetail?.terms?.length || selected.terms_count} au total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isLoadingDetail ? (
|
||||
<div className="flex items-center justify-center py-3 gap-1.5 text-[8px] text-brand-dark/40 dark:text-white/30 font-light">
|
||||
<Loader2 size={10} className="animate-spin text-brand-accent" /> Chargement...
|
||||
<div className="flex items-center justify-center py-3 gap-1.5 text-xs text-brand-dark/40 dark:text-white/30 font-light">
|
||||
<Loader2 size={12} className="animate-spin text-brand-accent" /> Chargement...
|
||||
</div>
|
||||
) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-1 max-h-[105px] overflow-y-auto pr-1">
|
||||
<div className="grid grid-cols-1 gap-1 max-h-[120px] overflow-y-auto pr-1">
|
||||
{selectedGlossaryDetail.terms.slice(0, 4).map((t: any, i: number) => (
|
||||
<div key={t.id || i} className="flex justify-between items-center bg-brand-muted/30 dark:bg-white/5 px-2 py-1 rounded-md text-[8px]">
|
||||
<span className="font-extrabold text-brand-dark/75 dark:text-white/70 truncate max-w-[220px]">
|
||||
<div key={t.id || i} className="flex justify-between items-center bg-brand-muted/30 dark:bg-white/5 px-2 py-1.5 rounded-md text-xs">
|
||||
<span className="font-semibold text-brand-dark/75 dark:text-white/70 truncate max-w-[220px]">
|
||||
{t.source} ➔ {t.target}
|
||||
</span>
|
||||
<span className="inline-block w-1.5 h-1.5 bg-brand-accent rounded-full shrink-0" />
|
||||
</div>
|
||||
))}
|
||||
{selectedGlossaryDetail.terms.length > 4 && (
|
||||
<span className="text-[7px] text-brand-dark/40 dark:text-white/40 block text-right font-medium mt-1">
|
||||
<span className="text-[10px] text-brand-dark/40 dark:text-white/40 block text-right font-medium mt-1">
|
||||
+ {selectedGlossaryDetail.terms.length - 4} autres termes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-3 text-[8px] text-brand-dark/40 dark:text-white/30 italic text-center font-light">Aucun terme dans ce glossaire.</p>
|
||||
<p className="py-3 text-xs text-brand-dark/40 dark:text-white/30 italic text-center font-light">Aucun terme dans ce glossaire.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ultra-neat Quick Term Adder */}
|
||||
{selected && (
|
||||
<form onSubmit={handleAddTerm} className="flex gap-1">
|
||||
<form onSubmit={handleAddTerm} className="flex gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -453,7 +532,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
value={newSource}
|
||||
onChange={(e) => 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"
|
||||
className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
@@ -462,80 +541,26 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
value={newTarget}
|
||||
onChange={(e) => 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"
|
||||
className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isAddingTerm || disabled || !newSource.trim() || !newTarget.trim()}
|
||||
className="px-2.5 bg-brand-dark dark:bg-white text-white dark:text-brand-dark rounded-lg flex items-center justify-center disabled:opacity-35 transition-colors"
|
||||
className="px-3 bg-brand-dark dark:bg-white text-white dark:text-brand-dark rounded-lg flex items-center justify-center disabled:opacity-35 transition-colors cursor-pointer"
|
||||
title="Ajouter le terme"
|
||||
>
|
||||
{isAddingTerm ? (
|
||||
<Loader2 size={10} className="animate-spin" />
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
) : (
|
||||
<Plus size={10} />
|
||||
<Plus size={14} />
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Interactive Accordion for Templates Generator */}
|
||||
{templates.length > 0 && (
|
||||
<div className="border-t border-black/[0.03] dark:border-white/[0.03] pt-2 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTemplates(!showTemplates)}
|
||||
className="w-full flex items-center justify-between text-[8px] font-black uppercase tracking-wider text-brand-dark/40 dark:text-white/40 hover:text-brand-dark dark:hover:text-white transition-colors"
|
||||
>
|
||||
<span>⚡ Créer ou Charger depuis un Template</span>
|
||||
<ChevronRight size={10} className={cn("transform transition-transform text-brand-accent", showTemplates ? 'rotate-95' : '')} />
|
||||
</button>
|
||||
|
||||
{showTemplates && (
|
||||
<div className="grid grid-cols-2 gap-1 mt-2 p-1 bg-white/40 dark:bg-black/10 rounded-lg animate-fade-in max-h-32 overflow-y-auto">
|
||||
{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 (
|
||||
<button
|
||||
key={tmpl.id}
|
||||
type="button"
|
||||
disabled={isImporting || disabled}
|
||||
onClick={() => {
|
||||
if (existingGlossary) {
|
||||
onChange(isAlreadySelected ? null : existingGlossary.id);
|
||||
} else {
|
||||
handleImportTemplate(tmpl);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"px-2 py-1 rounded text-left transition-all",
|
||||
isAlreadySelected
|
||||
? 'bg-brand-accent/10 border-l-2 border-brand-accent'
|
||||
: 'hover:bg-brand-muted dark:hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<span className="text-[7.5px] font-extrabold text-brand-dark dark:text-white block truncate leading-none">
|
||||
{isImporting ? 'Importation...' : tmpl.name.split('/')[0].trim()}
|
||||
</span>
|
||||
<span className="text-[6px] tracking-wider font-bold text-brand-dark/30 dark:text-white/30 block leading-tight uppercase">
|
||||
{tmpl.terms_count} termes
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center animate-fade-in">
|
||||
<span className="text-[7.5px] font-black uppercase opacity-40">Moteur neutre sans glossaire appliqué</span>
|
||||
<span className="text-xs font-black uppercase opacity-40">Moteur neutre sans glossaire appliqué</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -66,28 +66,28 @@ function Combobox({
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
'w-full py-2 px-3 bg-brand-muted/60 dark:bg-white/5 rounded-xl border text-[10px] font-bold uppercase tracking-wider text-brand-dark dark:text-white flex items-center justify-between hover:border-brand-accent/50 transition-all select-none cursor-pointer',
|
||||
'w-full py-2.5 px-3.5 bg-brand-muted/60 dark:bg-white/5 rounded-xl border text-xs font-bold uppercase tracking-wider text-brand-dark dark:text-white flex items-center justify-between hover:border-brand-accent/50 transition-all select-none cursor-pointer',
|
||||
open ? 'border-brand-accent/50' : 'border-brand-accent/20 dark:border-white/10'
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{label || placeholder}</span>
|
||||
<ChevronDown className={cn('size-3 shrink-0 text-brand-accent transition-transform ms-2', open && 'rotate-180')} />
|
||||
<ChevronDown className={cn('size-3.5 shrink-0 text-brand-accent transition-transform ms-2', open && 'rotate-180')} />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute top-[102%] right-0 left-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-2 z-50 max-h-48 overflow-y-auto animate-fade-in">
|
||||
<div className="border-b border-black/5 dark:border-white/5 px-2 py-1">
|
||||
<div className="border-b border-black/5 dark:border-white/5 px-2 py-1.5">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="Search..."
|
||||
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"
|
||||
className="w-full bg-transparent px-1 py-1 text-xs outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[160px] overflow-y-auto p-1 mt-1 space-y-0.5">
|
||||
{filtered.length === 0 && (
|
||||
<div className="px-3 py-3 text-center text-[9px] text-brand-dark/40 dark:text-white/40">No results</div>
|
||||
<div className="px-3 py-3 text-center text-xs text-brand-dark/40 dark:text-white/40">No results</div>
|
||||
)}
|
||||
{filtered.map(lang => (
|
||||
<button
|
||||
@@ -95,14 +95,14 @@ function Combobox({
|
||||
type="button"
|
||||
onClick={() => { onChange(lang.code); setOpen(false); setQuery(''); }}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-[9px] font-bold uppercase tracking-wider transition-colors cursor-pointer',
|
||||
'flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer',
|
||||
value === lang.code
|
||||
? 'bg-brand-accent/10 text-brand-accent'
|
||||
: 'text-brand-dark/70 dark:text-white/70 hover:bg-brand-muted dark:hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{lang.name}</span>
|
||||
{value === lang.code && <Check className="size-3 text-brand-accent shrink-0" />}
|
||||
{value === lang.code && <Check className="size-3.5 text-brand-accent shrink-0" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -143,7 +143,7 @@ export default function LanguageSelector({
|
||||
<div className="grid grid-cols-11 gap-2 items-center">
|
||||
{/* Source */}
|
||||
<div className="col-span-5 relative text-left">
|
||||
<span className="text-[8px] font-bold text-brand-dark/30 dark:text-white/30 uppercase tracking-widest block mb-1">Source</span>
|
||||
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">Source</span>
|
||||
<Combobox
|
||||
value={sourceLang}
|
||||
options={languages}
|
||||
@@ -174,7 +174,7 @@ export default function LanguageSelector({
|
||||
|
||||
{/* Target */}
|
||||
<div className="col-span-5 relative text-left">
|
||||
<span className="text-[8px] font-bold text-brand-dark/30 dark:text-white/30 uppercase tracking-widest block mb-1">Cible</span>
|
||||
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">Cible</span>
|
||||
<Combobox
|
||||
value={targetLang}
|
||||
options={languages}
|
||||
|
||||
@@ -142,7 +142,7 @@ export function ProviderSelector({
|
||||
type="button"
|
||||
onClick={() => onProviderChange(p.id)}
|
||||
className={cn(
|
||||
'p-2 py-1.5 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-center min-h-[44px]',
|
||||
'p-2.5 py-2 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-center min-h-[48px]',
|
||||
isSelected
|
||||
? 'border-brand-accent bg-brand-accent/5 dark:bg-brand-accent/10'
|
||||
: 'border-black/[0.05] dark:border-white/[0.05] bg-brand-muted/20 dark:bg-zinc-800/10 hover:bg-brand-muted/50 dark:hover:bg-zinc-800/20'
|
||||
@@ -150,14 +150,14 @@ export function ProviderSelector({
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={cn(
|
||||
"text-[8px] font-black uppercase tracking-tight truncate flex-1",
|
||||
"text-[9.5px] font-black uppercase tracking-tight truncate flex-1",
|
||||
isSelected ? 'text-brand-accent' : 'text-brand-dark/40 dark:text-white/40'
|
||||
)}>
|
||||
Standard
|
||||
</span>
|
||||
{isSelected && <div className="w-1.5 h-1.5 rounded-full bg-brand-accent animate-pulse shrink-0 ml-1" />}
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-brand-dark dark:text-white block leading-tight truncate">
|
||||
<span className="text-xs font-bold text-brand-dark dark:text-white block leading-tight truncate">
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
@@ -167,6 +167,7 @@ export function ProviderSelector({
|
||||
const renderLlmCard = (p: AvailableProvider, locked: boolean) => {
|
||||
const isSelected = provider === p.id;
|
||||
const theme = LLM_THEMES[p.id] || DEFAULT_LLM_THEME;
|
||||
const displayLabel = p.model || p.label.replace(/^Traduction\s+IA\s+/i, '');
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -175,7 +176,7 @@ export function ProviderSelector({
|
||||
disabled={locked}
|
||||
onClick={() => !locked && onProviderChange(p.id)}
|
||||
className={cn(
|
||||
'p-2 py-1.5 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-between min-h-[58px]',
|
||||
'p-2 py-2 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-between min-h-[64px]',
|
||||
isSelected
|
||||
? 'border-brand-accent bg-brand-accent/5 dark:bg-brand-accent/10'
|
||||
: locked
|
||||
@@ -185,7 +186,7 @@ export function ProviderSelector({
|
||||
>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className={cn(
|
||||
"text-[8px] font-black uppercase tracking-tight truncate flex-1",
|
||||
"text-[9px] font-black uppercase tracking-tight truncate flex-1",
|
||||
isSelected ? 'text-brand-accent' : 'text-brand-dark/40 dark:text-white/40'
|
||||
)}>
|
||||
{theme.badge}
|
||||
@@ -193,10 +194,10 @@ export function ProviderSelector({
|
||||
{isSelected && <div className="w-1.5 h-1.5 rounded-full bg-brand-accent animate-pulse shrink-0 ml-1" />}
|
||||
{locked && <Lock className="size-2 text-brand-dark/40 dark:text-white/40 shrink-0 ml-1" />}
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-brand-dark dark:text-white block leading-none mb-0.5 truncate">
|
||||
{p.label}
|
||||
<span className="text-xs font-bold text-brand-dark dark:text-white block leading-none mb-0.5 truncate">
|
||||
{displayLabel}
|
||||
</span>
|
||||
<span className="text-[7.5px] text-brand-dark/40 dark:text-white/45 uppercase font-bold block leading-none truncate">
|
||||
<span className="text-[8.5px] text-brand-dark/40 dark:text-white/45 uppercase font-bold block leading-none truncate">
|
||||
{theme.subBadge}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user