Files
office_translator/frontend/src/app/dashboard/glossaries/page.tsx
sepehr 9ab166c31e
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m58s
fix: glossaries page support for multilingual (target_language='multi')
The /dashboard/glossaries page had two issues:
1. SUPPORTED_LANGUAGES had no 'multi' code, so multilingual glossaries
   showed '🌐 undefined' instead of '🌐 Multilingue'
2. The Compatible/Autre cible badge compared target_language directly
   with currentTargetLang, so 'multi' never matched → always showed
   'Autre cible'. Now 'multi' is treated as compatible with any target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:48:48 +02:00

626 lines
30 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import {
BookText, Plus, Library, Calendar, Hash,
Zap, Save, Trash2, MessageSquare, Loader2,
Monitor, Scale, Stethoscope, BarChart3,
Megaphone, ShoppingCart, FlaskConical, Users,
CheckCircle2, AlertCircle, ArrowRight, MousePointerClick,
Info, ExternalLink,
} from 'lucide-react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { useUser } from '@/app/dashboard/useUser';
import { useI18n } from '@/lib/i18n';
import { useGlossaries, useGlossary } from './useGlossaries';
import type { Glossary, GlossaryTermInput, GlossaryListItem } from './types';
import { ProUpgradePrompt } from './ProUpgradePrompt';
import { CreateGlossaryDialog } from './CreateGlossaryDialog';
import { EditGlossaryDialog } from './EditGlossaryDialog';
import { DeleteGlossaryDialog } from './DeleteGlossaryDialog';
import { useToast } from '@/components/ui/toast';
import { SUPPORTED_LANGUAGES } from './types';
import { useTranslationStore } from '@/lib/store';
import { API_BASE } from '@/lib/config';
const PRESETS = [
{ key: 'it', title: 'IT / Logiciel', desc: 'Développement, infrastructure, DevOps', icon: Monitor, templateId: 'technology' },
{ key: 'legal', title: 'Juridique / Contrats', desc: 'Droit des affaires, contentieux', icon: Scale, templateId: 'legal' },
{ key: 'medical', title: 'Médical / Santé', desc: 'Pharmacologie, chirurgie, diagnostic', icon: Stethoscope, templateId: 'medical' },
{ key: 'finance', title: 'Finance / Comptabilité', desc: 'IFRS, bilans, fiscalité', icon: BarChart3, templateId: 'finance' },
{ key: 'marketing', title: 'Marketing / Publicité', desc: 'Digital, branding, analytics', icon: Megaphone, templateId: 'marketing' },
{ key: 'hr', title: 'RH / Ressources Humaines', desc: 'Contrats, politiques, recrutement', icon: Users, templateId: 'hr' },
{ key: 'scientific', title: 'Scientifique / Recherche', desc: 'Publications, thèses, articles', icon: FlaskConical, templateId: 'scientific' },
{ key: 'ecommerce', title: 'E-commerce / Vente', desc: 'Boutiques en ligne, catalogues, CRM', icon: ShoppingCart, templateId: 'ecommerce' },
];
export default function GlossariesPage() {
const { t } = useI18n();
const { data: user, isLoading: isLoadingUser } = useUser();
const {
glossaries,
total,
isLoading: isLoadingGlossaries,
isCreating,
isUpdating,
isDeleting,
isImportingTemplate,
createGlossary,
updateGlossary,
deleteGlossary,
importTemplate,
} = useGlossaries();
const { toast } = useToast();
const { settings, updateSettings } = useTranslationStore();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedGlossary, setSelectedGlossary] = useState<GlossaryListItem | null>(null);
const [glossaryToDelete, setGlossaryToDelete] = useState<{ id: string; name: string } | null>(null);
const [systemPrompt, setSystemPrompt] = useState(settings.systemPrompt);
const [isSavingPrompt, setIsSavingPrompt] = useState(false);
const [promptSaved, setPromptSaved] = useState(false);
const [creatingPreset, setCreatingPreset] = useState<string | null>(null);
const { glossary: fullGlossary, isLoading: isLoadingGlossaryDetail } = useGlossary(
selectedGlossary?.id || null
);
const isPro = user?.tier === 'pro';
const isLoading = isLoadingUser || isLoadingGlossaries;
// Current translation target from store
const currentTargetLang = settings.defaultTargetLanguage;
const currentTargetInfo = SUPPORTED_LANGUAGES.find(l => l.code === currentTargetLang);
// Track whether prompt has unsaved changes
const promptHasUnsavedChanges = systemPrompt !== settings.systemPrompt;
const promptIsActive = !!settings.systemPrompt?.trim();
useEffect(() => {
setSystemPrompt(settings.systemPrompt);
setPromptSaved(false);
}, [settings.systemPrompt]);
const handleSavePrompt = async () => {
setIsSavingPrompt(true);
try {
updateSettings({ systemPrompt });
await new Promise(resolve => setTimeout(resolve, 300));
setPromptSaved(true);
toast({ title: t('context.saved'), description: t('context.savedDesc') });
setTimeout(() => setPromptSaved(false), 3000);
} finally { setIsSavingPrompt(false); }
};
const handleClearPrompt = () => {
updateSettings({ systemPrompt: '' });
setSystemPrompt('');
};
const handleCreatePresetGlossary = async (preset: typeof PRESETS[0]) => {
setCreatingPreset(preset.key);
try {
const token = localStorage.getItem('token');
if (!token) return;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
};
const params = new URLSearchParams({ template_id: preset.templateId });
const res = await fetch(`${API_BASE}/api/v1/glossaries/import?${params.toString()}`, {
method: 'POST',
headers,
});
if (res.ok) {
const result = await res.json();
const glossary = result.data;
toast({
title: t('context.presets.created'),
description: t('context.presets.createdDesc', {
name: glossary?.name ?? preset.title,
count: String(glossary?.terms?.length ?? 0),
}),
});
} else {
toast({ variant: 'destructive', title: t('glossaries.toast.error'), description: t('glossaries.toast.errorCreate') });
}
} catch {
toast({ variant: 'destructive', title: t('glossaries.toast.error'), description: t('glossaries.toast.errorImport') });
} finally {
setCreatingPreset(null);
}
};
const handleEditClick = (id: string) => {
const glossary = glossaries.find((g: GlossaryListItem) => g.id === id);
if (glossary) {
setSelectedGlossary(glossary);
setEditDialogOpen(true);
}
};
const handleDeleteClick = (id: string, name: string) => {
setGlossaryToDelete({ id, name });
setDeleteDialogOpen(true);
};
const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => {
try {
await createGlossary(data);
setCreateDialogOpen(false);
toast({
title: t('glossaries.toast.created'),
description: t('glossaries.toast.createdDesc', { name: data.name }),
});
} catch (error) {
toast({
variant: 'destructive',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorCreate'),
});
throw error;
}
};
const handleImportTemplate = async (templateId: string, name?: string) => {
try {
await importTemplate(templateId, name);
setCreateDialogOpen(false);
toast({
title: t('glossaries.toast.imported'),
description: name
? t('glossaries.toast.importedDesc', { name })
: t('glossaries.toast.importedDesc', { name: templateId }),
});
} catch (error) {
toast({
variant: 'destructive',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorImport'),
});
throw error;
}
};
const handleSaveGlossary = async (id: string, data: { name: string; terms: GlossaryTermInput[] }) => {
try {
await updateGlossary(id, data);
setEditDialogOpen(false);
setSelectedGlossary(null);
toast({
title: t('glossaries.toast.updated'),
description: t('glossaries.toast.updatedDesc', { name: data.name }),
});
} catch (error) {
toast({
variant: 'destructive',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorUpdate'),
});
throw error;
}
};
const handleDeleteConfirm = async () => {
if (!glossaryToDelete) return;
try {
await deleteGlossary(glossaryToDelete.id);
setDeleteDialogOpen(false);
setGlossaryToDelete(null);
toast({
title: t('glossaries.toast.deleted'),
description: t('glossaries.toast.deletedDesc'),
});
} catch (error) {
toast({
variant: 'destructive',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorDelete'),
});
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-muted border-t-brand-accent mx-auto"></div>
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light">Chargement...</p>
</div>
</div>
);
}
if (!isPro) {
return <ProUpgradePrompt />;
}
const renderTitle = (title: string) => {
const lastSpaceIndex = title.lastIndexOf(' ');
if (lastSpaceIndex === -1) return title;
const firstPart = title.substring(0, lastSpaceIndex);
const lastWord = title.substring(lastSpaceIndex + 1);
return (
<>
{firstPart} <span className="italic">{lastWord}</span>
</>
);
};
return (
<div className="max-w-6xl mx-auto w-full p-6 lg:p-8">
{/* ── Editorial Header ───────────────────────────────────── */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end mb-10 gap-6">
<div>
<span className="accent-pill mb-3 block w-fit font-medium text-[10px] uppercase tracking-widest">
{t('glossaries.yourGlossaries') || "Vos Glossaires"}
</span>
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
{renderTitle(t('glossaries.title') || "Glossaires & Contexte")}
</h1>
<p className="text-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
{t('glossaries.description') || "Gérez vos glossaires et instructions de contexte pour des traductions plus précises."}
</p>
</div>
<button
onClick={() => setCreateDialogOpen(true)}
disabled={isCreating}
className="premium-button px-8 py-3 text-xs uppercase tracking-widest !rounded-xl flex items-center gap-2 disabled:opacity-50 shrink-0 cursor-pointer font-bold"
>
<Plus size={14} />
{t('glossaries.createNew')}
</button>
</div>
{/* ── Comment ça marche ─────────────────────────────────── */}
<div className="mb-10 p-5 rounded-2xl bg-brand-accent/5 dark:bg-brand-accent/10 border border-brand-accent/20 dark:border-brand-accent/15">
<div className="flex items-center gap-2 mb-4">
<Info size={15} className="text-brand-accent shrink-0" />
<span className="text-xs font-bold uppercase tracking-widest text-brand-accent">Comment ces paramètres sont utilisés</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Step 1 */}
<div className="flex items-start gap-3">
<div className="w-7 h-7 rounded-full bg-brand-accent text-white flex items-center justify-center text-[11px] font-black shrink-0 mt-0.5">1</div>
<div>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">Configurez ici</p>
<p className="text-[11px] text-brand-dark/55 dark:text-white/50 font-light leading-relaxed">
Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.
</p>
</div>
</div>
{/* Arrow */}
<div className="hidden sm:flex items-center justify-center">
<ArrowRight size={18} className="text-brand-accent/40" />
</div>
{/* Step 2 */}
<div className="flex items-start gap-3">
<div className="w-7 h-7 rounded-full bg-brand-accent text-white flex items-center justify-center text-[11px] font-black shrink-0 mt-0.5">2</div>
<div>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">Activez dans Traduire</p>
<p className="text-[11px] text-brand-dark/55 dark:text-white/50 font-light leading-relaxed">
Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.
</p>
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-brand-accent/10 flex items-center justify-between">
<p className="text-[11px] text-brand-dark/45 dark:text-white/40 font-light">
Les instructions de contexte s'appliquent <strong>automatiquement</strong> à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être <strong>sélectionnés manuellement</strong> sur la page Traduire.
</p>
<Link
href="/dashboard/translate"
className="ml-4 shrink-0 flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline"
>
Aller à Traduire <ExternalLink size={11} />
</Link>
</div>
</div>
<div className="space-y-10">
{/* ── System Prompt ────────────────────────────────────── */}
<section className="editorial-card p-8 lg:p-10 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm">
{/* Header with status badge */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3 text-brand-accent">
<MessageSquare size={18} />
<h3 className="text-sm font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{t('context.instructions.title')}
</h3>
</div>
{/* Status badge */}
{promptHasUnsavedChanges ? (
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400 bg-amber-500/10 px-3 py-1 rounded-full border border-amber-500/20">
<AlertCircle size={11} />
Non enregistré
</span>
) : promptIsActive ? (
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-3 py-1 rounded-full border border-emerald-500/20">
<CheckCircle2 size={11} />
Actif · s'applique à toutes les traductions IA
</span>
) : (
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-brand-dark/30 dark:text-white/30 bg-brand-muted dark:bg-white/5 px-3 py-1 rounded-full border border-black/5 dark:border-white/5">
Inactif
</span>
)}
</div>
{/* Explanation box */}
<div className="mb-5 p-3.5 rounded-xl bg-brand-muted/40 dark:bg-white/[0.03] border border-black/5 dark:border-white/5">
<p className="text-[11px] text-brand-dark/60 dark:text-white/50 font-light leading-relaxed">
<strong className="font-bold text-brand-dark/80 dark:text-white/80">À quoi ça sert ?</strong> Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.
</p>
<p className="text-[11px] text-brand-accent/80 dark:text-brand-accent/70 font-medium mt-2 italic">
Exemple : «&nbsp;Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres.&nbsp;»
</p>
</div>
<textarea
value={systemPrompt}
onChange={e => { setSystemPrompt(e.target.value); setPromptSaved(false); }}
placeholder={t('context.instructions.placeholder')}
className="w-full h-40 p-4 bg-brand-muted/30 dark:bg-white/[0.02] rounded-xl border border-black/5 dark:border-white/10 text-xs focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30 transition-all outline-none resize-y"
/>
<div className="flex justify-between items-center mt-4">
<p className="text-[10px] text-brand-dark/30 dark:text-white/25 font-light">
{systemPrompt.length > 0 ? `${systemPrompt.length} caractères` : 'Vide aucune instruction envoyée à l\'IA'}
</p>
<div className="flex gap-3">
<button
onClick={handleClearPrompt}
className="px-5 py-2.5 bg-brand-muted dark:bg-white/5 text-brand-dark/50 dark:text-white/40 rounded-lg text-xs font-bold uppercase tracking-wider hover:text-brand-dark dark:hover:text-white transition-all cursor-pointer"
>
<Trash2 size={12} className="inline mr-1.5" />Tout effacer
</button>
<button
onClick={handleSavePrompt}
disabled={isSavingPrompt || !promptHasUnsavedChanges}
className="premium-button px-8 py-2.5 text-xs uppercase tracking-widest !rounded-lg flex items-center gap-2 disabled:opacity-50 cursor-pointer font-bold"
>
{isSavingPrompt
? <><Loader2 size={14} className="animate-spin" /> Enregistrement</>
: promptSaved
? <><CheckCircle2 size={14} /> Enregistré</>
: <><Save size={14} /> {t('context.save')}</>
}
</button>
</div>
</div>
</section>
{/* ── Professional Presets ─────────────────────────────── */}
<section className="editorial-card p-8 lg:p-10 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm">
<div className="flex items-center gap-3 mb-3 text-brand-accent">
<Zap size={18} />
<h3 className="text-sm font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{t('context.presets.title')}
</h3>
</div>
{/* Explanation box */}
<div className="mb-6 p-3.5 rounded-xl bg-brand-muted/40 dark:bg-white/[0.03] border border-black/5 dark:border-white/5">
<p className="text-[11px] text-brand-dark/60 dark:text-white/50 font-light leading-relaxed">
<strong className="font-bold text-brand-dark/80 dark:text-white/80">À quoi ça sert ?</strong> Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le <strong>sélectionner manuellement</strong> sur la page Traduire pour forcer des traductions de termes précis.
</p>
<div className="flex items-center gap-2 mt-2.5">
<MousePointerClick size={11} className="text-brand-accent shrink-0" />
<p className="text-[11px] text-brand-accent/80 font-medium">
Cliquez sur une carte glossaire créé sélectionnez-le dans Traduire
</p>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{PRESETS.map((p) => {
const Icon = p.icon;
const isCreatingThis = creatingPreset === p.key;
return (
<button
key={p.key}
onClick={() => handleCreatePresetGlossary(p)}
disabled={!!creatingPreset}
className="p-4 bg-brand-muted/40 dark:bg-white/5 hover:bg-brand-accent/5 dark:hover:bg-brand-accent/10 border border-black/5 dark:border-white/5 rounded-xl text-left transition-all hover:border-brand-accent/30 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed group min-h-[105px] flex flex-col justify-between"
>
<div className="flex items-center justify-between mb-2">
<div className="p-1.5 bg-brand-accent/10 rounded-lg text-brand-accent group-hover:scale-110 transition-transform">
{isCreatingThis ? <Loader2 size={16} className="animate-spin" /> : <Icon size={16} />}
</div>
{isCreatingThis && <span className="text-[10px] text-brand-accent font-bold uppercase">Création</span>}
</div>
<div>
<h4 className="text-xs font-bold text-brand-dark dark:text-white mb-1">
{p.title}
</h4>
<p className="text-[10px] text-brand-dark/45 dark:text-white/45 leading-normal font-light">
{p.desc}
</p>
</div>
</button>
);
})}
</div>
</section>
{/* ── Glossary Grid ──────────────────────────────────────── */}
<div>
<div className="flex items-center justify-between mb-5">
<div>
<h2 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight">
Vos <span className="italic">glossaires</span>
</h2>
<p className="text-[11px] text-brand-dark/45 dark:text-white/40 font-light mt-1">
{glossaries.length > 0
? `${glossaries.length} glossaire${glossaries.length > 1 ? 's' : ''} — cliquez sur une carte pour la modifier`
: 'Créez votre premier glossaire ou importez un preset ci-dessus'}
</p>
</div>
<div className="flex items-center gap-3">
{currentTargetInfo && (
<span className="flex items-center gap-1.5 text-[10px] font-bold text-brand-dark/50 dark:text-white/40 bg-brand-muted dark:bg-white/5 border border-black/5 dark:border-white/5 px-3 py-1.5 rounded-full">
<span>Traduction active :</span>
<span>{currentTargetInfo.flag} {currentTargetInfo.label}</span>
</span>
)}
{glossaries.length > 0 && (
<Link
href="/dashboard/translate"
className="flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline shrink-0"
>
<ExternalLink size={12} />
Aller à Traduire pour activer
</Link>
)}
</div>
</div>
{glossaries.length === 0 ? (
<div className="editorial-card p-12 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm text-center">
<div className="w-12 h-12 bg-brand-muted dark:bg-white/10 rounded-xl flex items-center justify-center text-brand-accent mx-auto mb-4">
<Library size={24} />
</div>
<p className="text-base font-serif font-medium text-brand-dark dark:text-white mb-1">{t('glossaries.empty')}</p>
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light">{t('glossaries.emptyDesc')}</p>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{glossaries.map((glossary: GlossaryListItem) => {
const termCount = glossary.terms_count ?? 0;
const srcInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language);
const tgtInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language);
const isMultilingual = glossary.target_language === 'multi';
// Does this glossary match the current translation target?
const matchesTarget = currentTargetLang && (isMultilingual || glossary.target_language === currentTargetLang);
const mismatch = currentTargetLang && !isMultilingual && glossary.target_language && glossary.target_language !== currentTargetLang;
return (
<div
key={glossary.id}
className={cn(
'editorial-card p-6 bg-white dark:bg-[#141414] border rounded-2xl shadow-sm group transition-all relative',
matchesTarget
? 'border-brand-accent/40 ring-1 ring-brand-accent/20 hover:border-brand-accent/60'
: mismatch
? 'border-amber-300/40 dark:border-amber-500/20 hover:border-amber-400/60 opacity-75 hover:opacity-100'
: 'border-black/5 dark:border-white/5 hover:border-brand-accent/30'
)}
>
{/* Match / mismatch badge */}
{matchesTarget && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-brand-accent/10 text-brand-accent px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider">
<CheckCircle2 size={9} /> Compatible
</div>
)}
{mismatch && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-amber-500/10 text-amber-600 dark:text-amber-400 px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider">
<AlertCircle size={9} /> Autre cible
</div>
)}
<div className="flex items-start gap-3 mb-5">
<div className="w-10 h-10 bg-brand-muted dark:bg-white/10 rounded-xl flex items-center justify-center text-brand-accent shrink-0">
<Library size={18} />
</div>
<div className="flex-1 min-w-0 pt-0.5">
<h3 className="text-sm font-serif font-semibold text-brand-dark dark:text-white tracking-tight leading-snug line-clamp-2">
{glossary.name}
</h3>
<p className="text-[11px] text-brand-dark/40 dark:text-white/40 font-medium flex items-center gap-1 mt-0.5">
<span>{srcInfo?.flag ?? '🌐'}</span>
<span>{srcInfo?.label ?? glossary.source_language}</span>
<span className="text-brand-accent font-bold"></span>
<span>{tgtInfo?.flag ?? '🌐'}</span>
<span>{tgtInfo?.label ?? glossary.target_language}</span>
</p>
</div>
</div>
<div className="flex justify-between items-center pt-4 border-t border-black/5 dark:border-white/10 text-xs text-brand-dark/40 dark:text-white/40">
<span className="flex items-center gap-1">
<Hash size={12} className="text-brand-accent" />
{termCount} {t('glossaries.defineTerms')}
</span>
<span className="flex items-center gap-1 font-mono">
<Calendar size={12} />
{new Date(glossary.created_at).toLocaleDateString()}
</span>
</div>
{/* Action buttons — always visible, unambiguous */}
<div className="flex gap-2 mt-4">
<button
onClick={() => handleEditClick(glossary.id)}
className="flex-1 py-2 px-3 rounded-lg bg-brand-muted/60 dark:bg-white/5 hover:bg-brand-accent/10 dark:hover:bg-brand-accent/15 text-brand-dark/70 dark:text-white/60 hover:text-brand-accent text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer flex items-center justify-center gap-1.5"
>
<Save size={10} /> Modifier les termes
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(glossary.id, glossary.name);
}}
className="py-2 px-3 rounded-lg bg-red-500/5 hover:bg-red-500 hover:text-white text-red-500 text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1.5"
>
<Trash2 size={10} /> Supprimer
</button>
</div>
</div>
);
})}
</div>
)}
</div>
{/* ── About section ──────────────────────────────────────── */}
<div className="editorial-card p-8 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm">
<div className="flex items-center gap-3 text-brand-accent mb-4">
<BookText size={18} />
<span className="text-xs font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{t('glossaries.aboutTitle')}
</span>
</div>
<p className="text-xs text-brand-dark/50 dark:text-white/40 font-light leading-relaxed mb-3">{t('glossaries.aboutDesc')}</p>
<p className="text-[10px] font-bold uppercase tracking-wider text-brand-dark/60 dark:text-white/60">
{t('glossaries.aboutFormat')}
</p>
</div>
</div>
{/* Dialogs */}
<CreateGlossaryDialog
open={createDialogOpen}
onOpenChange={setCreateDialogOpen}
onCreate={handleCreateGlossary}
onImportTemplate={handleImportTemplate}
isCreating={isCreating}
isImportingTemplate={isImportingTemplate}
/>
{editDialogOpen && (fullGlossary || !isLoadingGlossaryDetail) && (
<EditGlossaryDialog
open={editDialogOpen}
onOpenChange={(open) => {
setEditDialogOpen(open);
if (!open) setSelectedGlossary(null);
}}
glossary={fullGlossary}
onSave={handleSaveGlossary}
isSaving={isUpdating}
/>
)}
<DeleteGlossaryDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onConfirm={handleDeleteConfirm}
isDeleting={isDeleting}
glossaryName={glossaryToDelete?.name}
/>
</div>
);
}