Compare commits

..

14 Commits

Author SHA1 Message Date
ebb2537fda feat(glossaries): implement a 3-step wizard for CSV/file imports with custom source/target language selection
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m21s
2026-06-28 11:38:18 +02:00
36aeac2c5e fix(glossaries): prevent translation data destruction on language selector change for multi glossaries
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m53s
2026-06-28 11:03:31 +02:00
de9407f974 revert(docker): remove automated database glossary translation from startup sequence
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m0s
2026-06-28 10:55:19 +02:00
5e3fb0098b feat(docker): run glossary translation script automatically on backend container startup
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
2026-06-28 10:53:58 +02:00
a57b8a8e4d fix(glossaries): normalize language code casing when retrieving display translations 2026-06-28 10:52:08 +02:00
dde80f6bc3 feat(glossaries): update script to translate missing terms in any glossary, even if target_language is already 'multi'
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m43s
2026-06-28 10:46:48 +02:00
7398cae359 feat(glossaries): add script to translate non-multilingual database glossaries using Google Translate adapter
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m25s
2026-06-28 10:42:41 +02:00
030950c962 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
2026-06-28 09:55:14 +02:00
5d6afd2dad fix(contrast): replace all opacity-based colors with concrete hex values meeting WCAG 4.5:1 ratio
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m44s
2026-06-21 10:54:17 +02:00
07c50151cb fix(contrast): increase text contrast on glossary page for light mode readability
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m24s
2026-06-20 19:39:33 +02:00
299173fff7 ux(glossary): clarify selection flow - compact banner, Utiliser button, hide create section by default
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m27s
2026-06-20 19:18:46 +02:00
9b354befe3 fix(glossary): hide create section by default when glossaries exist - only shown on demand via toggle button
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m11s
2026-06-20 19:07:30 +02:00
4a8f33d36f fix(glossary): eliminate all redundant elements - filter imported templates, remove duplicate buttons and links
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
2026-06-20 19:03:44 +02:00
489df66c0f fix(i18n): add missing translation keys for tabs and presets 2026-06-20 19:01:09 +02:00
10 changed files with 1548 additions and 581 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,206 @@
# Innovation Strategy: office_translator (Wordly.art)
**Date:** 2026-06-13
**Strategist:** Sepehr
**Strategic Focus:** Identifier comment transformer la préservation parfaite du format Office (aujourd'hui un avantage technique) en un positionnement de marché défendable et un modèle d'affaires scalable, face à des acteurs comme DeepL (qui copie déjà ce créneau) et Google (qui peut le copier à tout moment).
---
## 🎯 Strategic Context
### Current Situation
**Produit technique mature, pré-lancement commercial.**
- Stack production-ready : FastAPI + Next.js 15 + PostgreSQL + Redis + Docker, déployé en self-hosted
- Différenciateur technique central et vérifiable : **préservation de la mise en forme complexe Excel/Word/PowerPoint** (formules, fusions, styles, images, headers/footers, animations) — c'est le *seul* vrai fossé concurrentiel selon ton positionnement
- Architecture multi-providers déjà en place (Google, DeepL, OpenAI, Ollama, DeepSeek, OpenRouter, LibreTranslate, WebLLM en navigateur) — flexibilité réelle mais risque de dilution du message
- Capacités secondaires existantes : self-hosting, WebLLM zero-data, vision pour images dans Excel, glossaires techniques
- Infrastructure monétisation prête (Stripe) mais **pas encore de clients payants actifs** documentés
- Tarification définie : Free / Starter $9 / Pro $29 / Business $79
- Marketing plan rédigé mais assets visuels (captures, vidéo démo) non produits
- Faiblesses techniques identifiées (credentials en dur, stockage JSON, CORS permissif, pas de Celery)
### Strategic Challenge
**Le défi stratégique central : la préservation du format est un avantage technique reproductible.**
DeepL a déjà empiété sur ce territoire et Google peut le cloner en quelques trimestres. Ton avantage actuel est *technique* (comment tu traduis), pas *positionnel* (où tu te places dans l'esprit du client). Trois sous-défis critiques :
1. **Quel segment précis est prêt à payer significativement plus pour la préservation du format ?** (Pas "tout le monde qui traduit des documents" — c'est trop large et DeepL/Google les servent déjà.)
2. **Quel modèle d'affaires verrouille cet avantage** au point que même un clone gratuit de Google ne puisse pas te déloger ? (Self-hosting enterprise ? Vertical spécialisé ? Données/apprentissage ? Communauté ?)
3. **Quel "job to be done" non-traduction ton outil accomplit-il déjà** et que tu n'exploites pas encore commercialement ? (Compliance ? Archivage multilingue ? Workflow documentaire intégré ?)
Sans réponse tranchée à ces trois questions, tu risques d'être un excellent produit technique avec une proposition de valeur diluée.
---
## 📊 MARKET ANALYSIS
### Market Landscape
{{market_landscape}}
### Competitive Dynamics
{{competitive_dynamics}}
### Market Opportunities
{{market_opportunities}}
### Critical Insights
{{market_insights}}
---
## 💼 BUSINESS MODEL ANALYSIS
### Current Business Model
{{current_business_model}}
### Value Proposition Assessment
{{value_proposition}}
### Revenue and Cost Structure
{{revenue_cost_structure}}
### Business Model Weaknesses
{{model_weaknesses}}
---
## ⚡ DISRUPTION OPPORTUNITIES
### Disruption Vectors
{{disruption_vectors}}
### Unmet Customer Jobs
{{unmet_jobs}}
### Technology Enablers
{{technology_enablers}}
### Strategic White Space
{{strategic_whitespace}}
---
## 🚀 INNOVATION OPPORTUNITIES
### Innovation Initiatives
{{innovation_initiatives}}
### Business Model Innovation
{{business_model_innovation}}
### Value Chain Opportunities
{{value_chain_opportunities}}
### Partnership and Ecosystem Plays
{{partnership_opportunities}}
---
## 🎲 STRATEGIC OPTIONS
### Option A: {{option_a_name}}
{{option_a_description}}
**Pros:** {{option_a_pros}}
**Cons:** {{option_a_cons}}
### Option B: {{option_b_name}}
{{option_b_description}}
**Pros:** {{option_b_pros}}
**Cons:** {{option_b_cons}}
### Option C: {{option_c_name}}
{{option_c_description}}
**Pros:** {{option_c_pros}}
**Cons:** {{option_c_cons}}
---
## 🏆 RECOMMENDED STRATEGY
### Strategic Direction
{{recommended_strategy}}
### Key Hypotheses to Validate
{{key_hypotheses}}
### Critical Success Factors
{{success_factors}}
---
## 📋 EXECUTION ROADMAP
### Phase 1: Immediate Impact
{{phase_1}}
### Phase 2: Foundation Building
{{phase_2}}
### Phase 3: Scale & Optimization
{{phase_3}}
---
## 📈 SUCCESS METRICS
### Leading Indicators
{{leading_indicators}}
### Lagging Indicators
{{lagging_indicators}}
### Decision Gates
{{decision_gates}}
---
## ⚠️ RISKS AND MITIGATION
### Key Risks
{{key_risks}}
### Mitigation Strategies
{{risk_mitigation}}
---
_Generated using BMAD Creative Intelligence Suite - Innovation Strategy Workflow_

View File

@@ -41,9 +41,11 @@ function getDisplaySource(
glossarySourceLang: string
): string {
if (!lang || lang === 'multi') return '';
if (lang === glossarySourceLang) return term.source;
const normalizedLang = lang.toLowerCase();
const normalizedSourceLang = glossarySourceLang.toLowerCase();
if (normalizedLang === normalizedSourceLang) return term.source;
const translations = term.translations || {};
return translations[lang] || '';
return translations[normalizedLang] || '';
}
/** Target term in the chosen language.
@@ -54,9 +56,11 @@ function getDisplayTarget(
glossaryTargetLang: string
): string {
if (!lang) return '';
if (lang === 'multi' || lang === glossaryTargetLang) return term.target;
const normalizedLang = lang.toLowerCase();
const normalizedTargetLang = glossaryTargetLang.toLowerCase();
if (normalizedLang === 'multi' || normalizedLang === normalizedTargetLang) return term.target;
const translations = term.translations || {};
return translations[lang] || '';
return translations[normalizedLang] || '';
}
export default function GlossaryDetailPage() {
@@ -149,10 +153,12 @@ export default function GlossaryDetailPage() {
if (i !== index) return t;
const translations = { ...(t.translations || {}) } as Record<string, string>;
const editLang = field === 'source' ? sourceLanguage : targetLanguage;
const editLang = (field === 'source' ? sourceLanguage : targetLanguage).toLowerCase();
const srcLang = glossary.source_language.toLowerCase();
const tgtLang = glossary.target_language.toLowerCase();
if (field === 'source') {
if (editLang === glossary.source_language) {
if (editLang === srcLang) {
return { ...t, source: value };
} else {
if (editLang && editLang !== 'multi') {
@@ -161,7 +167,7 @@ export default function GlossaryDetailPage() {
return { ...t, translations };
}
} else {
if (editLang === 'multi' || editLang === glossary.target_language) {
if (editLang === 'multi' || editLang === tgtLang) {
return { ...t, target: value };
} else {
if (editLang && editLang !== 'multi') {
@@ -211,12 +217,24 @@ export default function GlossaryDetailPage() {
};
const handleSourceLanguageChange = (newLang: string) => {
// For multilingual glossaries, the selector is a VIEW filter only.
// migrateTerms must not be called — it would destroy translation data.
if (!glossary || glossary.target_language === 'multi') {
setSourceLanguage(newLang);
return;
}
const updated = migrateTerms(terms, sourceLanguage, newLang, targetLanguage, targetLanguage);
setSourceLanguage(newLang);
setTerms(updated);
};
const handleTargetLanguageChange = (newLang: string) => {
// For multilingual glossaries, the selector is a VIEW filter only.
// migrateTerms must not be called — it would destroy translation data.
if (!glossary || glossary.target_language === 'multi') {
setTargetLanguage(newLang);
return;
}
const updated = migrateTerms(terms, sourceLanguage, sourceLanguage, targetLanguage, newLang);
setTargetLanguage(newLang);
setTerms(updated);

View File

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

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,25 +43,14 @@ 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 isPro = user?.tier === 'pro';
@@ -186,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();
@@ -270,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>
);
@@ -280,149 +112,159 @@ 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-brand-dark/50 dark:text-white/50 text-sm font-light leading-relaxed">
{t('glossaries.description') || "Gérez vos glossaires et instructions de contexte pour des traductions plus précises."}
<p 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.'}
</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>
{/* ── How it works ───────────────────────────────────────── */}
<div className="mb-10 p-5 rounded-2xl bg-brand-accent/5 dark:bg-brand-accent/10 border border-brand-accent/20 dark:border-brand-accent/15">
<div className="flex items-center gap-2 mb-4">
<Info size={15} className="text-brand-accent shrink-0" />
<span className="text-xs font-bold uppercase tracking-widest text-brand-accent">{t('glossaries.howItWorks.title')}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Step 1 */}
<div className="flex items-start gap-3">
<div className="w-7 h-7 rounded-full bg-brand-accent text-white flex items-center justify-center text-[11px] font-black shrink-0 mt-0.5">1</div>
<div>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">{t('glossaries.howItWorks.step1Title')}</p>
<p className="text-[11px] text-brand-dark/55 dark:text-white/50 font-light leading-relaxed">
{t('glossaries.howItWorks.step1Desc')}
</p>
</div>
</div>
{/* Arrow */}
<div className="hidden sm:flex items-center justify-center">
<ArrowRight size={18} className="text-brand-accent/40" />
</div>
{/* Step 2 */}
<div className="flex items-start gap-3">
<div className="w-7 h-7 rounded-full bg-brand-accent text-white flex items-center justify-center text-[11px] font-black shrink-0 mt-0.5">2</div>
<div>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">{t('glossaries.howItWorks.step2Title')}</p>
<p className="text-[11px] text-brand-dark/55 dark:text-white/50 font-light leading-relaxed">
{t('glossaries.howItWorks.step2Desc')}
</p>
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-brand-accent/10">
<p className="text-[11px] text-brand-dark/45 dark:text-white/40 font-light">
{t('glossaries.howItWorks.warning')}
</p>
{/* ── 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é.
</p>
<Link
href="/dashboard/translate"
className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-[#7A5C35] dark:text-brand-accent hover:underline whitespace-nowrap"
>
Aller traduire <ArrowRight size={11} />
</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-brand-dark/40 dark:text-white/40 hover:text-brand-dark/70 dark:hover:text-white/70"
? '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-brand-dark/40 dark:text-white/40 hover:text-brand-dark/70 dark:hover:text-white/70"
? '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-brand-dark/30 dark:text-white/30 bg-brand-muted dark:bg-white/5 px-3 py-1 rounded-full border border-black/5 dark:border-white/5">
{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-brand-muted/40 dark:bg-white/[0.03] border border-black/5 dark:border-white/5">
<p className="text-[11px] text-brand-dark/60 dark:text-white/50 font-light leading-relaxed">
<strong className="font-bold text-brand-dark/80 dark:text-white/80">{t('glossaries.instructions.whatForBold')}</strong> {t('glossaries.instructions.whatForDesc')}
</p>
<p className="text-[11px] text-brand-accent/80 dark:text-brand-accent/70 font-medium mt-2 italic">
{t('glossaries.instructions.example')}
</p>
<p className="text-[10px] text-brand-accent font-bold mt-2.5 flex items-center gap-1.5 border-t border-black/5 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-brand-dark/30 dark:text-white/25 font-light">
{systemPrompt.length > 0 ? t('glossaries.instructions.charCount', { count: systemPrompt.length }) : t('glossaries.instructions.emptyHint')}
<p className="text-[10px] text-[#777777] dark:text-white/25 font-light">
{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-brand-muted dark:bg-white/5 text-brand-dark/50 dark:text-white/40 rounded-lg text-xs font-bold uppercase tracking-wider hover:text-brand-dark dark:hover:text-white transition-all cursor-pointer"
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}
@@ -430,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-brand-dark/45 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-brand-dark/50 dark:text-white/40 bg-brand-muted dark:bg-white/5 border border-black/5 dark:border-white/5 px-3 py-1.5 rounded-full">
<span>{t('glossaries.grid.activeTranslation')}</span>
<span>{currentTargetInfo.flag} {currentTargetInfo.label}</span>
</span>
)}
</div>
</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>
)}
/* ── Onglet Glossaires ──────────────────────────────────── */
<div className="space-y-6 animate-fade-in">
{glossaries.length === 0 ? (
<div className="editorial-card p-12 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm text-center">
<div className="w-12 h-12 bg-brand-muted dark:bg-white/10 rounded-xl flex items-center justify-center text-brand-accent mx-auto mb-4">
<Library size={24} />
</div>
<p className="text-base font-serif font-medium text-brand-dark dark:text-white mb-1">{t('glossaries.empty')}</p>
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light 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-brand-dark/40 dark:text-white/40 font-medium flex items-center gap-1 mt-0.5">
<span>{srcInfo?.flag ?? '🌐'}</span>
<span>{srcInfo?.label ?? glossary.source_language}</span>
<span className="text-brand-accent font-bold">→</span>
<span>{tgtInfo?.flag ?? '🌐'}</span>
<span>{tgtInfo?.label ?? glossary.target_language}</span>
</p>
</div>
</div>
<div className="flex justify-between items-center pb-4 text-xs text-brand-dark/40 dark:text-white/40">
<span className="flex items-center gap-1 font-semibold text-brand-dark/65 dark:text-white/65">
<Hash size={12} className="text-brand-accent" />
{termCount} {t('glossaries.defineTerms') || "termes"}
</span>
<span className="flex items-center gap-1 font-mono text-[9px] opacity-75">
<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-1 text-center py-2 rounded-lg bg-brand-muted/65 dark:bg-white/5 hover:bg-brand-accent/10 text-brand-dark/70 dark:text-white/60 hover:text-brand-accent text-[10px] font-bold uppercase tracking-wider transition-all"
>
Détails / Éditer
</Link>
<Link
href={`/dashboard/translate?glossaryId=${glossary.id}`}
className="flex-1 text-center py-2 rounded-lg bg-brand-accent hover:bg-brand-accent/90 text-white text-[10px] font-bold uppercase tracking-wider transition-all flex items-center justify-center gap-1.5"
>
Traduire
</Link>
</div>
</div>
);
})}
</div>
)}
</section>
{/* ── Section 2 : Créer ou Importer ─────────────────────────── */}
<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 gap-2 text-brand-accent mb-2">
<Zap size={18} />
<h2 className="text-sm font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{t('glossaries.presets.whatForBold') || 'Créer un nouveau glossaire'}
</h2>
</div>
<p className="text-xs text-brand-dark/50 dark:text-white/40 font-light">
{t('glossaries.presets.whatForDesc') || 'Choisissez un modèle professionnel pré-rempli ou importez vos propres termes.'}
{/* 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>
{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>
<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 é 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 isImported = importedTemplateIds.has(template.id);
const isProcessingThis = importingPresetId === template.id;
{/* 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>
)}
return (
<div
key={template.id}
className={cn(
'relative p-4 rounded-xl text-left border transition-all min-h-[110px] flex flex-col justify-between group',
isImported
? 'bg-emerald-500/5 dark:bg-emerald-500/5 border-emerald-500/20 dark:border-emerald-500/10 opacity-90'
: '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>
{isImported ? (
<div className="flex items-center gap-2">
<span className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20">
<CheckCircle2 size={9} /> {t('glossaries.presets.alreadyImported') || 'Importé'}
</span>
{(() => {
const matchingGlossary = glossaries.find((g) => g.template_id === template.id);
return matchingGlossary ? (
<Link
href={`/dashboard/glossaries/${matchingGlossary.id}`}
className="text-[9px] font-bold uppercase tracking-wider text-brand-accent hover:underline flex items-center gap-0.5"
>
Modifier
</Link>
) : null;
})()}
</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>
)}
{/* É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-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;
{/* 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

@@ -91,6 +91,8 @@
"glossaries.upgrade.proLabel": "Pro",
"glossaries.upgrade.upgradeBtn": "Upgrade to Pro",
"glossaries.loading": "Loading...",
"glossaries.tabs.glossaries": "Terminology Glossaries",
"glossaries.tabs.context": "Context Instructions (AI)",
"glossaries.howItWorks.title": "How these settings are used",
"glossaries.howItWorks.step1Title": "Configure here",
"glossaries.howItWorks.step1Desc": "Write your context instructions or create/import a glossary of terms.",
@@ -114,6 +116,8 @@
"glossaries.presets.clickHint": "Click a card → glossary created → select it in Translate",
"glossaries.presets.creating": "Creating...",
"glossaries.presets.alreadyImported": "Imported",
"glossaries.presets.importBtn": "Import",
"glossaries.presets.allImported": "All professional templates have been imported.",
"glossaries.presets.it.title": "IT / Software",
"glossaries.presets.it.desc": "Development, infrastructure, DevOps",
"glossaries.presets.legal.title": "Legal / Contracts",

View File

@@ -91,6 +91,8 @@
"glossaries.upgrade.proLabel": "Pro",
"glossaries.upgrade.upgradeBtn": "Passer à Pro",
"glossaries.loading": "Chargement...",
"glossaries.tabs.glossaries": "Glossaires terminologiques",
"glossaries.tabs.context": "Consignes de contexte (IA)",
"glossaries.howItWorks.title": "Comment ces paramètres sont utilisés",
"glossaries.howItWorks.step1Title": "Configurez ici",
"glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.",
@@ -114,6 +116,8 @@
"glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire",
"glossaries.presets.creating": "Création…",
"glossaries.presets.alreadyImported": "Déjà importé",
"glossaries.presets.importBtn": "Importer",
"glossaries.presets.allImported": "Tous les modèles professionnels ont été importés.",
"glossaries.presets.it.title": "IT / Logiciel",
"glossaries.presets.it.desc": "Développement, infrastructure, DevOps",
"glossaries.presets.legal.title": "Juridique / Contrats",

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
Migrate and translate all non-multilingual glossaries in the database to multilingual glossaries.
It uses the free Google Translate provider to fill in the translations for target languages
(de, es, it, pt, nl, ru, ja, ko, zh, ar, fa, en) in the glossary_terms table.
"""
import sys
import logging
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from database.connection import get_sync_session
from database.models import Glossary, GlossaryTerm
from services.providers.google_provider import get_legacy_google_adapter
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("translate_non_multilingual_glossaries")
TARGET_LANGUAGES = ["de", "es", "it", "pt", "nl", "ru", "ja", "ko", "zh", "ar", "fa"]
def translate_term(adapter, text_to_translate: str, src_lang: str, target_langs: list[str]) -> dict:
translations = {}
for lang in target_langs:
try:
translated = adapter.translate(text_to_translate, target_language=lang, source_language=src_lang)
translations[lang] = translated.strip()
except Exception as e:
logger.error(f"Failed to translate '{text_to_translate}' to {lang}: {e}")
translations[lang] = ""
return translations
def main():
logger.info("Connecting to database and fetching all glossaries...")
adapter = get_legacy_google_adapter()
with get_sync_session() as session:
# Find all glossaries
all_glossaries = session.query(Glossary).all()
if not all_glossaries:
logger.info("No glossaries found in database.")
return 0
logger.info(f"Found {len(all_glossaries)} glossaries to process.")
for glossary in all_glossaries:
logger.info(f"Processing glossary '{glossary.name}' (ID: {glossary.id})")
# Change target_language to 'multi' if not already
if glossary.target_language != 'multi':
glossary.target_language = 'multi'
# Rename the glossary to indicate it's now multilingual (e.g. replace "Anglais" with "Multilingue")
if "Anglais" in glossary.name:
glossary.name = glossary.name.replace("Anglais", "Multilingue")
elif "English" in glossary.name:
glossary.name = glossary.name.replace("English", "Multilingue")
else:
glossary.name = f"{glossary.name} → Multilingue"
src_lang = glossary.source_language or "fr"
terms = session.query(GlossaryTerm).filter(GlossaryTerm.glossary_id == glossary.id).all()
logger.info(f"Checking translations for {len(terms)} terms in '{glossary.name}'...")
updated_terms_count = 0
for idx, term in enumerate(terms):
translations = term.translations or {}
# Check if we already have translations for the target languages
missing_langs = [lang for lang in TARGET_LANGUAGES if lang not in translations or not translations[lang]]
if missing_langs:
# Translate
logger.info(f" [{idx+1}/{len(terms)}] Translating '{term.source}' to {missing_langs}...")
new_translations = translate_term(adapter, term.source, src_lang, missing_langs)
translations.update(new_translations)
updated_terms_count += 1
# Ensure the original default target (e.g. English translation) is in the translations dict under 'en'
if 'en' not in translations or not translations['en']:
translations['en'] = term.target
term.translations = translations
if updated_terms_count > 0:
session.commit()
logger.info(f"Glossary '{glossary.name}' updated with {updated_terms_count} translated terms.")
else:
logger.info(f"No translations were missing for glossary '{glossary.name}'.")
logger.info("Migration complete!")
return 0
if __name__ == '__main__':
sys.exit(main())

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"
}
}
}