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

This commit is contained in:
2026-06-28 09:55:14 +02:00
parent 5d6afd2dad
commit 030950c962
5 changed files with 1005 additions and 536 deletions

View File

@@ -0,0 +1,142 @@
---
name: find-skills
description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
---
# Find Skills
This skill helps you discover and install skills from the open agent skills ecosystem.
## When to Use This Skill
Use this skill when the user:
- Asks "how do I do X" where X might be a common task with an existing skill
- Says "find a skill for X" or "is there a skill for X"
- Asks "can you do X" where X is a specialized capability
- Expresses interest in extending agent capabilities
- Wants to search for tools, templates, or workflows
- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
## What is the Skills CLI?
The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
**Key commands:**
- `npx skills find [query] [--owner <owner>]` - Search for skills interactively or by keyword, optionally scoped to a GitHub owner
- `npx skills add <package>` - Install a skill from GitHub or other sources
- `npx skills check` - Check for skill updates
- `npx skills update` - Update all installed skills
**Browse skills at:** https://skills.sh/
## How to Help Users Find Skills
### Step 1: Understand What They Need
When a user asks for help with something, identify:
1. The domain (e.g., React, testing, design, deployment)
2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
3. Whether this is a common enough task that a skill likely exists
### Step 2: Check the Leaderboard First
Before running a CLI search, check the [skills.sh leaderboard](https://skills.sh/) to see if a well-known skill already exists for the domain. The leaderboard ranks skills by total installs, surfacing the most popular and battle-tested options.
For example, top skills for web development include:
- `vercel-labs/agent-skills` — React, Next.js, web design (100K+ installs each)
- `anthropics/skills` — Frontend design, document processing (100K+ installs)
### Step 3: Search for Skills
If the leaderboard doesn't cover the user's need, run the find command:
```bash
npx skills find [query] [--owner <owner>]
```
For example:
- User asks "how do I make my React app faster?" → `npx skills find react performance`
- User asks "can you help me with PR reviews?" → `npx skills find pr review`
- User asks "I need to create a changelog" → `npx skills find changelog`
### Step 4: Verify Quality Before Recommending
**Do not recommend a skill based solely on search results.** Always verify:
1. **Install count** — Prefer skills with 1K+ installs. Be cautious with anything under 100.
2. **Source reputation** — Official sources (`vercel-labs`, `anthropics`, `microsoft`) are more trustworthy than unknown authors.
3. **GitHub stars** — Check the source repository. A skill from a repo with <100 stars should be treated with skepticism.
### Step 5: Present Options to the User
When you find relevant skills, present them to the user with:
1. The skill name and what it does
2. The install count and source
3. The install command they can run
4. A link to learn more at skills.sh
Example response:
```
I found a skill that might help! The "react-best-practices" skill provides
React and Next.js performance optimization guidelines from Vercel Engineering.
(185K installs)
To install it:
npx skills add vercel-labs/agent-skills@react-best-practices
Learn more: https://skills.sh/vercel-labs/agent-skills/react-best-practices
```
### Step 6: Offer to Install
If the user wants to proceed, you can install the skill for them:
```bash
npx skills add <owner/repo@skill> -g -y
```
The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
## Common Skill Categories
When searching, consider these common categories:
| Category | Example Queries |
| --------------- | ---------------------------------------- |
| Web Development | react, nextjs, typescript, css, tailwind |
| Testing | testing, jest, playwright, e2e |
| DevOps | deploy, docker, kubernetes, ci-cd |
| Documentation | docs, readme, changelog, api-docs |
| Code Quality | review, lint, refactor, best-practices |
| Design | ui, ux, design-system, accessibility |
| Productivity | workflow, automation, git |
## Tips for Effective Searches
1. **Use specific keywords**: "react testing" is better than just "testing"
2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
## When No Skills Are Found
If no relevant skills exist:
1. Acknowledge that no existing skill was found
2. Offer to help with the task directly using your general capabilities
3. Suggest the user could create their own skill with `npx skills init`
Example:
```
I searched for skills related to "xyz" but didn't find any matches.
I can still help you with this task directly! Would you like me to proceed?
If this is something you do often, you could create your own skill:
npx skills init my-xyz-skill
```

View File

@@ -0,0 +1,39 @@
---
name: writing-guidelines
description: Review docs/prose for Writing Guidelines compliance. Use when asked to "review my docs", "check writing style", "audit prose", "review docs voice and tone", or "check this page against the writing handbook".
metadata:
author: vercel
version: "1.0.0"
argument-hint: <file-or-pattern>
---
# Writing Guidelines
Review files for compliance with Writing Guidelines.
## How It Works
1. Fetch the latest guidelines from the source URL below
2. Read the specified files (or prompt user for files/pattern)
3. Check against all rules in the fetched guidelines
4. Output findings in the terse `file:line` format
## Guidelines Source
Fetch fresh guidelines before each review:
```
https://raw.githubusercontent.com/vercel-labs/writing-guidelines/main/command.md
```
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
## Usage
When a user provides a file or pattern argument:
1. Fetch guidelines from the source URL above
2. Read the specified files
3. Apply all rules from the fetched guidelines
4. Output findings using the format specified in the guidelines
If no files specified, ask the user which files to review.

View 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à é 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>
);
}

View File

