feat(glossary): restructure page with tabs, direct translate redirects and dynamic mapping
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m39s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m39s
This commit is contained in:
@@ -152,6 +152,7 @@ export default function GlossariesPage() {
|
||||
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';
|
||||
const isLoading = isLoadingUser || isLoadingGlossaries;
|
||||
@@ -348,10 +349,36 @@ export default function GlossariesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10">
|
||||
{/* ── Tab Switcher ───────────────────────────────────────── */}
|
||||
<div className="flex border-b border-black/5 dark:border-white/5 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",
|
||||
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"
|
||||
)}
|
||||
>
|
||||
{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",
|
||||
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"
|
||||
)}
|
||||
>
|
||||
{t('glossaries.tabs.context') || "Consignes de contexte (IA)"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── System Prompt (Context) ─────────────────────────────── */}
|
||||
<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="space-y-10">
|
||||
{activeTab === 'context' ? (
|
||||
/* ── System Prompt (Context) ─────────────────────────────── */
|
||||
<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} />
|
||||
@@ -384,6 +411,10 @@ export default function GlossariesPage() {
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
@@ -418,124 +449,9 @@ export default function GlossariesPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Nouveau Glossaire / Création Directe ─────────────────────── */}
|
||||
<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.'}
|
||||
</p>
|
||||
</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>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{templates.map((template) => {
|
||||
const Icon = TEMPLATE_ICONS[template.id] || BookText;
|
||||
const isImported = importedTemplateIds.has(template.id);
|
||||
const isProcessingThis = importingPresetId === template.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={template.id}
|
||||
disabled={isImported || isProcessing}
|
||||
onClick={() => handleImportPreset(template.id, template.name)}
|
||||
className={cn(
|
||||
'relative p-4 rounded-xl text-left border transition-all cursor-pointer 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-80 cursor-default'
|
||||
: 'bg-brand-muted/40 dark:bg-white/5 hover:bg-brand-accent/5 dark:hover:bg-brand-accent/10 border-black/5 dark:border-white/5 hover:border-brand-accent/30'
|
||||
)}
|
||||
>
|
||||
<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-110 transition-transform">
|
||||
{isProcessingThis ? <Loader2 size={16} className="animate-spin" /> : <Icon className="size-4" />}
|
||||
</div>
|
||||
{isImported ? (
|
||||
<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>
|
||||
) : (
|
||||
<span className="accent-pill !px-2.5 !py-0.5 !text-[9px] lowercase font-light">
|
||||
{template.terms_count} {t('glossaries.defineTerms') || 'termes'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<p className="text-xs font-serif font-bold text-brand-dark dark:text-white leading-tight">
|
||||
{template.name.split(' - ')[0]}
|
||||
</p>
|
||||
<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>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fichier & Manuel (1/3 de largeur) */}
|
||||
<div className="space-y-5">
|
||||
{/* Import Fichier */}
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center gap-1.5 text-brand-accent">
|
||||
<Upload size={14} />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider">{t('glossaries.dialog.tabFile') || 'Importer un fichier'}</span>
|
||||
</div>
|
||||
<FileUploadZone
|
||||
onTermsParsed={handleFileTermsImport}
|
||||
disabled={isProcessing}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Création Manuelle */}
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center gap-1.5 text-brand-accent">
|
||||
<PenLine size={14} />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider">{t('glossaries.dialog.tabManual') || 'Création manuelle'}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
disabled={isProcessing}
|
||||
className="editorial-card w-full flex items-center justify-between p-5 text-left transition-all hover:border-brand-accent/30 hover:bg-brand-muted/30 dark:hover:bg-white/[0.02] cursor-pointer group border-black/5 dark:border-white/5"
|
||||
>
|
||||
<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} />
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Your Glossaries ────────────────────────────────────── */}
|
||||
<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>
|
||||
@@ -621,12 +537,10 @@ export default function GlossariesPage() {
|
||||
const matchesTarget = currentTargetLang && (isMultilingual || glossary.target_language === currentTargetLang);
|
||||
const mismatch = currentTargetLang && !isMultilingual && glossary.target_language && glossary.target_language !== currentTargetLang;
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
key={glossary.id}
|
||||
onClick={() => router.push(`/dashboard/glossaries/${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',
|
||||
'hover:shadow-md hover:-translate-y-0.5 cursor-pointer',
|
||||
'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
|
||||
@@ -645,12 +559,13 @@ export default function GlossariesPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-3 mb-5">
|
||||
<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">
|
||||
<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">
|
||||
@@ -663,22 +578,176 @@ export default function GlossariesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-4 border-t border-black/5 dark:border-white/10 text-xs text-brand-dark/40 dark:text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<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')}
|
||||
{termCount} {t('glossaries.defineTerms') || "termes"}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 font-mono">
|
||||
<Calendar size={12} />
|
||||
<span className="flex items-center gap-1 font-mono text-[9px] opacity-75">
|
||||
<Calendar size={11} />
|
||||
{new Date(glossary.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</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.'}
|
||||
</p>
|
||||
</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>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{templates.map((template) => {
|
||||
const Icon = TEMPLATE_ICONS[template.id] || BookText;
|
||||
const isImported = importedTemplateIds.has(template.id);
|
||||
const isProcessingThis = importingPresetId === template.id;
|
||||
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fichier & Manuel (1/3 de largeur) */}
|
||||
<div className="space-y-5">
|
||||
{/* Import Fichier */}
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center gap-1.5 text-brand-accent">
|
||||
<Upload size={14} />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider">{t('glossaries.dialog.tabFile') || 'Importer un fichier'}</span>
|
||||
</div>
|
||||
<FileUploadZone
|
||||
onTermsParsed={handleFileTermsImport}
|
||||
disabled={isProcessing}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Création Manuelle */}
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center gap-1.5 text-brand-accent">
|
||||
<PenLine size={14} />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider">{t('glossaries.dialog.tabManual') || 'Création manuelle'}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
disabled={isProcessing}
|
||||
className="editorial-card w-full flex items-center justify-between p-5 text-left transition-all hover:border-brand-accent/30 hover:bg-brand-muted/30 dark:hover:bg-white/[0.02] cursor-pointer group border-black/5 dark:border-white/5"
|
||||
>
|
||||
<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} />
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dialogs */}
|
||||
|
||||
@@ -83,9 +83,30 @@ export function useTranslationConfig(hasFile: boolean): UseTranslationConfigRetu
|
||||
// Reset glossary selection when source language changes
|
||||
// (multilingual glossaries are compatible with any target language)
|
||||
useEffect(() => {
|
||||
// If there is a query param, do not reset it on first load
|
||||
const params = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
||||
if (params.get('glossaryId') === glossaryId) return;
|
||||
setGlossaryId(null);
|
||||
}, [sourceLang]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Pre-select glossary and LLM provider from query parameter
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || isLoadingProviders || availableProviders.length === 0) return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const qGlossaryId = params.get('glossaryId');
|
||||
if (qGlossaryId) {
|
||||
setGlossaryId(qGlossaryId);
|
||||
|
||||
const p = availableProviders.find((ap) => ap.id === provider);
|
||||
if (!p || p.mode !== 'llm') {
|
||||
const firstLlm = availableProviders.find((ap) => ap.mode === 'llm');
|
||||
if (firstLlm) {
|
||||
setProvider(firstLlm.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isLoadingProviders, availableProviders, provider]);
|
||||
|
||||
// Fetch available (admin-configured) providers
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
Reference in New Issue
Block a user