feat(glossaries): option C - page management + wizard /new 2-step creation
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m6s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m6s
This commit is contained in:
565
frontend/src/app/dashboard/glossaries/new/page.tsx
Normal file
565
frontend/src/app/dashboard/glossaries/new/page.tsx
Normal file
@@ -0,0 +1,565 @@
|
||||
'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);
|
||||
|
||||
// É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('');
|
||||
};
|
||||
|
||||
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.');
|
||||
}
|
||||
};
|
||||
|
||||
// Créer depuis fichier
|
||||
const handleCreateFromFile = async () => {
|
||||
if (!parsedTerms.length) return;
|
||||
try {
|
||||
await createGlossary({ name: parsedFileName || 'Mon glossaire', source_language: 'fr', target_language: 'multi', 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 ─── */}
|
||||
{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>
|
||||
<p className="text-[#555555] dark:text-white/50 text-sm font-light mb-8">
|
||||
Formats acceptés : CSV, Excel (.xlsx), ODS, TSV — maximum 5 MB.
|
||||
</p>
|
||||
|
||||
{/* Format attendu */}
|
||||
<div className="mb-6 p-4 rounded-xl bg-[#F5F3EF] dark:bg-white/[0.02] border border-[#D9D6D0] dark:border-white/5">
|
||||
<p className="text-[11px] font-bold text-[#333333] dark:text-white/70 mb-1">Format CSV attendu :</p>
|
||||
<code className="text-[10px] text-[#555555] dark:text-white/50 font-mono">
|
||||
terme_source,terme_cible<br/>
|
||||
contrat,contract<br/>
|
||||
résiliation,termination
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* 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-[200px]',
|
||||
isDragging ? 'border-[#C5A17A] bg-[#F5F0EA]' : 'border-[#D9D6D0] dark:border-white/10 hover:border-[#C5A17A] hover:bg-[#FAFAF8]',
|
||||
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]">Lecture du fichier…</p>
|
||||
</>
|
||||
)}
|
||||
{fileStatus === 'success' && (
|
||||
<>
|
||||
<CheckCircle2 size={32} 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={32} 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] mt-1">Cliquez pour réessayer</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{fileStatus === 'idle' && (
|
||||
<>
|
||||
<div className="w-14 h-14 rounded-2xl bg-[#F0EDE8] flex items-center justify-center text-[#8B6F47]">
|
||||
<Upload size={28} />
|
||||
</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">
|
||||
ou cliquez pour parcourir
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nom du glossaire (si fichier parsé) */}
|
||||
{fileStatus === 'success' && (
|
||||
<div className="mt-6">
|
||||
<label className="block text-xs font-bold text-[#333333] dark:text-white/70 uppercase tracking-wider mb-2">
|
||||
Nom du glossaire
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={parsedFileName}
|
||||
onChange={e => setParsedFileName(e.target.value)}
|
||||
placeholder="Ex : Termes RH internes"
|
||||
className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-white dark:bg-[#141414] text-[#1A1A1A] dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
<button
|
||||
onClick={handleCreateFromFile}
|
||||
disabled={fileStatus !== 'success' || 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>
|
||||
)}
|
||||
|
||||
{/* ── 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user