'use client'; import { useState, useEffect } from 'react'; import { Plus, MessageSquareText, Cpu, ChevronDown, ChevronRight, Trash2, Settings, LogIn, Eye, Wrench, } from 'lucide-react'; import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; import { LLMModel, EmbeddingModel, ModelProvider, } from '@/app/infra/entities/api'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import LLMForm from './component/llm-form/LLMForm'; import EmbeddingForm from './component/embedding-form/EmbeddingForm'; import ProviderForm from './component/provider-form/ProviderForm'; import langbotIcon from '@/app/assets/langbot-logo.webp'; interface ModelsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } const LANGBOT_MODELS_PROVIDER_NAME = 'LangBot Models'; const LANGBOT_MODELS_PROVIDER_REQUESTER = 'space-chat-completions'; export default function ModelsDialog({ open, onOpenChange, }: ModelsDialogProps) { const { t } = useTranslation(); const [providers, setProviders] = useState([]); const [accountType, setAccountType] = useState<'local' | 'space'>('local'); const [spaceCredits, setSpaceCredits] = useState(null); // Expanded providers and their models const [expandedProviders, setExpandedProviders] = useState>( new Set(), ); const [providerModels, setProviderModels] = useState< Record >({}); const [loadingProviders, setLoadingProviders] = useState>( new Set(), ); // Form modals const [llmFormOpen, setLLMFormOpen] = useState(false); const [embeddingFormOpen, setEmbeddingFormOpen] = useState(false); const [providerFormOpen, setProviderFormOpen] = useState(false); const [editingLLMId, setEditingLLMId] = useState(null); const [editingEmbeddingId, setEditingEmbeddingId] = useState( null, ); const [editingProviderId, setEditingProviderId] = useState( null, ); const [requesterNameList, setRequesterNameList] = useState< { label: string; value: string }[] >([]); useEffect(() => { if (open) { loadUserInfo(); loadRequesterLists(); loadProviders(); } }, [open]); async function loadUserInfo() { try { const userInfo = await httpClient.getUserInfo(); setAccountType(userInfo.account_type); if (userInfo.account_type === 'space') { const creditsInfo = await httpClient.getSpaceCredits(); setSpaceCredits(creditsInfo.credits); } } catch { setAccountType('local'); } } async function loadRequesterLists() { try { const llmRequesters = await httpClient.getProviderRequesters('llm'); setRequesterNameList( llmRequesters.requesters.map((item) => ({ label: extractI18nObject(item.label), value: item.name, })), ); } catch (err) { console.error('Failed to load requester lists', err); } } async function loadProviders() { try { const resp = await httpClient.getModelProviders(); setProviders(resp.providers); } catch (err) { console.error('Failed to load providers', err); toast.error(t('models.loadError')); } } async function loadProviderModels(providerUuid: string) { if (loadingProviders.has(providerUuid)) return; setLoadingProviders((prev) => new Set(prev).add(providerUuid)); try { const [llmResp, embeddingResp] = await Promise.all([ httpClient.getProviderLLMModels(providerUuid), httpClient.getProviderEmbeddingModels(providerUuid), ]); setProviderModels((prev) => ({ ...prev, [providerUuid]: { llm: llmResp.models, embedding: embeddingResp.models, }, })); } catch (err) { console.error('Failed to load models', err); } finally { setLoadingProviders((prev) => { const next = new Set(prev); next.delete(providerUuid); return next; }); } } function toggleProvider(providerUuid: string) { setExpandedProviders((prev) => { const next = new Set(prev); if (next.has(providerUuid)) { next.delete(providerUuid); } else { next.add(providerUuid); if (!providerModels[providerUuid]) { loadProviderModels(providerUuid); } } return next; }); } function handleCreateLLM() { setEditingLLMId(null); setLLMFormOpen(true); } function handleCreateEmbedding() { setEditingEmbeddingId(null); setEmbeddingFormOpen(true); } function handleEditLLM(modelId: string) { setEditingLLMId(modelId); setLLMFormOpen(true); } function handleEditEmbedding(modelId: string) { setEditingEmbeddingId(modelId); setEmbeddingFormOpen(true); } function handleEditProvider(providerId: string) { setEditingProviderId(providerId); setProviderFormOpen(true); } async function handleDeleteProvider(providerId: string) { try { await httpClient.deleteModelProvider(providerId); toast.success(t('models.providerDeleted')); loadProviders(); } catch (err) { toast.error(t('models.providerDeleteError') + (err as Error).message); } } async function handleDeleteLLM(modelId: string, providerUuid: string) { try { await httpClient.deleteProviderLLMModel(modelId); toast.success(t('models.deleteSuccess')); loadProviderModels(providerUuid); loadProviders(); // Refresh counts } catch (err) { toast.error(t('models.deleteError') + (err as Error).message); } } async function handleDeleteEmbedding(modelId: string, providerUuid: string) { try { await httpClient.deleteProviderEmbeddingModel(modelId); toast.success(t('models.deleteSuccess')); loadProviderModels(providerUuid); loadProviders(); } catch (err) { toast.error(t('models.deleteError') + (err as Error).message); } } function handleSpaceLogin() { window.location.href = '/auth/space'; } function getRequesterLabel(requester: string) { return ( requesterNameList.find((r) => r.value === requester)?.label || requester ); } function maskApiKey(key: string): string { if (!key) return ''; if (key.length <= 8) return '****'; return `${key.slice(0, 4)}...${key.slice(-4)}`; } // Separate LangBot Models provider const langbotProvider = providers.find( (p) => p.requester === LANGBOT_MODELS_PROVIDER_REQUESTER, ); const otherProviders = providers.filter( (p) => p.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER, ); function renderProviderCard( provider: ModelProvider, isLangBotModels: boolean = false, ) { const isExpanded = expandedProviders.has(provider.uuid); const isLoading = loadingProviders.has(provider.uuid); const models = providerModels[provider.uuid]; const canDelete = !isLangBotModels && (provider.llm_count || 0) === 0 && (provider.embedding_count || 0) === 0; const totalModels = (provider.llm_count || 0) + (provider.embedding_count || 0); return ( toggleProvider(provider.uuid)} >
{isLangBotModels ? (
LangBot
) : ( {provider.name} )}
{isLangBotModels ? provider.name : getRequesterLabel(provider.requester)} {t('models.modelsCount', { count: totalModels })}

{isLangBotModels ? ( t('models.langbotModelsDescription') ) : ( <> {provider.base_url} {provider.base_url && provider.api_keys?.length > 0 && ' ยท '} {provider.api_keys?.length > 0 && maskApiKey(provider.api_keys[0])} )}

{isLangBotModels && accountType !== 'space' && ( )} {isLangBotModels && accountType === 'space' && spaceCredits !== null && (
{(spaceCredits / 5000).toFixed(2)} {t('models.credits')}
)} {!isLangBotModels && ( <> {canDelete && ( )} )}
{isExpanded ? ( ) : ( )} {isExpanded ? t('models.collapseModels') : t('models.expandModels')}
{isLoading ? (

{t('common.loading')}...

) : models ? (
{models.llm.map((model) => (
handleEditLLM(model.uuid)} >
{model.name} {t('models.chat')} {model.abilities?.includes('vision') && ( )} {model.abilities?.includes('func_call') && ( )}
))} {models.embedding.map((model) => (
handleEditEmbedding(model.uuid)} >
{model.name} {t('models.embedding')}
))} {models.llm.length === 0 && models.embedding.length === 0 && (

{t('models.noModels')}

)}
) : (

{t('models.noModels')}

)}
); } // Virtual LangBot Models card if not exists function renderLangBotModelsCard() { if (langbotProvider) { return renderProviderCard(langbotProvider, true); } } function handleFormClose() { setLLMFormOpen(false); setEmbeddingFormOpen(false); setProviderFormOpen(false); loadProviders(); // Refresh expanded providers expandedProviders.forEach((uuid) => loadProviderModels(uuid)); } return ( <> { if ( !newOpen && (llmFormOpen || embeddingFormOpen || providerFormOpen) ) return; onOpenChange(newOpen); }} > {t('models.title')}
{/* Fixed LangBot Models Card */}
{renderLangBotModelsCard()}
{/* Add Model Button */}
{t('models.providerCount', { count: otherProviders.length })} {t('models.addLLMModel')} {t('models.addEmbeddingModel')}
{/* Scrollable Provider List */}
{otherProviders.map((p) => renderProviderCard(p))}
{editingLLMId ? t('models.editModel') : t('models.createModel')} setLLMFormOpen(false)} onLLMDeleted={handleFormClose} /> {editingEmbeddingId ? t('embedding.editModel') : t('embedding.createModel')} setEmbeddingFormOpen(false)} onEmbeddingDeleted={handleFormClose} /> {t('models.editProvider')} setProviderFormOpen(false)} /> ); }