From 030950c962f735a4c90c488a09e5bc22490524a6 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 28 Jun 2026 09:55:14 +0200 Subject: [PATCH] feat(glossaries): option C - page management + wizard /new 2-step creation --- .agents/skills/find-skills/SKILL.md | 142 ++++ .agents/skills/writing-guidelines/SKILL.md | 39 + .../src/app/dashboard/glossaries/new/page.tsx | 565 +++++++++++++ .../src/app/dashboard/glossaries/page.tsx | 783 ++++++------------ skills-lock.json | 12 + 5 files changed, 1005 insertions(+), 536 deletions(-) create mode 100644 .agents/skills/find-skills/SKILL.md create mode 100644 .agents/skills/writing-guidelines/SKILL.md create mode 100644 frontend/src/app/dashboard/glossaries/new/page.tsx diff --git a/.agents/skills/find-skills/SKILL.md b/.agents/skills/find-skills/SKILL.md new file mode 100644 index 0000000..7369d66 --- /dev/null +++ b/.agents/skills/find-skills/SKILL.md @@ -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 ]` - Search for skills interactively or by keyword, optionally scoped to a GitHub owner +- `npx skills add ` - 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 ] +``` + +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 -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 +``` diff --git a/.agents/skills/writing-guidelines/SKILL.md b/.agents/skills/writing-guidelines/SKILL.md new file mode 100644 index 0000000..63facf3 --- /dev/null +++ b/.agents/skills/writing-guidelines/SKILL.md @@ -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: +--- + +# 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. diff --git a/frontend/src/app/dashboard/glossaries/new/page.tsx b/frontend/src/app/dashboard/glossaries/new/page.tsx new file mode 100644 index 0000000..45201eb --- /dev/null +++ b/frontend/src/app/dashboard/glossaries/new/page.tsx @@ -0,0 +1,565 @@ +'use client'; + +import { useState, useRef } from 'react'; +import { + BookText, Upload, PenLine, ArrowRight, ArrowLeft, CheckCircle2, + AlertCircle, Loader2, Scale, Cpu, TrendingUp, HeartPulse, + Megaphone, Users, FlaskConical, ShoppingCart, X, Plus +} from 'lucide-react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { cn } from '@/lib/utils'; +import { useI18n } from '@/lib/i18n'; +import { useGlossaries, useGlossaryTemplates } from '../useGlossaries'; +import { SUPPORTED_LANGUAGES } from '../types'; +import type { GlossaryTermInput } from '../types'; +import { parseFileToTerms } from '../csvUtils'; +import { useToast } from '@/components/ui/toast'; + +// ── Icônes par catégorie de modèle ───────────────────────────────────────── +const TEMPLATE_ICONS: Record> = { + 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(1); + const [method, setMethod] = useState(null); + + // État pour modèle + const [selectedTemplateId, setSelectedTemplateId] = useState(null); + const [isImportingThis, setIsImportingThis] = useState(false); + + // État pour fichier + const fileInputRef = useRef(null); + const [fileStatus, setFileStatus] = useState<'idle' | 'parsing' | 'success' | 'error'>('idle'); + const [fileError, setFileError] = useState(''); + const [parsedTerms, setParsedTerms] = useState([]); + const [parsedFileName, setParsedFileName] = useState(''); + const [isDragging, setIsDragging] = useState(false); + + // État pour manuel + const [manualName, setManualName] = useState(''); + const [manualSrc, setManualSrc] = useState('fr'); + const [manualTgt, setManualTgt] = useState('multi'); + + const isProcessing = isCreating || isImportingTemplate || isImportingThis; + + // ── Handlers ──────────────────────────────────────────────────────────── + + const handleSelectMethod = (m: CreationMethod) => { + setMethod(m); + setStep(2); + // reset + setSelectedTemplateId(null); + setFileStatus('idle'); + setParsedTerms([]); + setParsedFileName(''); + setManualName(''); + }; + + const handleBack = () => { + setStep(1); + setMethod(null); + }; + + // Importer un modèle + const handleImportTemplate = async () => { + if (!selectedTemplateId) return; + const tpl = templates.find(t => t.id === selectedTemplateId); + setIsImportingThis(true); + try { + await importTemplate(selectedTemplateId, tpl?.name); + toast({ title: 'Glossaire importé !', description: `Le modèle « ${tpl?.name} » a été ajouté à vos glossaires.` }); + router.push('/dashboard/glossaries'); + } catch { + toast({ variant: 'destructive', title: 'Erreur', description: 'Impossible d\'importer ce modèle.' }); + } finally { + setIsImportingThis(false); + } + }; + + // Parser un fichier CSV + const processFile = async (file: File) => { + const ext = file.name.split('.').pop()?.toLowerCase(); + if (!ext || !['csv', 'xlsx', 'xls', 'ods', 'txt', 'tsv'].includes(ext)) { + setFileStatus('error'); + setFileError('Format non supporté. Utilisez CSV, Excel (.xlsx), ODS, ou TSV.'); + return; + } + if (file.size > 5 * 1024 * 1024) { + setFileStatus('error'); + setFileError('Fichier trop volumineux (maximum 5 MB).'); + return; + } + setFileStatus('parsing'); + setFileError(''); + try { + const terms = await parseFileToTerms(file); + if (terms.length === 0) { + setFileStatus('error'); + setFileError('Le fichier est vide ou n\'a pas pu être lu.'); + return; + } + setFileStatus('success'); + setParsedTerms(terms); + setParsedFileName(file.name.replace(/\.[^.]+$/, '').replace(/[_-]/g, ' ')); + } catch { + setFileStatus('error'); + setFileError('Erreur lors de la lecture du fichier.'); + } + }; + + // Créer depuis fichier + const handleCreateFromFile = async () => { + if (!parsedTerms.length) return; + try { + await createGlossary({ name: parsedFileName || 'Mon glossaire', source_language: 'fr', target_language: 'multi', terms: parsedTerms }); + toast({ title: 'Glossaire créé !', description: `${parsedTerms.length} termes importés depuis votre fichier.` }); + router.push('/dashboard/glossaries'); + } catch { + toast({ variant: 'destructive', title: 'Erreur', description: 'Impossible de créer le glossaire.' }); + } + }; + + // Créer manuellement + const handleCreateManual = async () => { + if (!manualName.trim()) return; + try { + await createGlossary({ name: manualName.trim(), source_language: manualSrc, target_language: manualTgt, terms: [] }); + toast({ title: 'Glossaire créé !', description: `« ${manualName} » est prêt. Ajoutez vos premiers termes.` }); + router.push('/dashboard/glossaries'); + } catch { + toast({ variant: 'destructive', title: 'Erreur', description: 'Impossible de créer le glossaire.' }); + } + }; + + // ── Rendu ─────────────────────────────────────────────────────────────── + + return ( +
+ + {/* ── Fil d'Ariane ──────────────────────────────────────────── */} +
+ + Mes glossaires + + + Nouveau glossaire +
+ + {/* ── Indicateur d'étapes ────────────────────────────────────── */} +
+ {/* Étape 1 */} +
+
= 1 ? 'bg-[#1A1A1A] text-white' : 'bg-[#EBEBEB] text-[#888888]' + )}> + {step > 1 ? : '1'} +
+ + Choisir le type + +
+ +
+ + {/* Étape 2 */} +
+
+ 2 +
+ + Configurer + +
+
+ + {/* ══════════════ ÉTAPE 1 : Choisir le type ══════════════════ */} + {step === 1 && ( +
+
+

+ Comment voulez-vous créer votre{' '} + glossaire ? +

+

+ Choisissez la méthode qui correspond à votre situation. +

+
+ +
+ + {/* Option 1 — Modèle professionnel */} + + + {/* Option 2 — Fichier CSV */} + + + {/* Option 3 — Manuel */} + +
+
+ )} + + {/* ══════════════ ÉTAPE 2 : Configurer ═══════════════════════ */} + {step === 2 && ( +
+ + {/* Bouton retour */} + + + {/* ── CAS A : Depuis un modèle ─── */} + {method === 'template' && ( +
+

+ Choisissez un modèle professionnel +

+

+ Sélectionnez le modèle le plus adapté à vos documents. Vous pourrez modifier les termes après l'import. +

+ + {isLoadingTemplates ? ( +
+ +
+ ) : templates.length === 0 ? ( +
+

+ Tous les modèles disponibles ont déjà été importés. +

+ +
+ ) : ( + <> +
+ {templates.map((tpl) => { + const Icon = TEMPLATE_ICONS[tpl.id] || BookText; + const isSelected = selectedTemplateId === tpl.id; + return ( + + ); + })} +
+ + {/* Bouton d'import */} +
+ {selectedTemplateId && ( +

+ Modèle sélectionné : {templates.find(t => t.id === selectedTemplateId)?.name.split(' - ')[0]} +

+ )} + +
+ + )} +
+ )} + + {/* ── CAS B : Depuis un fichier ─── */} + {method === 'file' && ( +
+

+ Importez votre fichier de termes +

+

+ Formats acceptés : CSV, Excel (.xlsx), ODS, TSV — maximum 5 MB. +

+ + {/* Format attendu */} +
+

Format CSV attendu :

+ + terme_source,terme_cible
+ contrat,contract
+ résiliation,termination +
+
+ + {/* Zone de dépôt */} +
{ e.preventDefault(); setIsDragging(true); }} + onDragLeave={() => setIsDragging(false)} + onDrop={e => { e.preventDefault(); setIsDragging(false); const f = e.dataTransfer.files[0]; if (f) processFile(f); }} + onClick={() => fileInputRef.current?.click()} + className={cn( + 'flex flex-col items-center justify-center gap-4 p-12 rounded-2xl border-2 border-dashed cursor-pointer transition-all min-h-[200px]', + isDragging ? 'border-[#C5A17A] bg-[#F5F0EA]' : 'border-[#D9D6D0] dark:border-white/10 hover:border-[#C5A17A] hover:bg-[#FAFAF8]', + fileStatus === 'error' && 'border-red-400 bg-red-50 dark:bg-red-500/5' + )} + > + { const f = e.target.files?.[0]; if (f) processFile(f); e.target.value = ''; }} + /> + {fileStatus === 'parsing' && ( + <> + +

Lecture du fichier…

+ + )} + {fileStatus === 'success' && ( + <> + +
+

{parsedTerms.length} termes détectés

+

Cliquez pour changer de fichier

+
+ + )} + {fileStatus === 'error' && ( + <> + +
+

{fileError}

+

Cliquez pour réessayer

+
+ + )} + {fileStatus === 'idle' && ( + <> +
+ +
+
+

+ Glissez votre fichier ici +

+

+ ou cliquez pour parcourir +

+
+ + )} +
+ + {/* Nom du glossaire (si fichier parsé) */} + {fileStatus === 'success' && ( +
+ + setParsedFileName(e.target.value)} + placeholder="Ex : Termes RH internes" + className="w-full px-4 py-3 rounded-xl border border-[#D9D6D0] dark:border-white/10 bg-white dark:bg-[#141414] text-[#1A1A1A] dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50" + /> +
+ )} + +
+ +
+
+ )} + + {/* ── CAS C : Manuel ─── */} + {method === 'manual' && ( +
+

+ Créez votre glossaire manuellement +

+

+ Donnez un nom à votre glossaire et choisissez les langues. Vous ajouterez les termes dans l'éditeur. +

+ +
+ + {/* Nom */} +
+ + 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" + /> +
+ + {/* Langues */} +
+
+ + +
+
+ + +
+
+ + {/* Info */} +
+

+ Que se passe-t-il ensuite ?{' '} + Un glossaire vide sera créé. Vous serez redirigé vers l'éditeur pour ajouter vos termes. +

+
+
+ +
+ +
+
+ )} + +
+ )} +
+ ); +} diff --git a/frontend/src/app/dashboard/glossaries/page.tsx b/frontend/src/app/dashboard/glossaries/page.tsx index 9471817..c529e3e 100644 --- a/frontend/src/app/dashboard/glossaries/page.tsx +++ b/frontend/src/app/dashboard/glossaries/page.tsx @@ -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> = { - 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(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 ( -
{ 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' - )} - > - { const file = e.target.files?.[0]; if (file) processFile(file); e.target.value = ''; }} - disabled={disabled} - /> - {status === 'parsing' ? ( - <> - -

{t('glossaries.dialog.parsing') || 'Analyse…'}

- - ) : status === 'error' ? ( - <> - -

{errorMsg}

- {t('glossaries.dialog.retry') || 'Réessayer'} - - ) : status === 'success' ? ( - <> - -

{t('glossaries.toast.imported') || 'Importé avec succès'}

- - ) : ( - <> -
- -
-
-

{t('glossaries.dialog.tabFile') || 'Glissez un fichier CSV'}

-

{t('glossaries.dialog.dropFormats') || 'Format supporté: CSV, Excel'}

-
- - )} -
+ <> + {title.substring(0, lastSpaceIndex)}{' '} + {title.substring(lastSpaceIndex + 1)} + ); } @@ -133,27 +43,15 @@ export default function GlossariesPage() { const { t } = useI18n(); const router = useRouter(); const { data: user, isLoading: isLoadingUser } = useUser(); - const { - glossaries, - total, - isLoading: isLoadingGlossaries, - isCreating, - isImportingTemplate, - createGlossary, - importTemplate, - } = useGlossaries(); - const { templates, isLoading: isLoadingTemplates } = useGlossaryTemplates(); + const { glossaries, isLoading: isLoadingGlossaries } = useGlossaries(); const { toast } = useToast(); const { settings, updateSettings } = useTranslationStore(); - const [createDialogOpen, setCreateDialogOpen] = useState(false); const [systemPrompt, setSystemPrompt] = useState(settings.systemPrompt); const [isSavingPrompt, setIsSavingPrompt] = useState(false); const [promptSaved, setPromptSaved] = useState(false); const [searchQuery, setSearchQuery] = useState(''); - const [importingPresetId, setImportingPresetId] = useState(null); const [activeTab, setActiveTab] = useState<'glossaries' | 'context'>('glossaries'); - const [showCreateSection, setShowCreateSection] = useState(false); const isPro = user?.tier === 'pro'; const isLoading = isLoadingUser || isLoadingGlossaries; @@ -187,79 +85,12 @@ export default function GlossariesPage() { setSystemPrompt(''); }; - const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => { - try { - await createGlossary(data); - setCreateDialogOpen(false); - toast({ - title: t('glossaries.toast.created'), - description: t('glossaries.toast.createdDesc', { name: data.name }), - }); - } catch (error) { - toast({ - variant: 'destructive', - title: t('glossaries.toast.error'), - description: t('glossaries.toast.errorCreate'), - }); - throw error; - } + const handleAddSuggestion = (value: string) => { + const current = systemPrompt.trim(); + const newPrompt = current ? `${current}\n${value}` : value; + setSystemPrompt(newPrompt); }; - const handleImportPreset = async (templateId: string, name?: string) => { - setImportingPresetId(templateId); - try { - await importTemplate(templateId, name); - toast({ - title: t('glossaries.toast.imported'), - description: name - ? t('glossaries.toast.importedDesc', { name }) - : t('glossaries.toast.importedDesc', { name: templateId }), - }); - } catch (error) { - toast({ - variant: 'destructive', - title: t('glossaries.toast.error'), - description: t('glossaries.toast.errorImport'), - }); - } finally { - setImportingPresetId(null); - } - }; - - const handleFileTermsImport = async (parsedTerms: GlossaryTermInput[], filename: string) => { - try { - const baseName = filename.replace(/\.[^.]+$/, '').replace(/[_-]/g, ' '); - await createGlossary({ - name: baseName, - source_language: 'fr', - target_language: 'multi', - terms: parsedTerms, - }); - toast({ - title: t('glossaries.toast.created'), - description: t('glossaries.toast.createdDesc', { name: baseName }), - }); - } catch (error) { - toast({ - variant: 'destructive', - title: t('glossaries.toast.error'), - description: t('glossaries.toast.errorCreate'), - }); - } - }; - - const importedTemplateIds = useMemo(() => { - return new Set( - glossaries - .map((g: GlossaryListItem) => g.template_id) - .filter(Boolean) as string[] - ); - }, [glossaries]); - - const availableTemplates = useMemo(() => { - return templates.filter(t => !importedTemplateIds.has(t.id)); - }, [templates, importedTemplateIds]); - const filteredGlossaries = useMemo(() => { if (!searchQuery.trim()) return glossaries; const q = searchQuery.toLowerCase(); @@ -271,7 +102,7 @@ export default function GlossariesPage() {
-

{t('glossaries.loading')}

+

{t('glossaries.loading')}

); @@ -281,34 +112,41 @@ export default function GlossariesPage() { return ; } - const isProcessing = isCreating || isImportingTemplate || !!importingPresetId; - return (
- {/* ── Editorial Header ───────────────────────────────────── */} + {/* ── Header ────────────────────────────────────────────────── */}
- {t('glossaries.yourGlossaries') || "Vos Glossaires"} + {t('glossaries.yourGlossaries') || 'Vos Glossaires'}

- {renderTitle(t('glossaries.title') || "Glossaires & Contexte")} + {renderTitle(t('glossaries.title') || 'Glossaires & Contexte')}

- {t('glossaries.description') || "Gérez vos glossaires et instructions de contexte pour des traductions plus précises."} + {t('glossaries.description') || 'Gérez vos glossaires et instructions de contexte pour des traductions plus précises.'}

+ {/* Bouton principal — unique point d'entrée pour créer */} + + + Nouveau glossaire +
- {/* ── Bandeau usage ──────────────────────────────────── */} + {/* ── Bandeau informatif ────────────────────────────────────── */}

Pour utiliser un glossaire dans une traduction :{' '} - cliquez sur « Utiliser » sur la carte souhaitée. Vous serez redirigé sur la page Traduire avec ce glossaire déjà sélectionné. + cliquez sur « Utiliser » sur la carte souhaitée. + Vous serez redirigé sur la page Traduire avec ce glossaire déjà sélectionné.

- {/* ── Tab Switcher ───────────────────────────────────────── */} -
+ {/* ── Onglets ───────────────────────────────────────────────── */} +
{activeTab === 'context' ? ( - /* ── System Prompt (Context) ─────────────────────────────── */ + + /* ── Onglet Consignes de Contexte ─────────────────────── */
-
-
- -

- {t('context.instructions.title')} -

+ + {/* En-tête section */} +
+
+
+ +
+
+

+ Consignes de contexte +

+

+ Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction en mode Pro LLM. +

+
+ {/* Badge d'état */} {promptHasUnsavedChanges ? ( - + - {t('glossaries.status.unsaved')} + Non sauvegardé ) : promptIsActive ? ( - + - {t('glossaries.status.active')} + Actif ) : ( - - {t('glossaries.status.inactive')} + + Inactif )}
- {/* Explanation box */} -
-

- {t('glossaries.instructions.whatForBold')} {t('glossaries.instructions.whatForDesc')} -

-

- {t('glossaries.instructions.example')} -

-

- - Remarque : Ces consignes s'appliquent automatiquement à toutes vos traductions réalisées en mode Pro LLM. -

+ {/* Chips de suggestions rapides */} +
+
+ + + Suggestions rapides — cliquez pour ajouter + +
+
+ {CONTEXT_SUGGESTIONS.map((s) => ( + + ))} +
+ {/* Zone de texte */}