style(translate): make LLM cards compact in a 3-column grid, remove glossary language filtering, and add contextual help under engine selection
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m58s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m58s
This commit is contained in:
@@ -222,11 +222,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
|
||||
const sourceFlag = SUPPORTED_LANGUAGES.find(l => l.code === sourceLang)?.flag ?? '';
|
||||
const targetFlag = SUPPORTED_LANGUAGES.find(l => l.code === targetLang)?.flag ?? '';
|
||||
|
||||
const langFiltered = sourceLang === 'auto'
|
||||
? glossaries
|
||||
: glossaries.filter(g => g.source_language === sourceLang);
|
||||
|
||||
const filteredGlossaries = langFiltered.length > 0 ? langFiltered : glossaries;
|
||||
const filteredGlossaries = glossaries;
|
||||
const selected = glossaries.find(g => g.id === glossaryId);
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -135,7 +135,6 @@ export function ProviderSelector({
|
||||
const isSelected = provider === p.id;
|
||||
const meta = CLASSIC_THEMES[p.id];
|
||||
const label = meta?.labelOverride || p.label;
|
||||
const description = meta?.descriptionOverride || p.description;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -143,37 +142,24 @@ export function ProviderSelector({
|
||||
type="button"
|
||||
onClick={() => onProviderChange(p.id)}
|
||||
className={cn(
|
||||
'relative overflow-hidden w-full text-start rounded-2xl border p-4 transition-all duration-300 active:scale-[0.99] flex items-start gap-3.5',
|
||||
'p-2 py-1.5 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-center min-h-[44px]',
|
||||
isSelected
|
||||
? 'border-brand-accent bg-brand-muted/20 dark:bg-zinc-800/40 ring-1 ring-brand-accent/20'
|
||||
: 'border-brand-dark/10 dark:border-white/10 bg-white dark:bg-[#141414] hover:border-brand-accent/30 dark:hover:border-brand-accent/30 hover:scale-[1.005]'
|
||||
? 'border-brand-accent bg-brand-accent/5 dark:bg-brand-accent/10'
|
||||
: 'border-black/[0.05] dark:border-white/[0.05] bg-brand-muted/20 dark:bg-zinc-800/10 hover:bg-brand-muted/50 dark:hover:bg-zinc-800/20'
|
||||
)}
|
||||
>
|
||||
{/* Radio Indicator */}
|
||||
<div className={cn(
|
||||
'mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors',
|
||||
isSelected ? 'border-brand-accent bg-brand-accent' : 'border-brand-dark/20 dark:border-white/20'
|
||||
)}>
|
||||
{isSelected && <div className="size-1.5 rounded-full bg-white" />}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={cn(
|
||||
'text-xs font-semibold leading-tight tracking-tight',
|
||||
isSelected ? 'text-brand-dark dark:text-white' : 'text-brand-dark/80 dark:text-white/80'
|
||||
"text-[8px] font-black uppercase tracking-tight truncate flex-1",
|
||||
isSelected ? 'text-brand-accent' : 'text-brand-dark/40 dark:text-white/40'
|
||||
)}>
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-[11px] text-brand-dark/50 dark:text-white/50 leading-relaxed font-light">
|
||||
{description}
|
||||
Standard
|
||||
</span>
|
||||
{isSelected && <div className="w-1.5 h-1.5 rounded-full bg-brand-accent animate-pulse shrink-0 ml-1" />}
|
||||
</div>
|
||||
|
||||
{/* Selected badge */}
|
||||
{isSelected && (
|
||||
<CheckCircle2 className="size-4 shrink-0 text-brand-accent" />
|
||||
)}
|
||||
<span className="text-[10px] font-bold text-brand-dark dark:text-white block leading-tight truncate">
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -181,7 +167,6 @@ export function ProviderSelector({
|
||||
const renderLlmCard = (p: AvailableProvider, locked: boolean) => {
|
||||
const isSelected = provider === p.id;
|
||||
const theme = LLM_THEMES[p.id] || DEFAULT_LLM_THEME;
|
||||
const description = theme.descriptionOverride || p.description;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -190,73 +175,36 @@ export function ProviderSelector({
|
||||
disabled={locked}
|
||||
onClick={() => !locked && onProviderChange(p.id)}
|
||||
className={cn(
|
||||
'group relative overflow-hidden w-full text-start rounded-2xl border p-4 transition-all duration-300 flex flex-col gap-2.5',
|
||||
'p-2 py-1.5 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-between min-h-[58px]',
|
||||
isSelected
|
||||
? 'border-brand-accent bg-gradient-to-br from-brand-muted/30 via-white to-brand-accent/[0.03] dark:via-[#141414] dark:to-brand-accent/[0.05] ring-1 ring-brand-accent/20 scale-[1.01]'
|
||||
? 'border-brand-accent bg-brand-accent/5 dark:bg-brand-accent/10'
|
||||
: locked
|
||||
? 'cursor-not-allowed border-brand-dark/5 dark:border-white/5 bg-brand-dark/[0.01] dark:bg-white/[0.01] opacity-50'
|
||||
: 'border-brand-dark/10 dark:border-white/10 bg-white dark:bg-[#141414] hover:border-brand-accent/30 dark:hover:border-brand-accent/30 hover:scale-[1.005]'
|
||||
: 'border-black/[0.05] dark:border-white/[0.05] bg-brand-muted/20 dark:bg-zinc-800/10 hover:bg-brand-muted/50 dark:hover:bg-zinc-800/20'
|
||||
)}
|
||||
>
|
||||
{/* Glow effect when selected or hovered */}
|
||||
<div className={cn(
|
||||
'absolute inset-0 bg-gradient-to-r opacity-0 transition-opacity duration-300 pointer-events-none',
|
||||
theme.glowClass,
|
||||
isSelected ? 'opacity-100' : 'group-hover:opacity-40'
|
||||
)} />
|
||||
|
||||
{/* Top line with label and category badge */}
|
||||
<div className="relative flex items-center justify-between gap-2 z-10">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className={cn(
|
||||
'text-xs font-semibold tracking-tight',
|
||||
isSelected ? 'text-brand-dark dark:text-white' : locked ? 'text-brand-dark/40 dark:text-white/40' : 'text-brand-dark/80 dark:text-white/80'
|
||||
)}>
|
||||
{p.label}
|
||||
</span>
|
||||
<span className={cn(
|
||||
'text-[9px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full border',
|
||||
theme.accentClass
|
||||
"text-[8px] font-black uppercase tracking-tight truncate flex-1",
|
||||
isSelected ? 'text-brand-accent' : 'text-brand-dark/40 dark:text-white/40'
|
||||
)}>
|
||||
{theme.badge}
|
||||
</span>
|
||||
{isSelected && <div className="w-1.5 h-1.5 rounded-full bg-brand-accent animate-pulse shrink-0 ml-1" />}
|
||||
{locked && <Lock className="size-2 text-brand-dark/40 dark:text-white/40 shrink-0 ml-1" />}
|
||||
</div>
|
||||
|
||||
{/* Middle description */}
|
||||
<div className="relative z-10 flex flex-col gap-1">
|
||||
<span className="text-[9px] font-bold tracking-widest text-brand-dark/40 dark:text-white/30 uppercase">
|
||||
{theme.subBadge}
|
||||
</span>
|
||||
<p className="text-[11px] text-brand-dark/65 dark:text-white/60 leading-relaxed font-light">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom meta & status */}
|
||||
<div className="relative z-10 flex items-center justify-between mt-1 pt-2 border-t border-brand-dark/5 dark:border-white/5">
|
||||
<span className="text-[9px] font-mono text-brand-dark/40 dark:text-white/40 tracking-tight">
|
||||
{p.model || 'model-default'}
|
||||
</span>
|
||||
|
||||
{locked ? (
|
||||
<span className="flex items-center gap-1 text-[9px] font-bold text-brand-dark/45 dark:text-white/45 uppercase tracking-widest">
|
||||
<Lock className="size-3 text-brand-dark/40 dark:text-white/40" /> PRO
|
||||
</span>
|
||||
) : isSelected ? (
|
||||
<span className="flex items-center gap-1 text-[9px] font-bold text-brand-accent uppercase tracking-widest">
|
||||
<CheckCircle2 className="size-3 text-brand-accent" /> ACTIF
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[9px] text-brand-dark/35 dark:text-white/35 uppercase tracking-widest group-hover:text-brand-accent transition-colors font-medium">
|
||||
Sélectionner
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-brand-dark dark:text-white block leading-none mb-0.5 truncate">
|
||||
{p.label}
|
||||
</span>
|
||||
<span className="text-[7.5px] text-brand-dark/40 dark:text-white/45 uppercase font-bold block leading-none truncate">
|
||||
{theme.subBadge}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Title */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-bold uppercase tracking-[0.2em] text-brand-dark/40 dark:text-white/45">
|
||||
@@ -270,7 +218,7 @@ export function ProviderSelector({
|
||||
type="button"
|
||||
onClick={() => setActiveTab('classic')}
|
||||
className={cn(
|
||||
'py-2 text-xs font-semibold rounded-lg transition-all duration-200',
|
||||
'py-1.5 text-xs font-semibold rounded-lg transition-all duration-200',
|
||||
activeTab === 'classic'
|
||||
? 'bg-white dark:bg-zinc-900 text-brand-dark dark:text-white shadow-sm'
|
||||
: 'text-brand-dark/50 dark:text-white/40 hover:text-brand-dark dark:hover:text-white'
|
||||
@@ -282,7 +230,7 @@ export function ProviderSelector({
|
||||
type="button"
|
||||
onClick={() => setActiveTab('llm')}
|
||||
className={cn(
|
||||
'py-2 text-xs font-semibold rounded-lg transition-all duration-200 flex items-center justify-center gap-1.5',
|
||||
'py-1.5 text-xs font-semibold rounded-lg transition-all duration-200 flex items-center justify-center gap-1.5',
|
||||
activeTab === 'llm'
|
||||
? 'bg-white dark:bg-zinc-900 text-brand-dark dark:text-white shadow-sm'
|
||||
: 'text-brand-dark/50 dark:text-white/40 hover:text-brand-dark dark:hover:text-white'
|
||||
@@ -295,43 +243,72 @@ export function ProviderSelector({
|
||||
</div>
|
||||
|
||||
{/* Active Tab List */}
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
{activeTab === 'classic' ? (
|
||||
classicProviders.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-2.5">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{classicProviders.map((p) => renderClassicCard(p))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-6 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-2xl font-light">
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-4 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-xl font-light">
|
||||
Aucun traducteur standard disponible.
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
llmProviders.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{llmProviders.map((p) => renderLlmCard(p, !isPro))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-6 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-2xl font-light">
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/30 text-center py-4 border border-dashed border-brand-dark/10 dark:border-white/10 rounded-xl font-light">
|
||||
Aucun modèle IA configuré.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Dynamic Contextual Help Area */}
|
||||
{provider && (
|
||||
<div className="mt-1 p-2.5 rounded-lg bg-brand-muted/20 dark:bg-white/[0.01] border border-black/[0.03] dark:border-white/[0.03]">
|
||||
{activeTab === 'classic' ? (
|
||||
(() => {
|
||||
const activeP = classicProviders.find(p => p.id === provider);
|
||||
const meta = activeP ? CLASSIC_THEMES[activeP.id] : null;
|
||||
return activeP ? (
|
||||
<p className="text-[10px] text-brand-dark/60 dark:text-white/60 leading-normal font-light">
|
||||
<span className="font-bold text-brand-dark dark:text-white">{meta?.labelOverride || activeP.label} : </span>
|
||||
{meta?.descriptionOverride || activeP.description}
|
||||
</p>
|
||||
) : null;
|
||||
})()
|
||||
) : (
|
||||
(() => {
|
||||
const activeP = llmProviders.find(p => p.id === provider);
|
||||
const theme = activeP ? (LLM_THEMES[activeP.id] || DEFAULT_LLM_THEME) : null;
|
||||
return activeP && theme ? (
|
||||
<p className="text-[10px] text-brand-dark/60 dark:text-white/60 leading-normal font-light">
|
||||
<span className="font-bold text-brand-dark dark:text-white">{activeP.label} : </span>
|
||||
{theme.descriptionOverride} <span className="opacity-50 text-[9px] font-mono">({activeP.model || 'default'})</span>
|
||||
</p>
|
||||
) : null;
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pro upgrade banner when llm is active and user is not pro */}
|
||||
{!isPro && activeTab === 'llm' && (
|
||||
<div className="p-4 rounded-2xl border border-brand-accent/20 bg-brand-accent/[0.02] dark:bg-brand-accent/[0.01] text-center flex flex-col items-center gap-2">
|
||||
<Sparkles className="size-4 text-brand-accent animate-pulse" />
|
||||
<span className="text-xs font-semibold text-brand-dark dark:text-white">
|
||||
<div className="p-3.5 rounded-xl border border-brand-accent/20 bg-brand-accent/[0.02] dark:bg-brand-accent/[0.01] text-center flex flex-col items-center gap-1.5">
|
||||
<Sparkles className="size-3.5 text-brand-accent animate-pulse" />
|
||||
<span className="text-[11px] font-bold text-brand-dark dark:text-white leading-none">
|
||||
{t('dashboard.translate.provider.llmDivider') || 'Intelligence Artificielle Active'}
|
||||
</span>
|
||||
<p className="text-[10.5px] text-brand-dark/50 dark:text-white/50 leading-relaxed max-w-[280px] font-light">
|
||||
Débloquez la traduction contextuelle haut de gamme pour des documents entiers tout en préservant le ton exact.
|
||||
<p className="text-[9.5px] text-brand-dark/50 dark:text-white/50 leading-relaxed font-light">
|
||||
Débloquez la traduction contextuelle haut de gamme pour vos documents entiers.
|
||||
</p>
|
||||
<a
|
||||
href="/pricing"
|
||||
className="mt-1 w-full inline-flex items-center justify-center py-2 px-3 rounded-xl bg-brand-dark dark:bg-white text-white dark:text-brand-dark text-xs font-medium hover:opacity-95 active:scale-[0.98] transition-all shadow-sm"
|
||||
className="w-full inline-flex items-center justify-center py-1.5 rounded-lg bg-brand-dark dark:bg-white text-white dark:text-brand-dark text-[10px] font-semibold hover:opacity-95 active:scale-[0.98] transition-all shadow-sm"
|
||||
>
|
||||
{t('dashboard.translate.provider.upgrade') || 'Passer Pro'}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user