All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m21s
750 lines
40 KiB
TypeScript
750 lines
40 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useRef } from 'react';
|
|
import {
|
|
BookText, Upload, PenLine, ArrowRight, ArrowLeft, CheckCircle2,
|
|
AlertCircle, Loader2, Scale, Cpu, TrendingUp, HeartPulse,
|
|
Megaphone, Users, FlaskConical, ShoppingCart, X, Plus
|
|
} from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import { cn } from '@/lib/utils';
|
|
import { useI18n } from '@/lib/i18n';
|
|
import { useGlossaries, useGlossaryTemplates } from '../useGlossaries';
|
|
import { SUPPORTED_LANGUAGES } from '../types';
|
|
import type { GlossaryTermInput } from '../types';
|
|
import { parseFileToTerms } from '../csvUtils';
|
|
import { useToast } from '@/components/ui/toast';
|
|
|
|
// ── Icônes par catégorie de modèle ─────────────────────────────────────────
|
|
const TEMPLATE_ICONS: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
|
|
legal: Scale,
|
|
technology: Cpu,
|
|
finance: TrendingUp,
|
|
medical: HeartPulse,
|
|
marketing: Megaphone,
|
|
hr: Users,
|
|
scientific: FlaskConical,
|
|
ecommerce: ShoppingCart,
|
|
};
|
|
|
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
type CreationMethod = 'template' | 'file' | 'manual' | null;
|
|
type Step = 1 | 2;
|
|
|
|
export default function NewGlossaryPage() {
|
|
const { t } = useI18n();
|
|
const router = useRouter();
|
|
const { toast } = useToast();
|
|
const { createGlossary, importTemplate, isCreating, isImportingTemplate } = useGlossaries();
|
|
const { templates, isLoading: isLoadingTemplates } = useGlossaryTemplates();
|
|
|
|
const [step, setStep] = useState<Step>(1);
|
|
const [method, setMethod] = useState<CreationMethod>(null);
|
|
|
|
// État pour modèle
|
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
|
|
const [isImportingThis, setIsImportingThis] = useState(false);
|
|
|
|
// État pour fichier
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const [fileStatus, setFileStatus] = useState<'idle' | 'parsing' | 'success' | 'error'>('idle');
|
|
const [fileError, setFileError] = useState('');
|
|
const [parsedTerms, setParsedTerms] = useState<GlossaryTermInput[]>([]);
|
|
const [parsedFileName, setParsedFileName] = useState('');
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [fileWizardStep, setFileWizardStep] = useState<1 | 2 | 3>(1);
|
|
const [fileSrcLang, setFileSrcLang] = useState('fr');
|
|
const [fileTgtLang, setFileTgtLang] = useState('multi');
|
|
|
|
// État pour manuel
|
|
const [manualName, setManualName] = useState('');
|
|
const [manualSrc, setManualSrc] = useState('fr');
|
|
const [manualTgt, setManualTgt] = useState('multi');
|
|
|
|
const isProcessing = isCreating || isImportingTemplate || isImportingThis;
|
|
|
|
// ── Handlers ────────────────────────────────────────────────────────────
|
|
|
|
const handleSelectMethod = (m: CreationMethod) => {
|
|
setMethod(m);
|
|
setStep(2);
|
|
// reset
|
|
setSelectedTemplateId(null);
|
|
setFileStatus('idle');
|
|
setParsedTerms([]);
|
|
setParsedFileName('');
|
|
setManualName('');
|
|
setFileWizardStep(1);
|
|
setFileSrcLang('fr');
|
|
setFileTgtLang('multi');
|
|
};
|
|
|
|
const handleBack = () => {
|
|
setStep(1);
|
|
setMethod(null);
|
|
};
|
|
|
|
// Importer un modèle
|
|
const handleImportTemplate = async () => {
|
|
if (!selectedTemplateId) return;
|
|
const tpl = templates.find(t => t.id === selectedTemplateId);
|
|
setIsImportingThis(true);
|
|
try {
|
|
await importTemplate(selectedTemplateId, tpl?.name);
|
|
toast({ title: 'Glossaire importé !', description: `Le modèle « ${tpl?.name} » a été ajouté à vos glossaires.` });
|
|
router.push('/dashboard/glossaries');
|
|
} catch {
|
|
toast({ variant: 'destructive', title: 'Erreur', description: 'Impossible d\'importer ce modèle.' });
|
|
} finally {
|
|
setIsImportingThis(false);
|
|
}
|
|
};
|
|
|
|
// Parser un fichier CSV
|
|
const processFile = async (file: File) => {
|
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
if (!ext || !['csv', 'xlsx', 'xls', 'ods', 'txt', 'tsv'].includes(ext)) {
|
|
setFileStatus('error');
|
|
setFileError('Format non supporté. Utilisez CSV, Excel (.xlsx), ODS, ou TSV.');
|
|
return;
|
|
}
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
setFileStatus('error');
|
|
setFileError('Fichier trop volumineux (maximum 5 MB).');
|
|
return;
|
|
}
|
|
setFileStatus('parsing');
|
|
setFileError('');
|
|
try {
|
|
const terms = await parseFileToTerms(file);
|
|
if (terms.length === 0) {
|
|
setFileStatus('error');
|
|
setFileError('Le fichier est vide ou n\'a pas pu être lu.');
|
|
return;
|
|
}
|
|
setFileStatus('success');
|
|
setParsedTerms(terms);
|
|
setParsedFileName(file.name.replace(/\.[^.]+$/, '').replace(/[_-]/g, ' '));
|
|
} catch {
|
|
setFileStatus('error');
|
|
setFileError('Erreur lors de la lecture du fichier.');
|
|
}
|
|
};
|
|
|
|
// Télécharger exemple CSV
|
|
const handleDownloadSample = () => {
|
|
const sample = `source,target\nbénéfice,profit\nflux de trésorerie,cash flow\nbilan,balance sheet\ncompte de résultat,income statement\nrésiliation,termination`;
|
|
const blob = new Blob([sample], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'exemple_glossaire.csv';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
// Créer depuis fichier
|
|
const handleCreateFromFile = async () => {
|
|
if (!parsedTerms.length) return;
|
|
try {
|
|
await createGlossary({
|
|
name: parsedFileName || 'Mon glossaire',
|
|
source_language: fileSrcLang,
|
|
target_language: fileTgtLang,
|
|
terms: parsedTerms,
|
|
});
|
|
toast({ title: 'Glossaire créé !', description: `${parsedTerms.length} termes importés depuis votre fichier.` });
|
|
router.push('/dashboard/glossaries');
|
|
} catch {
|
|
toast({ variant: 'destructive', title: 'Erreur', description: 'Impossible de créer le glossaire.' });
|
|
}
|
|
};
|
|
|
|
// Créer manuellement
|
|
const handleCreateManual = async () => {
|
|
if (!manualName.trim()) return;
|
|
try {
|
|
await createGlossary({ name: manualName.trim(), source_language: manualSrc, target_language: manualTgt, terms: [] });
|
|
toast({ title: 'Glossaire créé !', description: `« ${manualName} » est prêt. Ajoutez vos premiers termes.` });
|
|
router.push('/dashboard/glossaries');
|
|
} catch {
|
|
toast({ variant: 'destructive', title: 'Erreur', description: 'Impossible de créer le glossaire.' });
|
|
}
|
|
};
|
|
|
|
// ── Rendu ───────────────────────────────────────────────────────────────
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto w-full p-6 lg:p-8">
|
|
|
|
{/* ── Fil d'Ariane ──────────────────────────────────────────── */}
|
|
<div className="flex items-center gap-2 mb-8 text-[11px] font-medium text-[#555555] dark:text-white/40">
|
|
<Link href="/dashboard/glossaries" className="hover:text-[#1A1A1A] dark:hover:text-white transition-colors">
|
|
Mes glossaires
|
|
</Link>
|
|
<ArrowRight size={11} />
|
|
<span className="text-[#1A1A1A] dark:text-white font-bold">Nouveau glossaire</span>
|
|
</div>
|
|
|
|
{/* ── Indicateur d'étapes ────────────────────────────────────── */}
|
|
<div className="flex items-center gap-4 mb-10">
|
|
{/* Étape 1 */}
|
|
<div className="flex items-center gap-2">
|
|
<div className={cn(
|
|
'w-7 h-7 rounded-full flex items-center justify-center text-[11px] font-black transition-all',
|
|
step >= 1 ? 'bg-[#1A1A1A] text-white' : 'bg-[#EBEBEB] text-[#888888]'
|
|
)}>
|
|
{step > 1 ? <CheckCircle2 size={14} /> : '1'}
|
|
</div>
|
|
<span className={cn('text-xs font-bold uppercase tracking-wider', step === 1 ? 'text-[#1A1A1A] dark:text-white' : 'text-[#888888]')}>
|
|
Choisir le type
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex-1 h-px bg-[#E5E3DF] dark:bg-white/10" />
|
|
|
|
{/* Étape 2 */}
|
|
<div className="flex items-center gap-2">
|
|
<div className={cn(
|
|
'w-7 h-7 rounded-full flex items-center justify-center text-[11px] font-black transition-all',
|
|
step === 2 ? 'bg-[#1A1A1A] text-white' : 'bg-[#EBEBEB] text-[#888888]'
|
|
)}>
|
|
2
|
|
</div>
|
|
<span className={cn('text-xs font-bold uppercase tracking-wider', step === 2 ? 'text-[#1A1A1A] dark:text-white' : 'text-[#888888]')}>
|
|
Configurer
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ══════════════ ÉTAPE 1 : Choisir le type ══════════════════ */}
|
|
{step === 1 && (
|
|
<div className="animate-fade-in">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-serif font-medium text-[#1A1A1A] dark:text-white tracking-tight mb-2">
|
|
Comment voulez-vous créer votre{' '}
|
|
<span className="italic">glossaire ?</span>
|
|
</h1>
|
|
<p className="text-[#555555] dark:text-white/50 text-sm font-light">
|
|
Choisissez la méthode qui correspond à votre situation.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
|
|
|
{/* Option 1 — Modèle professionnel */}
|
|
<button
|
|
onClick={() => handleSelectMethod('template')}
|
|
className="group text-left p-7 bg-white dark:bg-[#141414] border-2 border-[#E5E3DF] dark:border-white/10 rounded-2xl hover:border-[#C5A17A] dark:hover:border-brand-accent/50 hover:shadow-lg transition-all cursor-pointer"
|
|
>
|
|
<div className="w-12 h-12 rounded-xl bg-[#F0EDE8] dark:bg-brand-accent/10 flex items-center justify-center text-[#8B6F47] mb-5 group-hover:scale-110 transition-transform">
|
|
<BookText size={24} />
|
|
</div>
|
|
<h2 className="text-base font-serif font-bold text-[#1A1A1A] dark:text-white mb-2 tracking-tight">
|
|
Depuis un modèle
|
|
</h2>
|
|
<p className="text-[12px] text-[#555555] dark:text-white/50 font-light leading-relaxed">
|
|
Choisissez parmi nos modèles professionnels pré-remplis : RH, Médical, Juridique, Tech…
|
|
</p>
|
|
<div className="mt-5 flex items-center gap-1.5 text-[11px] font-bold text-[#8B6F47] dark:text-brand-accent uppercase tracking-wider">
|
|
Choisir un modèle <ArrowRight size={12} />
|
|
</div>
|
|
</button>
|
|
|
|
{/* Option 2 — Fichier CSV */}
|
|
<button
|
|
onClick={() => handleSelectMethod('file')}
|
|
className="group text-left p-7 bg-white dark:bg-[#141414] border-2 border-[#E5E3DF] dark:border-white/10 rounded-2xl hover:border-[#C5A17A] dark:hover:border-brand-accent/50 hover:shadow-lg transition-all cursor-pointer"
|
|
>
|
|
<div className="w-12 h-12 rounded-xl bg-[#F0EDE8] dark:bg-brand-accent/10 flex items-center justify-center text-[#8B6F47] mb-5 group-hover:scale-110 transition-transform">
|
|
<Upload size={24} />
|
|
</div>
|
|
<h2 className="text-base font-serif font-bold text-[#1A1A1A] dark:text-white mb-2 tracking-tight">
|
|
Importer un fichier
|
|
</h2>
|
|
<p className="text-[12px] text-[#555555] dark:text-white/50 font-light leading-relaxed">
|
|
Importez vos propres termes depuis un fichier CSV, Excel (.xlsx) ou ODS existant.
|
|
</p>
|
|
<div className="mt-5 flex items-center gap-1.5 text-[11px] font-bold text-[#8B6F47] dark:text-brand-accent uppercase tracking-wider">
|
|
Importer un fichier <ArrowRight size={12} />
|
|
</div>
|
|
</button>
|
|
|
|
{/* Option 3 — Manuel */}
|
|
<button
|
|
onClick={() => handleSelectMethod('manual')}
|
|
className="group text-left p-7 bg-white dark:bg-[#141414] border-2 border-[#E5E3DF] dark:border-white/10 rounded-2xl hover:border-[#C5A17A] dark:hover:border-brand-accent/50 hover:shadow-lg transition-all cursor-pointer"
|
|
>
|
|
<div className="w-12 h-12 rounded-xl bg-[#F0EDE8] dark:bg-brand-accent/10 flex items-center justify-center text-[#8B6F47] mb-5 group-hover:scale-110 transition-transform">
|
|
<PenLine size={24} />
|
|
</div>
|
|
<h2 className="text-base font-serif font-bold text-[#1A1A1A] dark:text-white mb-2 tracking-tight">
|
|
Créer manuellement
|
|
</h2>
|
|
<p className="text-[12px] text-[#555555] dark:text-white/50 font-light leading-relaxed">
|
|
Partez de zéro et ajoutez vos termes un par un directement dans l'éditeur.
|
|
</p>
|
|
<div className="mt-5 flex items-center gap-1.5 text-[11px] font-bold text-[#8B6F47] dark:text-brand-accent uppercase tracking-wider">
|
|
Créer vide <ArrowRight size={12} />
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ══════════════ ÉTAPE 2 : Configurer ═══════════════════════ */}
|
|
{step === 2 && (
|
|
<div className="animate-fade-in">
|
|
|
|
{/* Bouton retour */}
|
|
<button
|
|
onClick={handleBack}
|
|
className="flex items-center gap-2 text-[11px] font-bold uppercase tracking-wider text-[#555555] hover:text-[#1A1A1A] dark:hover:text-white transition-colors mb-8 cursor-pointer"
|
|
>
|
|
<ArrowLeft size={14} /> Retour
|
|
</button>
|
|
|
|
{/* ── CAS A : Depuis un modèle ─── */}
|
|
{method === 'template' && (
|
|
<div>
|
|
<h1 className="text-2xl font-serif font-medium text-[#1A1A1A] dark:text-white mb-2">
|
|
Choisissez un <span className="italic">modèle professionnel</span>
|
|
</h1>
|
|
<p className="text-[#555555] dark:text-white/50 text-sm font-light mb-8">
|
|
Sélectionnez le modèle le plus adapté à vos documents. Vous pourrez modifier les termes après l'import.
|
|
</p>
|
|
|
|
{isLoadingTemplates ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="size-8 animate-spin text-[#C5A17A]" />
|
|
</div>
|
|
) : templates.length === 0 ? (
|
|
<div className="p-12 text-center bg-white dark:bg-[#141414] border border-[#E5E3DF] dark:border-white/5 rounded-2xl">
|
|
<p className="text-sm text-[#555555] dark:text-white/40">
|
|
Tous les modèles disponibles ont déjà été importés.
|
|
</p>
|
|
<button onClick={handleBack} className="mt-4 text-[11px] font-bold text-[#7A5C35] hover:underline cursor-pointer">
|
|
← Choisir une autre méthode
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
|
{templates.map((tpl) => {
|
|
const Icon = TEMPLATE_ICONS[tpl.id] || BookText;
|
|
const isSelected = selectedTemplateId === tpl.id;
|
|
return (
|
|
<button
|
|
key={tpl.id}
|
|
onClick={() => setSelectedTemplateId(isSelected ? null : tpl.id)}
|
|
className={cn(
|
|
'text-left p-5 rounded-xl border-2 transition-all cursor-pointer',
|
|
isSelected
|
|
? 'border-[#1A1A1A] dark:border-white bg-[#F5F3EF] dark:bg-white/5 shadow-md'
|
|
: 'border-[#E5E3DF] dark:border-white/10 bg-white dark:bg-[#141414] hover:border-[#C5A17A] dark:hover:border-brand-accent/40'
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
<div className={cn(
|
|
'w-10 h-10 rounded-xl flex items-center justify-center shrink-0',
|
|
isSelected ? 'bg-[#1A1A1A] text-white' : 'bg-[#F0EDE8] dark:bg-brand-accent/10 text-[#8B6F47]'
|
|
)}>
|
|
<Icon size={18} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<p className="text-sm font-serif font-bold text-[#1A1A1A] dark:text-white leading-tight">
|
|
{tpl.name.split(' - ')[0]}
|
|
</p>
|
|
<span className="shrink-0 text-[9px] font-mono font-semibold text-[#666666] dark:text-white/40">
|
|
{tpl.terms_count} termes
|
|
</span>
|
|
</div>
|
|
<p className="text-[11px] text-[#555555] dark:text-white/40 font-light mt-1 line-clamp-2">
|
|
{tpl.description}
|
|
</p>
|
|
</div>
|
|
{isSelected && (
|
|
<CheckCircle2 size={18} className="text-[#1A1A1A] dark:text-white shrink-0 mt-0.5" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Bouton d'import */}
|
|
<div className="flex items-center justify-end gap-4">
|
|
{selectedTemplateId && (
|
|
<p className="text-[11px] text-[#555555] dark:text-white/40 font-light">
|
|
Modèle sélectionné : <strong className="text-[#1A1A1A] dark:text-white">{templates.find(t => t.id === selectedTemplateId)?.name.split(' - ')[0]}</strong>
|
|
</p>
|
|
)}
|
|
<button
|
|
onClick={handleImportTemplate}
|
|
disabled={!selectedTemplateId || isProcessing}
|
|
className="flex items-center gap-2 px-7 py-3 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
>
|
|
{isImportingThis ? <><Loader2 size={14} className="animate-spin" /> Import en cours…</> : <><CheckCircle2 size={14} /> Importer ce modèle</>}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── CAS B : Depuis un fichier — Wizard 3 sous-étapes ─── */}
|
|
{method === 'file' && (
|
|
<div>
|
|
<h1 className="text-2xl font-serif font-medium text-[#1A1A1A] dark:text-white mb-2">
|
|
Importez votre <span className="italic">fichier de termes</span>
|
|
</h1>
|
|
|
|
{/* Indicateur sous-étapes */}
|
|
<div className="flex items-center gap-2 mb-8">
|
|
{([1, 2, 3] as const).map((s, i) => {
|
|
const labels = ['Format', 'Fichier', 'Configurer'];
|
|
const done = fileWizardStep > s;
|
|
const active = fileWizardStep === s;
|
|
return (
|
|
<>
|
|
<div key={s} className="flex items-center gap-1.5">
|
|
<div className={cn(
|
|
'w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-black transition-all',
|
|
done ? 'bg-emerald-600 text-white' : active ? 'bg-[#1A1A1A] text-white' : 'bg-[#EBEBEB] dark:bg-white/10 text-[#888]'
|
|
)}>
|
|
{done ? <CheckCircle2 size={12} /> : s}
|
|
</div>
|
|
<span className={cn('text-[10px] font-bold uppercase tracking-wider hidden sm:block',
|
|
active ? 'text-[#1A1A1A] dark:text-white' : 'text-[#AAAAAA]'
|
|
)}>{labels[i]}</span>
|
|
</div>
|
|
{i < 2 && <div className="flex-1 h-px bg-[#E5E3DF] dark:bg-white/10" />}
|
|
</>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* ── Sous-étape 1 : Format ─── */}
|
|
{fileWizardStep === 1 && (
|
|
<div className="space-y-5">
|
|
<div className="bg-white dark:bg-[#141414] border border-[#E5E3DF] dark:border-white/5 rounded-2xl p-6 space-y-5">
|
|
|
|
<div>
|
|
<p className="text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-3">Format attendu</p>
|
|
<p className="text-[12px] text-[#555555] dark:text-white/50 font-light mb-4 leading-relaxed">
|
|
Votre fichier doit avoir <strong className="text-[#1A1A1A] dark:text-white">2 colonnes</strong> : la première pour les termes source, la seconde pour les termes cibles.
|
|
La première ligne peut être un en-tête (elle sera ignorée automatiquement).
|
|
</p>
|
|
|
|
{/* Tableau exemple */}
|
|
<div className="rounded-xl overflow-hidden border border-[#E5E3DF] dark:border-white/10 text-[11px] font-mono">
|
|
<div className="grid grid-cols-2 bg-[#F5F3EF] dark:bg-white/5 border-b border-[#E5E3DF] dark:border-white/10">
|
|
<div className="px-4 py-2 font-bold text-[#8B6F47] uppercase tracking-wider">source</div>
|
|
<div className="px-4 py-2 font-bold text-[#8B6F47] uppercase tracking-wider border-l border-[#E5E3DF] dark:border-white/10">target</div>
|
|
</div>
|
|
{[['bénéfice', 'profit'], ['flux de trésorerie', 'cash flow'], ['bilan', 'balance sheet'], ['résiliation', 'termination']].map(([s, t]) => (
|
|
<div key={s} className="grid grid-cols-2 border-b border-[#E5E3DF] dark:border-white/5 last:border-0 hover:bg-[#FAFAF8] dark:hover:bg-white/[0.02]">
|
|
<div className="px-4 py-2 text-[#1A1A1A] dark:text-white">{s}</div>
|
|
<div className="px-4 py-2 text-[#555555] dark:text-white/60 border-l border-[#E5E3DF] dark:border-white/10">{t}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Formats acceptés */}
|
|
<div className="flex flex-wrap gap-2">
|
|
{['CSV (.csv)', 'Excel (.xlsx)', 'Excel (.xls)', 'ODS (.ods)', 'TSV (.tsv)', 'Texte (.txt)'].map(f => (
|
|
<span key={f} className="px-2.5 py-1 rounded-lg bg-[#F0EDE8] dark:bg-white/5 text-[10px] font-bold text-[#8B6F47] dark:text-brand-accent uppercase tracking-wider">{f}</span>
|
|
))}
|
|
</div>
|
|
|
|
{/* Règles */}
|
|
<div className="space-y-2">
|
|
{[
|
|
'Séparateur : virgule (CSV) ou tabulation (TSV)',
|
|
'Encodage recommandé : UTF-8',
|
|
'Taille maximale : 5 MB',
|
|
'Limite : 500 termes par glossaire',
|
|
'Les champs contenant des virgules doivent être entre guillemets',
|
|
].map(rule => (
|
|
<div key={rule} className="flex items-start gap-2 text-[11px] text-[#555555] dark:text-white/50">
|
|
<CheckCircle2 size={12} className="text-emerald-500 mt-0.5 shrink-0" />
|
|
{rule}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Télécharger exemple */}
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
onClick={handleDownloadSample}
|
|
className="flex items-center gap-2 text-[11px] font-bold text-[#8B6F47] dark:text-brand-accent hover:underline cursor-pointer"
|
|
>
|
|
<Upload size={12} /> Télécharger un exemple CSV
|
|
</button>
|
|
<button
|
|
onClick={() => setFileWizardStep(2)}
|
|
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all cursor-pointer"
|
|
>
|
|
Suivant <ArrowRight size={13} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Sous-étape 2 : Fichier ─── */}
|
|
{fileWizardStep === 2 && (
|
|
<div className="space-y-5">
|
|
{/* Zone de dépôt */}
|
|
<div
|
|
onDragOver={e => { e.preventDefault(); setIsDragging(true); }}
|
|
onDragLeave={() => setIsDragging(false)}
|
|
onDrop={e => {
|
|
e.preventDefault(); setIsDragging(false);
|
|
const f = e.dataTransfer.files[0];
|
|
if (f) processFile(f);
|
|
}}
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className={cn(
|
|
'flex flex-col items-center justify-center gap-4 p-12 rounded-2xl border-2 border-dashed cursor-pointer transition-all min-h-[220px]',
|
|
isDragging ? 'border-[#C5A17A] bg-[#F5F0EA]' : 'border-[#D9D6D0] dark:border-white/10 hover:border-[#C5A17A] hover:bg-[#FAFAF8] dark:hover:bg-white/[0.02]',
|
|
fileStatus === 'error' && 'border-red-400 bg-red-50 dark:bg-red-500/5'
|
|
)}
|
|
>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".csv,.xlsx,.xls,.ods,.txt,.tsv"
|
|
className="hidden"
|
|
onChange={e => { const f = e.target.files?.[0]; if (f) processFile(f); e.target.value = ''; }}
|
|
/>
|
|
{fileStatus === 'parsing' && (
|
|
<><Loader2 size={32} className="animate-spin text-[#C5A17A]" /><p className="text-sm font-medium text-[#555555] dark:text-white/50">Lecture du fichier…</p></>
|
|
)}
|
|
{fileStatus === 'success' && (
|
|
<>
|
|
<CheckCircle2 size={36} className="text-emerald-600" />
|
|
<div className="text-center">
|
|
<p className="text-sm font-bold text-emerald-700 dark:text-emerald-400">{parsedTerms.length} termes détectés</p>
|
|
<p className="text-[11px] text-[#555555] dark:text-white/40 mt-1">Cliquez pour changer de fichier</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
{fileStatus === 'error' && (
|
|
<>
|
|
<AlertCircle size={36} className="text-red-500" />
|
|
<div className="text-center">
|
|
<p className="text-sm font-medium text-red-600">{fileError}</p>
|
|
<p className="text-[11px] text-[#555555] dark:text-white/40 mt-1">Cliquez pour réessayer</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
{fileStatus === 'idle' && (
|
|
<>
|
|
<div className="w-16 h-16 rounded-2xl bg-[#F0EDE8] dark:bg-white/5 flex items-center justify-center text-[#8B6F47]">
|
|
<Upload size={32} />
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-sm font-serif font-bold text-[#1A1A1A] dark:text-white">Glissez votre fichier ici</p>
|
|
<p className="text-[11px] text-[#555555] dark:text-white/40 mt-1">CSV, Excel, ODS, TSV — max 5 MB</p>
|
|
</div>
|
|
<span className="text-[10px] text-[#AAAAAA] font-light">ou cliquez pour parcourir</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<button onClick={() => setFileWizardStep(1)} className="flex items-center gap-2 text-[11px] font-bold uppercase tracking-wider text-[#555555] hover:text-[#1A1A1A] dark:hover:text-white transition-colors cursor-pointer">
|
|
<ArrowLeft size={13} /> Retour
|
|
</button>
|
|
<button
|
|
onClick={() => setFileWizardStep(3)}
|
|
disabled={fileStatus !== 'success'}
|
|
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
>
|
|
Suivant <ArrowRight size={13} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Sous-étape 3 : Configurer ─── */}
|
|
{fileWizardStep === 3 && (
|
|
<div className="space-y-5">
|
|
<div className="bg-white dark:bg-[#141414] border border-[#E5E3DF] dark:border-white/5 rounded-2xl p-6 space-y-6">
|
|
|
|
{/* Résumé fichier */}
|
|
<div className="flex items-center gap-3 p-3 rounded-xl bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20">
|
|
<CheckCircle2 size={16} className="text-emerald-600 shrink-0" />
|
|
<div>
|
|
<p className="text-[11px] font-bold text-emerald-800 dark:text-emerald-400">{parsedTerms.length} termes prêts à importer</p>
|
|
<button onClick={() => { setFileWizardStep(2); setFileStatus('idle'); setParsedTerms([]); }} className="text-[10px] text-emerald-700 dark:text-emerald-500 hover:underline cursor-pointer">
|
|
Changer de fichier
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Nom */}
|
|
<div>
|
|
<label className="block text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-2">
|
|
Nom du glossaire <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={parsedFileName}
|
|
onChange={e => setParsedFileName(e.target.value)}
|
|
placeholder="Ex : Termes RH internes, Glossaire juridique…"
|
|
className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-[#FAFAF8] dark:bg-[#1A1A1A] text-[#1A1A1A] dark:text-white text-sm placeholder:text-[#AAAAAA] dark:placeholder:text-white/25 focus:outline-none focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50"
|
|
/>
|
|
</div>
|
|
|
|
{/* Langues */}
|
|
<div className="grid grid-cols-2 gap-5">
|
|
<div>
|
|
<label className="block text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-2">
|
|
Langue source
|
|
</label>
|
|
<select
|
|
value={fileSrcLang}
|
|
onChange={e => setFileSrcLang(e.target.value)}
|
|
className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-[#FAFAF8] dark:bg-[#1A1A1A] text-[#1A1A1A] dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent/30"
|
|
>
|
|
{SUPPORTED_LANGUAGES.filter(l => l.code !== 'multi').map(l => (
|
|
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
|
|
))}
|
|
</select>
|
|
<p className="text-[10px] text-[#AAAAAA] dark:text-white/30 mt-1.5 font-light">Langue de la 1ère colonne de votre fichier</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-2">
|
|
Langue cible
|
|
</label>
|
|
<select
|
|
value={fileTgtLang}
|
|
onChange={e => setFileTgtLang(e.target.value)}
|
|
className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-[#FAFAF8] dark:bg-[#1A1A1A] text-[#1A1A1A] dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent/30"
|
|
>
|
|
{SUPPORTED_LANGUAGES.map(l => (
|
|
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
|
|
))}
|
|
</select>
|
|
<p className="text-[10px] text-[#AAAAAA] dark:text-white/30 mt-1.5 font-light">Langue de la 2ème colonne de votre fichier</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="p-3.5 rounded-xl bg-[#F5F3EF] dark:bg-white/[0.02] border border-[#D9D6D0] dark:border-white/5">
|
|
<p className="text-[11px] text-[#555555] dark:text-white/50 font-light leading-relaxed">
|
|
<strong className="font-bold text-[#1A1A1A] dark:text-white/80">Conseil :</strong>{' '}
|
|
Choisissez <strong>Multilingue</strong> comme langue cible si vous prévoyez d'utiliser ce glossaire pour traduire vers plusieurs langues. Vous pourrez ajouter des traductions dans l'éditeur.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<button onClick={() => setFileWizardStep(2)} className="flex items-center gap-2 text-[11px] font-bold uppercase tracking-wider text-[#555555] hover:text-[#1A1A1A] dark:hover:text-white transition-colors cursor-pointer">
|
|
<ArrowLeft size={13} /> Retour
|
|
</button>
|
|
<button
|
|
onClick={handleCreateFromFile}
|
|
disabled={!parsedFileName.trim() || isProcessing}
|
|
className="flex items-center gap-2 px-7 py-3 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
>
|
|
{isCreating ? <><Loader2 size={14} className="animate-spin" /> Création…</> : <><CheckCircle2 size={14} /> Créer le glossaire</>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── CAS C : Manuel ─── */}
|
|
{method === 'manual' && (
|
|
<div>
|
|
<h1 className="text-2xl font-serif font-medium text-[#1A1A1A] dark:text-white mb-2">
|
|
Créez votre glossaire <span className="italic">manuellement</span>
|
|
</h1>
|
|
<p className="text-[#555555] dark:text-white/50 text-sm font-light mb-8">
|
|
Donnez un nom à votre glossaire et choisissez les langues. Vous ajouterez les termes dans l'éditeur.
|
|
</p>
|
|
|
|
<div className="bg-white dark:bg-[#141414] border border-[#E5E3DF] dark:border-white/5 rounded-2xl p-8 space-y-6">
|
|
|
|
{/* Nom */}
|
|
<div>
|
|
<label className="block text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-2">
|
|
Nom du glossaire <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={manualName}
|
|
onChange={e => setManualName(e.target.value)}
|
|
placeholder="Ex : Termes juridiques internes, Glossaire RH…"
|
|
className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-[#FAFAF8] dark:bg-[#1A1A1A] text-[#1A1A1A] dark:text-white text-sm placeholder:text-[#AAAAAA] dark:placeholder:text-white/25 focus:outline-none focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50"
|
|
/>
|
|
</div>
|
|
|
|
{/* Langues */}
|
|
<div className="grid grid-cols-2 gap-5">
|
|
<div>
|
|
<label className="block text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-2">
|
|
Langue source
|
|
</label>
|
|
<select
|
|
value={manualSrc}
|
|
onChange={e => setManualSrc(e.target.value)}
|
|
className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-[#FAFAF8] dark:bg-[#1A1A1A] text-[#1A1A1A] dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent/30"
|
|
>
|
|
{SUPPORTED_LANGUAGES.filter(l => l.code !== 'multi').map(l => (
|
|
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-2">
|
|
Langue cible
|
|
</label>
|
|
<select
|
|
value={manualTgt}
|
|
onChange={e => setManualTgt(e.target.value)}
|
|
className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-[#FAFAF8] dark:bg-[#1A1A1A] text-[#1A1A1A] dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent/30"
|
|
>
|
|
{SUPPORTED_LANGUAGES.map(l => (
|
|
<option key={l.code} value={l.code}>{l.flag} {l.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="p-3.5 rounded-xl bg-[#F5F3EF] dark:bg-white/[0.02] border border-[#D9D6D0] dark:border-white/5">
|
|
<p className="text-[11px] text-[#555555] dark:text-white/50 font-light leading-relaxed">
|
|
<strong className="font-bold text-[#1A1A1A] dark:text-white/80">Que se passe-t-il ensuite ?</strong>{' '}
|
|
Un glossaire vide sera créé. Vous serez redirigé vers l'éditeur pour ajouter vos termes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end mt-6">
|
|
<button
|
|
onClick={handleCreateManual}
|
|
disabled={!manualName.trim() || isProcessing}
|
|
className="flex items-center gap-2 px-7 py-3 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
>
|
|
{isCreating ? <><Loader2 size={14} className="animate-spin" /> Création…</> : <><Plus size={14} /> Créer le glossaire</>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|