@@ -1,131 +1,41 @@
'use client';
import { useState, useEffect, useMemo, useRef } from 'react';
import { useState, useEffect, useMemo } from 'react';
import {
BookText, Plus, Library, Calendar, Hash,
MessageSquare, Save, Trash2, Loader2,
CheckCircle2, AlertCircle, ArrowRight, Info, ExternalLink, Search,
Upload, Scale, Cpu, TrendingUp, HeartPulse, Megaphone, Users, FlaskConical, ShoppingCart, Zap, PenLine
Library, Calendar, Hash, MessageSquare, Save, Trash2, Loader2,
CheckCircle2, AlertCircle, ArrowRight, Info, Search, Plus,
Sparkles, Zap
} from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { cn } from '@/lib/utils';
import { useUser } from '@/app/dashboard/useUser';
import { useI18n } from '@/lib/i18n';
import { useGlossaries, useGlossaryTemplates } from './useGlossaries';
import type { GlossaryListItem, GlossaryTermInput } from './types';
import { useGlossaries } from './useGlossaries';
import type { GlossaryListItem } from './types';
import { ProUpgradePrompt } from './ProUpgradePrompt';
import { CreateGlossaryDialog } from './CreateGlossaryDialog';
import { useToast } from '@/components/ui/toast';
import { SUPPORTED_LANGUAGES } from './types';
import { useTranslationStore } from '@/lib/store';
import { parseFileToTerms } from './csvUtils';
const TEMPLATE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
legal: Scale,
technology: Cpu,
finance: TrendingUp,
medical: HeartPulse,
marketing: Megaphone,
hr: Users,
scientific: FlaskConical,
ecommerce: ShoppingCart,
};
function FileUploadZone({
onTermsParsed,
disabled,
t,
}: {
onTermsParsed: (terms: GlossaryTermInput[], filename: string) => void;
disabled: boolean;
t: (key: string, params?: any) => string;
}) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [status, setStatus] = useState<'idle' | 'parsing' | 'success' | 'error'>('idle');
const [errorMsg, setErrorMsg] = useState('');
const processFile = async (file: File) => {
const ext = file.name.split('.').pop()?.toLowerCase();
const allowed = ['csv', 'xlsx', 'xls', 'ods', 'txt', 'tsv'];
if (!ext || !allowed.includes(ext)) {
setStatus('error');
setErrorMsg(t('glossaries.dialog.errorFormat') || 'Format non supporté');
return;
}
if (file.size > 5 * 1024 * 1024) {
setStatus('error');
setErrorMsg(t('glossaries.dialog.errorSize', { max: '5' }) || 'Fichier trop volumineux (max 5MB)');
return;
}
setStatus('parsing');
setErrorMsg('');
try {
const terms = await parseFileToTerms(file);
if (terms.length === 0) {
setStatus('error');
setErrorMsg(t('glossaries.dialog.errorEmpty') || 'Fichier vide');
return;
}
setStatus('success');
onTermsParsed(terms, file.name);
setTimeout(() => setStatus('idle'), 2000);
} catch {
setStatus('error');
setErrorMsg(t('glossaries.dialog.errorRead') || 'Erreur de lecture');
}
};
// ── Chips de suggestions pour les consignes de contexte ─────────────────────
const CONTEXT_SUGGESTIONS = [
{ label: 'Ton formel', value: 'Utilise toujours un ton formel et professionnel dans tes traductions.' },
{ label: 'Noms propres', value: 'Ne traduis pas les noms propres, marques et noms de personnes.' },
{ label: 'Chiffres', value: 'Garde tous les chiffres, pourcentages et montants tels quels sans les modifier.' },
{ label: 'Placeholders', value: 'Ne traduis pas les variables entre accolades comme {nom}, {date}, {montant}.' },
{ label: 'Termes techniques', value: 'Conserve les termes techniques en langue originale et ne les traduis pas.' },
{ label: 'Style concis', value: 'Préfère des formulations courtes et directes. Évite les périphrases.' },
];
function renderTitle(title: string) {
const lastSpaceIndex = title.lastIndexOf(' ');
if (lastSpaceIndex === -1) return title;
return (
<div
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={(e) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) processFile(file); }}
onClick={() => !disabled && fileInputRef.current?.click()}
className={cn(
'editorial-card flex flex-col items-center justify-center gap-3 p-6 text-center transition-all cursor-pointer min-h-[140px]',
isDragging ? 'border-brand-accent bg-brand-accent/5' : 'border-black/5 dark:border-white/5 hover:border-brand-accent/30 hover:bg-brand-muted/30 dark:hover:bg-white/[0.02]',
disabled && 'opacity-50 cursor-not-allowed',
status === 'error' && 'border-destructive/30 bg-destructive/5'
)}
>
<input
ref={fileInputRef}
type="file"
accept=".csv,.xlsx,.xls,.ods,.txt,.tsv"
className="hidden"
onChange={(e) => { const file = e.target.files?.[0]; if (file) processFile(file); e.target.value = ''; }}
disabled={disabled}
/>
{status === 'parsing' ? (
<>
<Loader2 className="size-6 animate-spin text-brand-accent" />
<p className="text-[11px] text-brand-dark/50 dark:text-white/40 font-light">{t('glossaries.dialog.parsing') || 'Analyse…'}</p>
</>
) : status === 'error' ? (
<>
<AlertCircle className="size-6 text-destructive" />
<p className="text-[11px] font-medium text-destructive leading-tight">{errorMsg}</p>
<span className="text-[9px] font-bold uppercase tracking-wider text-brand-dark/40 dark:text-white/35">{t('glossaries.dialog.retry') || 'Réessayer'}</span>
</>
) : status === 'success' ? (
<>
<CheckCircle2 className="size-6 text-emerald-600 animate-bounce" />
<p className="text-[11px] font-bold text-emerald-700 dark:text-emerald-400">{t('glossaries.toast.imported') || 'Importé avec succès'}</p>
</>
) : (
<>
<div className="w-10 h-10 rounded-xl bg-brand-accent/10 dark:bg-brand-accent/15 flex items-center justify-center text-brand-accent shrink-0 animate-pulse">
<Upload className="size-5" />
</div>
<div>
<p className="text-xs font-serif font-semibold text-brand-dark dark:text-white leading-tight tracking-tight">{t('glossaries.dialog.tabFile') || 'Glissez un fichier CSV'}</p>
<p className="text-[10px] text-brand-dark/40 dark:text-white/30 mt-1 font-light leading-snug">{t('glossaries.dialog.dropFormats') || 'Format supporté: CSV, Excel'}</p>
</div>
</>
)}
</div>
<>
{title.substring(0, lastSpaceIndex)}{' '}
<span className="italic">{title.substring(lastSpaceIndex + 1)}</span>
</>
);
}
@@ -133,27 +43,15 @@ export default function GlossariesPage() {
const { t } = useI18n();
const router = useRouter();
const { data: user, isLoading: isLoadingUser } = useUser();
const {
glossaries,
total,
isLoading: isLoadingGlossaries,
isCreating,
isImportingTemplate,
createGlossary,
importTemplate,
} = useGlossaries();
const { templates, isLoading: isLoadingTemplates } = useGlossaryTemplates();
const { glossaries, isLoading: isLoadingGlossaries } = useGlossaries();
const { toast } = useToast();
const { settings, updateSettings } = useTranslationStore();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [systemPrompt, setSystemPrompt] = useState(settings.systemPrompt);
const [isSavingPrompt, setIsSavingPrompt] = useState(false);
const [promptSaved, setPromptSaved] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [importingPresetId, setImportingPresetId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'glossaries' | 'context'>('glossaries');
const [showCreateSection, setShowCreateSection] = useState(false);
const isPro = user?.tier === 'pro';
const isLoading = isLoadingUser || isLoadingGlossaries;
@@ -187,79 +85,12 @@ export default function GlossariesPage() {
setSystemPrompt('');
};
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 handleAddSuggestion = (value: string) => {
const current = systemPrompt.trim();
const newPrompt = current ? `${current}\n${value}` : value;
setSystemPrompt(newPrompt);
};
const handleImportPreset = async (templateId: string, name?: string) => {
setImportingPresetId(templateId);
try {
await importTemplate(templateId, name);
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'),
});
} finally {
setImportingPresetId(null);
}
};
const handleFileTermsImport = async (parsedTerms: GlossaryTermInput[], filename: string) => {
try {
const baseName = filename.replace(/\.[^.]+$/, '').replace(/[_-]/g, ' ');
await createGlossary({
name: baseName,
source_language: 'fr',
target_language: 'multi',
terms: parsedTerms,
});
toast({
title: t('glossaries.toast.created'),
description: t('glossaries.toast.createdDesc', { name: baseName }),
});
} catch (error) {
toast({
variant: 'destructive',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorCreate'),
});
}
};
const importedTemplateIds = useMemo(() => {
return new Set(
glossaries
.map((g: GlossaryListItem) => g.template_id)
.filter(Boolean) as string[]
);
}, [glossaries]);
const availableTemplates = useMemo(() => {
return templates.filter(t => !importedTemplateIds.has(t.id));
}, [templates, importedTemplateIds]);
const filteredGlossaries = useMemo(() => {
if (!searchQuery.trim()) return glossaries;
const q = searchQuery.toLowerCase();
@@ -271,7 +102,7 @@ export default function GlossariesPage() {
<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">{t('glossaries.loading')}</p>
<p className="text-xs text-[#555555] dark:text-white/40 font-light">{t('glossaries.loading')}</p>
</div>
</div>
);
@@ -281,34 +112,41 @@ export default function GlossariesPage() {
return <ProUpgradePrompt />;
}
const isProcessing = isCreating || isImportingTemplate || !!importingPresetId;
return (
<div className="max-w-6xl mx-auto w-full p-6 lg:p-8">
{/* ── Editorial Header ───────────────────────────────────── */}
{/* ── 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"}
{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")}
{renderTitle(t('glossaries.title') || 'Glossaires & Contexte')}
</h1>
<p className="text-[#555555] dark:text-white/60 text-sm font-light leading-relaxed">
{t('glossaries.description') || "Gérez vos glossaires et instructions de contexte pour des traductions plus précises."}
{t('glossaries.description') || 'Gérez vos glossaires et instructions de contexte pour des traductions plus précises.'}
</p>
</div>
{/* Bouton principal — unique point d'entrée pour créer */}
<Link
href="/dashboard/glossaries/new"
className="shrink-0 flex items-center gap-2 px-5 py-2.5 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all"
>
<Plus size={14} />
Nouveau glossaire
</Link>
</div>
{/* ── Bandeau usage ──────────────────────────────────── */}
{/* ── Bandeau informatif ────────────────────────────────────── */}
<div className="mb-8 flex items-center gap-3 p-4 rounded-xl bg-[#F5F0EA] dark:bg-brand-accent/10 border border-[#D4B896] dark:border-brand-accent/20">
<div className="w-7 h-7 rounded-full bg-[#8B6F47] text-white flex items-center justify-center shrink-0">
<Info size={13} />
</div>
<p className="text-[11px] text-[#2D2D2D] dark:text-white/70 font-medium leading-relaxed flex-1">
<strong className="font-bold text-[#1A1A1A] dark:text-white">Pour utiliser un glossaire dans une traduction :</strong>{' '}
cliquez sur <span className="font-bold text-[#7A5C35] dark:text-brand-accent">« Utiliser »</span> sur la carte souhaitée. Vous serez redirigé sur la page Traduire avec ce glossaire déjà sélectionné.
cliquez sur <span className="font-bold text-[#7A5C35] dark:text-brand-accent">« Utiliser »</span> sur la carte souhaitée.
Vous serez redirigé sur la page Traduire avec ce glossaire déjà sélectionné.
</p>
<Link
href="/dashboard/translate"
@@ -318,90 +156,115 @@ export default function GlossariesPage() {
</Link>
</div>
{/* ── Tab Switcher ───────────────────────────────────────── */}
<div className="flex border-b border-black/5 dark:border-white/5 mb-8">
{/* ── Onglets ───────────────────────────────────────────────── */}
<div className="flex border-b border-black/10 dark:border-white/10 mb-8">
<button
onClick={() => setActiveTab('glossaries')}
className={cn(
"pb-4 px-6 text-xs uppercase tracking-widest font-bold border-b-2 transition-all cursor-pointer",
'pb-4 px-6 text-xs uppercase tracking-widest font-bold border-b-2 transition-all cursor-pointer',
activeTab === 'glossaries'
? "border-brand-accent text-brand-dark dark:text-white"
: "border-transparent text-[#555555] dark:text-white/40 hover:text-[#1A1A1A] dark:hover:text-white/80"
? 'border-brand-accent text-[#1A1A1A] dark:text-white'
: 'border-transparent text-[#555555] dark:text-white/40 hover:text-[#1A1A1A] dark:hover:text-white/80'
)}
>
{t('glossaries.tabs.glossaries') || "Glossaires terminologiques"}
{t('glossaries.tabs.glossaries') || 'Glossaires terminologiques'}
</button>
<button
onClick={() => setActiveTab('context')}
className={cn(
"pb-4 px-6 text-xs uppercase tracking-widest font-bold border-b-2 transition-all cursor-pointer",
'pb-4 px-6 text-xs uppercase tracking-widest font-bold border-b-2 transition-all cursor-pointer',
activeTab === 'context'
? "border-brand-accent text-brand-dark dark:text-white"
: "border-transparent text-[#555555] dark:text-white/40 hover:text-[#1A1A1A] dark:hover:text-white/80"
? 'border-brand-accent text-[#1A1A1A] dark:text-white'
: 'border-transparent text-[#555555] dark:text-white/40 hover:text-[#1A1A1A] dark:hover:text-white/80'
)}
>
{t('glossaries.tabs.context') || "Consignes de contexte (IA)"}
{t('glossaries.tabs.context') || 'Consignes de contexte (IA)'}
</button>
</div>
<div className="space-y-10">
{activeTab === 'context' ? (
/* ── System Prompt (Context) ─────────────────────────────── */
/* ── Onglet Consignes de Contexte ─────────────────────── */
<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 animate-fade-in">
<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>
{/* En-tête section */}
<div className="flex items-start justify-between mb-6 gap-4">
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-xl bg-brand-accent/10 flex items-center justify-center text-[#8B6F47] shrink-0">
<MessageSquare size={18} />
</div>
<div>
<h2 className="text-base font-serif font-semibold text-[#1A1A1A] dark:text-white tracking-tight">
Consignes de contexte
</h2>
<p className="text-[11px] text-[#555555] dark:text-white/50 font-light mt-0.5">
Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction en mode Pro LLM.
</p>
</div>
</div>
{/* Badge d'état */}
{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">
<span className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-amber-700 bg-amber-100 dark:text-amber-400 dark:bg-amber-500/10 px-3 py-1.5 rounded-full border border-amber-300 dark:border-amber-500/20">
<AlertCircle size={11} />
{t('glossaries.status.unsaved')}
Non sauvegardé
</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">
<span className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-emerald-700 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-500/10 px-3 py-1.5 rounded-full border border-emerald-300 dark:border-emerald-500/20">
<CheckCircle2 size={11} />
{t('glossaries.status.active')}
Actif
</span>
) : (
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-[#555555] dark:text-white/30 bg-[#EBEBEB] dark:bg-white/5 px-3 py-1 rounded-full border border-[#CCCCCC] dark:border-white/5">
{t('glossaries.status.inactive')}
<span className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-[#555555] bg-[#EBEBEB] dark:text-white/30 dark:bg-white/5 px-3 py-1.5 rounded-full border border-[#CCCCCC] dark:border-white/10">
Inactif
</span>
)}
</div>
{/* Explanation box */}
<div className="mb-5 p-3.5 rounded-xl bg-[#F5F3EF] dark:bg-white/[0.03] border border-[#D9D6D0] dark:border-white/5">
<p className="text-[11px] text-[#3D3D3D] dark:text-white/50 font-medium leading-relaxed">
<strong className="font-bold text-[#1A1A1A] dark:text-white/80">{t('glossaries.instructions.whatForBold')}</strong> {t('glossaries.instructions.whatForDesc')}
</p>
<p className="text-[11px] text-[#6B4F2A] dark:text-brand-accent/70 font-medium mt-2 italic">
{t('glossaries.instructions.example')}
</p>
<p className="text-[10px] text-[#6B4F2A] dark:text-brand-accent font-bold mt-2.5 flex items-center gap-1.5 border-t border-[#D9D6D0] dark:border-white/5 pt-2">
<Info size={11} />
Remarque : Ces consignes s'appliquent automatiquement à toutes vos traductions réalisées en mode Pro LLM.
</p>
{/* Chips de suggestions rapides */}
<div className="mb-5">
<div className="flex items-center gap-2 mb-3">
<Sparkles size={13} className="text-[#8B6F47]" />
<span className="text-[10px] font-bold uppercase tracking-widest text-[#555555] dark:text-white/40">
Suggestions rapides cliquez pour ajouter
</span>
</div>
<div className="flex flex-wrap gap-2">
{CONTEXT_SUGGESTIONS.map((s) => (
<button
key={s.label}
onClick={() => handleAddSuggestion(s.value)}
className="px-3 py-1.5 rounded-lg text-[11px] font-semibold text-[#3D3D3D] dark:text-white/70 bg-[#F0EDE8] dark:bg-white/5 border border-[#D9D5CE] dark:border-white/10 hover:bg-[#E8E2DA] hover:border-[#C5A17A] dark:hover:bg-brand-accent/10 dark:hover:border-brand-accent/30 transition-all cursor-pointer"
>
+ {s.label}
</button>
))}
</div>
</div>
{/* Zone de texte */}
<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"
placeholder="Ex : Vous traduisez des documents techniques HVAC. Utilisez une terminologie d'ingénierie précise et un ton professionnel…"
className="w-full h-44 p-4 bg-[#FAFAF8] dark:bg-white/[0.02] rounded-xl border border-[#D9D6D0] dark:border-white/10 text-[#1A1A1A] dark:text-white text-xs leading-relaxed focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50 transition-all outline-none resize-y placeholder:text-[#AAAAAA] dark:placeholder:text-white/25"
/>
{/* Pied de section */}
<div className="flex justify-between items-center mt-4">
<p className="text-[10px] text-[#777777] dark:text-white/25 font-light">
{systemPrompt.length > 0 ? t('glossaries.instructions.charCount', { count: systemPrompt.length }) : t('glossaries.instructions.emptyHint')}
{systemPrompt.length > 0
? `${systemPrompt.length} caractères`
: 'Aucune consigne enregistrée'}
</p>
<div className="flex gap-3">
<button
onClick={handleClearPrompt}
className="px-5 py-2.5 bg-[#EBEBEB] dark:bg-white/5 text-[#444444] dark:text-white/40 rounded-lg text-xs font-bold uppercase tracking-wider hover:text-[#1A1A1A] dark:hover:text-white transition-all cursor-pointer"
disabled={!systemPrompt}
className="px-5 py-2.5 bg-[#EBEBEB] dark:bg-white/5 text-[#444444] dark:text-white/40 rounded-lg text-xs font-bold uppercase tracking-wider hover:text-[#1A1A1A] hover:bg-[#E0E0E0] dark:hover:text-white transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
>
<Trash2 size={12} className="inline mr-1.5" />{t('glossaries.instructions.clearAll')}
<Trash2 size={12} className="inline mr-1.5" />
Tout effacer
</button>
<button
onClick={handleSavePrompt}
@@ -409,323 +272,171 @@ export default function GlossariesPage() {
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" /> {t('glossaries.instructions.saving')}</>
? <><Loader2 size={14} className="animate-spin" /> Enregistrement</>
: promptSaved
? <><CheckCircle2 size={14} /> {t('glossaries.instructions.saved')}</>
: <><Save size={14} /> {t('context.save')}</>
? <><CheckCircle2 size={14} /> Enregistré</>
: <><Save size={14} /> Enregistrer</>
}
</button>
</div>
</div>
</section>
) : (
<div className="space-y-12 animate-fade-in">
{/* ── Section 1 : Vos Glossaires (En premier !) ───────────────── */}
<section>
<div className="flex items-center justify-between mb-5 gap-4">
<div>
<h2 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{t('glossaries.grid.title')} <span className="italic">{t('glossaries.grid.titleHighlight')}</span>
</h2>
<p className="text-[11px] text-[#555555] dark:text-white/40 font-light mt-1">
{glossaries.length > 0
? t('glossaries.grid.countWithAction', { count: glossaries.length, plural: glossaries.length > 1 ? 's' : '' })
: t('glossaries.grid.emptyAction')}
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
{currentTargetInfo && (
<span className="flex items-center gap-1.5 text-[10px] font-bold text-[#444444] dark:text-white/40 bg-[#EBEBEB] dark:bg-white/5 border border-[#CCCCCC] dark:border-white/5 px-3 py-1.5 rounded-full">
<span>{t('glossaries.grid.activeTranslation')}</span>
<span>{currentTargetInfo.flag} {currentTargetInfo.label}</span>
</span>
)}
</div>
/* ── Onglet Glossaires ──────────────────────────────────── */
<div className="space-y-6 animate-fade-in">
{/* En-tête de section + compteur */}
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-lg font-serif font-medium text-[#1A1A1A] dark:text-white tracking-tight">
{t('glossaries.grid.title') || 'Vos'}{' '}
<span className="italic">{t('glossaries.grid.titleHighlight') || 'glossaires'}</span>
</h2>
<p className="text-[11px] text-[#555555] 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`
: 'Aucun glossaire — créez-en un pour commencer'}
</p>
</div>
{/* Search bar (only if more than 3 glossaries) */}
{glossaries.length > 3 && (
<div className="relative mb-5">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-brand-dark/30 dark:text-white/30" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('glossaries.grid.searchPlaceholder') || "Rechercher un glossaire…"}
className="w-full pl-9 pr-3 py-2.5 text-xs rounded-lg border border-black/5 dark:border-white/10 bg-white dark:bg-[#141414] focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30"
/>
</div>
{currentTargetInfo && (
<span className="flex items-center gap-1.5 text-[10px] font-bold text-[#444444] dark:text-white/40 bg-[#EBEBEB] dark:bg-white/5 border border-[#CCCCCC] dark:border-white/5 px-3 py-1.5 rounded-full shrink-0">
<span>Traduction active :</span>
<span>{currentTargetInfo.flag} {currentTargetInfo.label}</span>
</span>
)}
</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-[#555555] dark:text-white/40 font-light mb-5">{t('glossaries.emptyDesc')}</p>
<button
onClick={() => setCreateDialogOpen(true)}
className="premium-button px-6 py-2.5 text-[11px] uppercase tracking-widest !rounded-lg inline-flex items-center gap-2 cursor-pointer font-bold"
>
<Plus size={12} />
{t('glossaries.createNew') || "Créer un glossaire"}
</button>
</div>
) : filteredGlossaries.length === 0 ? (
<div className="editorial-card p-8 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm text-center">
<p className="text-xs text-brand-dark/50 dark:text-white/50 font-light">
{t('glossaries.grid.noResults') || "Aucun résultat pour cette recherche."}
</p>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredGlossaries.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';
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 text-left w-full flex flex-col justify-between min-h-[200px]',
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'
)}
>
{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} /> {t('glossaries.badge.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} /> {t('glossaries.badge.otherTarget')}
</div>
)}
<div className="cursor-pointer flex-1" onClick={() => router.push(`/dashboard/glossaries/${glossary.id}`)}>
<div className="flex items-start gap-3 mb-4">
<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 group-hover:text-brand-accent transition-colors">
{glossary.name}
</h3>
<p className="text-[11px] text-[#555555] dark:text-white/50 font-medium flex items-center gap-1 mt-0.5">
<span>{srcInfo?.flag ?? '🌐'}</span>
<span>{srcInfo?.label ?? glossary.source_language}</span>
<span className="text-[#7A5C35] dark: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 pb-4">
<span className="flex items-center gap-1 text-xs font-semibold text-[#333333] dark:text-white/65">
<Hash size={12} className="text-[#8B6F47] dark:text-brand-accent" />
{termCount} {t('glossaries.defineTerms') || "termes"}
</span>
<span className="flex items-center gap-1 font-mono text-[9px] text-[#666666] dark:text-white/40">
<Calendar size={11} />
{new Date(glossary.created_at).toLocaleDateString()}
</span>
</div>
</div>
<div className="flex gap-2 pt-4 border-t border-black/5 dark:border-white/10 mt-auto shrink-0 w-full">
<Link
href={`/dashboard/glossaries/${glossary.id}`}
className="flex-none px-3 py-2 rounded-lg bg-[#EBEBEB] dark:bg-white/5 hover:bg-[#E0D9D1] text-[#333333] dark:text-white/50 hover:text-[#1A1A1A] text-[10px] font-bold uppercase tracking-wider transition-all border border-[#CCCCCC] dark:border-white/10"
>
Éditer
</Link>
<Link
href={`/dashboard/translate?glossaryId=${glossary.id}`}
className="flex-1 text-center py-2 rounded-lg bg-[#1A1A1A] hover:bg-[#333333] text-white text-[10px] font-bold uppercase tracking-wider transition-all flex items-center justify-center gap-1.5"
>
Utiliser <ArrowRight size={11} />
</Link>
</div>
</div>
);
})}
</div>
)}
</section>
{/* ── Bouton Ajouter (visible quand l'utilisateur a déjà des glossaires) */}
{glossaries.length > 0 && !showCreateSection && (
<div className="flex justify-center">
<button
onClick={() => setShowCreateSection(true)}
className="flex items-center gap-2 px-6 py-3 rounded-xl border-2 border-dashed border-[#999999] text-[#333333] dark:text-white/60 dark:border-white/20 text-xs font-bold uppercase tracking-widest hover:border-[#555555] hover:text-[#1A1A1A] dark:hover:text-white transition-all cursor-pointer"
>
<Plus size={14} />
Ajouter un glossaire
</button>
{/* Barre de recherche (si > 3 glossaires) */}
{glossaries.length > 3 && (
<div className="relative">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[#888888] dark:text-white/30" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Rechercher un glossaire…"
className="w-full pl-9 pr-3 py-2.5 text-xs rounded-lg border border-[#D9D6D0] dark:border-white/10 bg-white dark:bg-[#141414] text-[#1A1A1A] dark:text-white placeholder:text-[#AAAAAA] dark:placeholder:text-white/25 focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30"
/>
</div>
)}
{/* ── Section 2 : Créer ou Importer ─────────────────────────── */}
{(glossaries.length === 0 || showCreateSection) && (
<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="mb-6">
<div className="flex items-center justify-between gap-4 mb-2">
<div className="flex items-center gap-2 text-brand-accent">
<Zap size={18} />
<h2 className="text-sm font-serif font-medium text-brand-dark dark:text-white tracking-tight">
Ajouter un glossaire
</h2>
</div>
{showCreateSection && (
<button
onClick={() => setShowCreateSection(false)}
className="text-[10px] font-bold uppercase tracking-wider text-brand-dark/30 dark:text-white/25 hover:text-brand-dark dark:hover:text-white transition-colors cursor-pointer"
>
Fermer
</button>
)}
{/* État vide */}
{glossaries.length === 0 ? (
<div className="editorial-card p-16 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm text-center">
<div className="w-14 h-14 bg-[#F0EDE8] dark:bg-white/10 rounded-2xl flex items-center justify-center text-[#8B6F47] mx-auto mb-5">
<Library size={28} />
</div>
<p className="text-xs text-[#555555] dark:text-white/40 font-light">
Choisissez un modèle professionnel, importez un fichier CSV ou créez manuellement.
<p className="text-base font-serif font-semibold text-[#1A1A1A] dark:text-white mb-2">
{t('glossaries.empty') || 'Aucun glossaire'}
</p>
<p className="text-sm text-[#555555] dark:text-white/40 font-light mb-6 max-w-xs mx-auto">
{t('glossaries.emptyDesc') || 'Créez votre premier glossaire pour garantir une terminologie cohérente dans vos traductions.'}
</p>
<Link
href="/dashboard/glossaries/new"
className="inline-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"
>
<Plus size={12} />
Créer mon premier glossaire
</Link>
</div>
) : filteredGlossaries.length === 0 ? (
<div className="p-8 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl text-center">
<p className="text-sm text-[#555555] dark:text-white/40 font-light">
Aucun résultat pour «&nbsp;{searchQuery}&nbsp;»
</p>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5">
{filteredGlossaries.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';
const matchesTarget = currentTargetLang && (isMultilingual || glossary.target_language === currentTargetLang);
const mismatch = currentTargetLang && !isMultilingual && glossary.target_language && glossary.target_language !== currentTargetLang;
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Grille des Modèles (2/3 de largeur) */}
<div className="lg:col-span-2 space-y-3">
<div className="flex items-center gap-1.5 text-brand-accent">
<BookText size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">{t('context.presets.title') || 'Modèles professionnels'}</span>
</div>
{isLoadingTemplates ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="size-6 animate-spin text-brand-muted" />
</div>
) : availableTemplates.length === 0 ? (
<div className="p-8 bg-brand-muted/20 dark:bg-white/[0.01] border border-black/5 dark:border-white/5 rounded-xl text-center w-full">
<p className="text-xs text-brand-dark/40 dark:text-white/30 font-light">
{t('glossaries.presets.allImported') || 'Tous les modèles professionnels ont été importés.'}
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{availableTemplates.map((template) => {
const Icon = TEMPLATE_ICONS[template.id] || BookText;
const isProcessingThis = importingPresetId === template.id;
return (
<div
key={template.id}
className="relative p-4 rounded-xl text-left border transition-all min-h-[110px] flex flex-col justify-between group bg-brand-muted/40 dark:bg-white/5 border-black/5 dark:border-white/5 hover:border-brand-accent/20 hover:bg-brand-accent/[0.02]"
>
<div className="flex items-center justify-between gap-2 w-full">
<div className="p-1.5 bg-brand-accent/10 rounded-lg text-brand-accent group-hover:scale-115 transition-transform">
{isProcessingThis ? <Loader2 size={16} className="animate-spin" /> : <Icon className="size-4" />}
</div>
<button
disabled={isProcessing}
onClick={() => handleImportPreset(template.id, template.name)}
className="accent-pill !px-3 !py-1 !text-[9px] font-bold uppercase tracking-wider bg-brand-accent text-white hover:bg-brand-accent/90 rounded-md transition-colors cursor-pointer"
>
{t('glossaries.presets.importBtn') || 'Importer'}
</button>
</div>
<div className="mt-3">
<div className="flex justify-between items-baseline gap-2">
<p className="text-xs font-serif font-bold text-brand-dark dark:text-white leading-tight">
{template.name.split(' - ')[0]}
</p>
<span className="text-[9px] text-brand-dark/40 dark:text-white/30 font-semibold font-mono shrink-0">
{template.terms_count} {t('glossaries.defineTerms') || 'termes'}
</span>
</div>
<p className="text-[10px] text-brand-dark/45 dark:text-white/35 font-light leading-normal mt-1 line-clamp-2">
{template.description}
</p>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Fichier & Manuel (1/3 de largeur) */}
<div className="space-y-5">
{/* Import Fichier */}
<div className="space-y-2.5">
<div className="flex items-center gap-1.5 text-brand-accent">
<Upload size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">{t('glossaries.dialog.tabFile') || 'Importer un fichier'}</span>
</div>
<FileUploadZone
onTermsParsed={handleFileTermsImport}
disabled={isProcessing}
t={t}
/>
</div>
{/* Création Manuelle */}
<div className="space-y-2.5">
<div className="flex items-center gap-1.5 text-brand-accent">
<PenLine size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">{t('glossaries.dialog.tabManual') || 'Création manuelle'}</span>
</div>
<button
onClick={() => setCreateDialogOpen(true)}
disabled={isProcessing}
className="editorial-card w-full flex items-center justify-between p-5 text-left transition-all hover:border-brand-accent/30 hover:bg-brand-muted/30 dark:hover:bg-white/[0.02] cursor-pointer group border-black/5 dark:border-white/5"
return (
<div
key={glossary.id}
className={cn(
'bg-white dark:bg-[#141414] border rounded-2xl p-6 shadow-sm group transition-all relative flex flex-col justify-between min-h-[200px]',
matchesTarget
? 'border-brand-accent/50 ring-1 ring-brand-accent/20 hover:border-brand-accent/70'
: mismatch
? 'border-amber-300/50 dark:border-amber-500/20 hover:border-amber-400/70 opacity-80 hover:opacity-100'
: 'border-[#E5E3DF] dark:border-white/5 hover:border-[#C5A17A]/40'
)}
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-brand-accent/10 dark:bg-brand-accent/15 flex items-center justify-center text-brand-accent shrink-0 group-hover:scale-110 transition-transform">
<Plus size={16} />
{/* Badge compatible / autre langue */}
{matchesTarget && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider border border-emerald-300 dark:border-emerald-500/20">
<CheckCircle2 size={9} /> Compatible
</div>
<div>
<p className="text-xs font-serif font-bold text-brand-dark dark:text-white leading-tight">{t('glossaries.createNew') || 'Créer manuellement'}</p>
<p className="text-[10px] text-brand-dark/40 dark:text-white/35 mt-1 font-light leading-tight">{t('glossaries.dialog.createEmpty') || 'À partir de zéro'}</p>
)}
{mismatch && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-amber-100 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider border border-amber-300 dark:border-amber-500/20">
<AlertCircle size={9} /> Autre langue
</div>
)}
{/* Corps cliquable → page de détail */}
<div className="cursor-pointer flex-1" onClick={() => router.push(`/dashboard/glossaries/${glossary.id}`)}>
<div className="flex items-start gap-3 mb-4">
<div className="w-10 h-10 bg-[#F0EDE8] dark:bg-white/10 rounded-xl flex items-center justify-center text-[#8B6F47] 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-[#1A1A1A] dark:text-white tracking-tight leading-snug line-clamp-2 group-hover:text-[#8B6F47] transition-colors">
{glossary.name}
</h3>
<p className="text-[11px] text-[#555555] dark:text-white/50 font-medium flex items-center gap-1 mt-0.5">
<span>{srcInfo?.flag ?? '🌐'}</span>
<span>{srcInfo?.label ?? glossary.source_language}</span>
<span className="text-[#7A5C35] dark: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 pb-4">
<span className="flex items-center gap-1 text-xs font-semibold text-[#333333] dark:text-white/65">
<Hash size={12} className="text-[#8B6F47] dark:text-brand-accent" />
{termCount} terme{termCount > 1 ? 's' : ''}
</span>
<span className="flex items-center gap-1 font-mono text-[9px] text-[#666666] dark:text-white/40">
<Calendar size={11} />
{new Date(glossary.created_at).toLocaleDateString('fr-FR')}
</span>
</div>
</div>
<ArrowRight className="size-4 text-brand-dark/30 dark:text-white/20 group-hover:text-brand-accent group-hover:translate-x-1 transition-all" />
</button>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-4 border-t border-[#F0EDE8] dark:border-white/10 mt-auto shrink-0">
<Link
href={`/dashboard/glossaries/${glossary.id}`}
className="flex-none px-3 py-2 rounded-lg bg-[#EBEBEB] dark:bg-white/5 hover:bg-[#E0D9D1] text-[#333333] dark:text-white/50 hover:text-[#1A1A1A] text-[10px] font-bold uppercase tracking-wider transition-all border border-[#CCCCCC] dark:border-white/10"
>
Éditer
</Link>
<Link
href={`/dashboard/translate?glossaryId=${glossary.id}`}
className="flex-1 text-center py-2 rounded-lg bg-[#1A1A1A] hover:bg-[#333333] text-white text-[10px] font-bold uppercase tracking-wider transition-all flex items-center justify-center gap-1.5"
>
Utiliser <ArrowRight size={11} />
</Link>
</div>
</div>
);
})}
</div>
</section>
)}
</div>
)}
</div>
{/* Dialogs */}
<CreateGlossaryDialog
open={createDialogOpen}
onOpenChange={setCreateDialogOpen}
onCreate={handleCreateGlossary}
isCreating={isCreating}
/>
</div>
);
}
function 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>
</>
);
}

View File

@@ -5,6 +5,18 @@
"source": "vercel-labs/agent-browser",
"sourceType": "github",
"computedHash": "0eafe0971692cf8fcecb75f955595821452a835791d315db738f89ee49063a34"
},
"find-skills": {
"source": "vercel-labs/skills",
"sourceType": "github",
"skillPath": "skills/find-skills/SKILL.md",
"computedHash": "781bd6d3f9b19f8c9af6b53d8d0e4876d0183841b565db34ca7092ffa412d111"
},
"writing-guidelines": {
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"skillPath": "skills/writing-guidelines/SKILL.md",
"computedHash": "25aa3a33a97bddbcb1847ce4a6169106ec25e5b19b15b99c66e75ab383866ef2"
}
}
}