-
{t('common.theme')}
-
{
- if (value) setTheme(value);
- }}
- className="justify-start"
- >
-
-
-
-
-
-
-
-
-
-
+
{
+ handleAccountSettingsChange(true);
+ setPopoverOpen(false);
+ }}
+ >
+
+ {userEmail ? userEmail.charAt(0).toUpperCase() : 'U'}
+
+
+ {userEmail || t('account.settings')}
+
-
-
- {t('common.language')}
-
+
+
-
-
- {t('common.integration')}
-
+
{t('common.apiIntegration')}
-
-
-
- {t('common.account')}
- {systemInfo?.allow_change_password && (
-
- )}
-
+
);
}
+
+function SidebarLoadingFallback() {
+ return (
+
+ );
+}
+
+export default function HomeSidebar({
+ onSelectedChangeAction,
+}: {
+ onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
+}) {
+ return (
+
}>
+
+
+ );
+}
diff --git a/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx b/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx
index b3edb98a..868ac5b4 100644
--- a/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx
+++ b/web/src/app/home/components/home-sidebar/sidbarConfigList.tsx
@@ -27,27 +27,6 @@ export const sidebarConfigList = [
zh_Hans: 'https://docs.langbot.app/zh/deploy/platforms/readme.html',
},
}),
- new SidebarChildVO({
- id: 'models',
- name: t('models.title'),
- icon: (
-
- ),
- route: '/home/models',
- description: t('models.description'),
- helpLink: {
- en_US: 'https://docs.langbot.app/en/deploy/models/readme.html',
- zh_Hans: 'https://docs.langbot.app/zh/deploy/models/readme.html',
- },
- }),
-
new SidebarChildVO({
id: 'pipelines',
name: t('pipelines.title'),
diff --git a/web/src/app/home/components/models-dialog/ModelsDialog.tsx b/web/src/app/home/components/models-dialog/ModelsDialog.tsx
new file mode 100644
index 00000000..c860c9a8
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/ModelsDialog.tsx
@@ -0,0 +1,496 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Plus, Boxes } from 'lucide-react';
+import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
+import { ModelProvider } from '@/app/infra/entities/api';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
+import ProviderForm from './component/provider-form/ProviderForm';
+import { ProviderCard } from './components';
+import {
+ ExtraArg,
+ ModelType,
+ TestResult,
+ ProviderModels,
+ LANGBOT_MODELS_PROVIDER_REQUESTER,
+} from './types';
+import { CustomApiError } from '@/app/infra/entities/common';
+
+interface ModelsDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+function convertExtraArgsToObject(
+ args: ExtraArg[],
+): Record
{
+ const obj: Record = {};
+ args.forEach((arg) => {
+ if (arg.key.trim()) {
+ if (arg.type === 'number') obj[arg.key] = Number(arg.value);
+ else if (arg.type === 'boolean') obj[arg.key] = arg.value === 'true';
+ else obj[arg.key] = arg.value;
+ }
+ });
+ return obj;
+}
+
+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(),
+ );
+
+ // Provider form modal
+ const [providerFormOpen, setProviderFormOpen] = useState(false);
+ const [editingProviderId, setEditingProviderId] = useState(
+ null,
+ );
+
+ // Popover states
+ const [addModelPopoverOpen, setAddModelPopoverOpen] = useState(
+ null,
+ );
+ const [editModelPopoverOpen, setEditModelPopoverOpen] = useState<
+ string | null
+ >(null);
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(
+ null,
+ );
+
+ // Form states
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isTesting, setIsTesting] = useState(false);
+ const [testResult, setTestResult] = useState(null);
+
+ // Track if providers have been loaded initially
+ const [providersLoaded, setProvidersLoaded] = useState(false);
+
+ // Separate LangBot Models provider (hide when models service is disabled)
+ const langbotProvider = systemInfo.disable_models_service
+ ? undefined
+ : providers.find((p) => p.requester === LANGBOT_MODELS_PROVIDER_REQUESTER);
+ const otherProviders = providers.filter(
+ (p) => p.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
+ );
+
+ useEffect(() => {
+ if (open) {
+ loadUserInfo();
+ loadProviders();
+ }
+ }, [open]);
+
+ // Auto-expand LangBot Models when no external providers exist
+ useEffect(() => {
+ if (providersLoaded && langbotProvider && otherProviders.length === 0) {
+ if (!expandedProviders.has(langbotProvider.uuid)) {
+ setExpandedProviders(new Set([langbotProvider.uuid]));
+ if (!providerModels[langbotProvider.uuid]) {
+ loadProviderModels(langbotProvider.uuid);
+ }
+ }
+ }
+ }, [providersLoaded, providers]);
+
+ 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 loadProviders() {
+ try {
+ const resp = await httpClient.getModelProviders();
+ setProviders(resp.providers);
+ setProvidersLoaded(true);
+ } catch (err) {
+ console.error('Failed to load providers', err);
+ toast.error(t('models.loadError'));
+ }
+ }
+
+ async function loadProviderModels(providerUuid: string, silent = false) {
+ if (loadingProviders.has(providerUuid)) return;
+
+ if (!silent) {
+ 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 {
+ if (!silent) {
+ 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 handleCreateProvider() {
+ setEditingProviderId(null);
+ setProviderFormOpen(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);
+ }
+ }
+
+ function handleSpaceLogin() {
+ window.location.href = '/auth/space';
+ }
+
+ async function handleAddModel(
+ providerUuid: string,
+ modelType: ModelType,
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) {
+ if (!name.trim()) {
+ toast.error(t('models.modelNameRequired'));
+ return;
+ }
+ setIsSubmitting(true);
+ try {
+ const extraArgsObj = convertExtraArgsToObject(extraArgs);
+
+ if (modelType === 'llm') {
+ await httpClient.createProviderLLMModel({
+ name,
+ provider_uuid: providerUuid,
+ abilities,
+ extra_args: extraArgsObj,
+ } as never);
+ } else {
+ await httpClient.createProviderEmbeddingModel({
+ name,
+ provider_uuid: providerUuid,
+ extra_args: extraArgsObj,
+ } as never);
+ }
+ setAddModelPopoverOpen(null);
+ loadProviderModels(providerUuid, true);
+ loadProviders();
+ } catch (err) {
+ toast.error(t('models.createError') + (err as Error).message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ async function handleUpdateModel(
+ providerUuid: string,
+ modelId: string,
+ modelType: ModelType,
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) {
+ if (!name.trim()) {
+ toast.error(t('models.modelNameRequired'));
+ return;
+ }
+ setIsSubmitting(true);
+ try {
+ const extraArgsObj = convertExtraArgsToObject(extraArgs);
+
+ if (modelType === 'llm') {
+ await httpClient.updateProviderLLMModel(modelId, {
+ name,
+ provider_uuid: providerUuid,
+ abilities,
+ extra_args: extraArgsObj,
+ } as never);
+ } else {
+ await httpClient.updateProviderEmbeddingModel(modelId, {
+ name,
+ provider_uuid: providerUuid,
+ extra_args: extraArgsObj,
+ } as never);
+ }
+ setEditModelPopoverOpen(null);
+ loadProviderModels(providerUuid, true);
+ loadProviders();
+ } catch (err) {
+ toast.error(t('models.saveError') + (err as Error).message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ async function handleDeleteModel(
+ providerUuid: string,
+ modelId: string,
+ modelType: ModelType,
+ ) {
+ try {
+ if (modelType === 'llm') {
+ await httpClient.deleteProviderLLMModel(modelId);
+ } else {
+ await httpClient.deleteProviderEmbeddingModel(modelId);
+ }
+ toast.success(t('models.deleteSuccess'));
+ loadProviderModels(providerUuid, true);
+ loadProviders();
+ } catch (err) {
+ toast.error(t('models.deleteError') + (err as Error).message);
+ }
+ }
+
+ async function handleTestModel(
+ providerUuid: string,
+ name: string,
+ modelType: ModelType,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) {
+ setIsTesting(true);
+ setTestResult(null);
+ const startTime = Date.now();
+ try {
+ const extraArgsObj = convertExtraArgsToObject(extraArgs);
+
+ // Get the provider info
+ const provider = providers.find((p) => p.uuid === providerUuid);
+ const providerData = {
+ requester: provider?.requester || '',
+ base_url: provider?.base_url || '',
+ api_keys: provider?.api_keys || [],
+ };
+
+ if (modelType === 'llm') {
+ await httpClient.testLLMModel('_', {
+ uuid: '',
+ name,
+ provider_uuid: '',
+ provider: providerData,
+ abilities,
+ extra_args: extraArgsObj,
+ } as never);
+ } else {
+ await httpClient.testEmbeddingModel('_', {
+ uuid: '',
+ name,
+ provider_uuid: '',
+ provider: providerData,
+ extra_args: extraArgsObj,
+ } as never);
+ }
+ const duration = Date.now() - startTime;
+ setTestResult({ success: true, duration });
+ } catch (err) {
+ console.error('Failed to test model', err);
+ toast.error(t('models.testError') + ': ' + (err as CustomApiError).msg);
+ setTestResult(null);
+ } finally {
+ setIsTesting(false);
+ }
+ }
+
+ function handleFormClose() {
+ setProviderFormOpen(false);
+ loadProviders();
+ // Refresh expanded providers
+ expandedProviders.forEach((uuid) => loadProviderModels(uuid));
+ }
+
+ function renderProviderCard(
+ provider: ModelProvider,
+ isLangBotModels: boolean = false,
+ ) {
+ return (
+ toggleProvider(provider.uuid)}
+ onEditProvider={() => handleEditProvider(provider.uuid)}
+ onDeleteProvider={() => handleDeleteProvider(provider.uuid)}
+ onSpaceLogin={handleSpaceLogin}
+ onOpenAddModel={() => setAddModelPopoverOpen(provider.uuid)}
+ onCloseAddModel={() => setAddModelPopoverOpen(null)}
+ onAddModel={(modelType, name, abilities, extraArgs) =>
+ handleAddModel(provider.uuid, modelType, name, abilities, extraArgs)
+ }
+ onOpenEditModel={(modelId) => setEditModelPopoverOpen(modelId)}
+ onCloseEditModel={() => setEditModelPopoverOpen(null)}
+ onUpdateModel={(modelId, modelType, name, abilities, extraArgs) =>
+ handleUpdateModel(
+ provider.uuid,
+ modelId,
+ modelType,
+ name,
+ abilities,
+ extraArgs,
+ )
+ }
+ onOpenDeleteConfirm={(modelId) => setDeleteConfirmOpen(modelId)}
+ onCloseDeleteConfirm={() => setDeleteConfirmOpen(null)}
+ onDeleteModel={(modelId, modelType) =>
+ handleDeleteModel(provider.uuid, modelId, modelType)
+ }
+ onTestModel={(name, modelType, abilities, extraArgs) =>
+ handleTestModel(provider.uuid, name, modelType, abilities, extraArgs)
+ }
+ isSubmitting={isSubmitting}
+ isTesting={isTesting}
+ testResult={testResult}
+ onResetTestResult={() => setTestResult(null)}
+ />
+ );
+ }
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx b/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx
new file mode 100644
index 00000000..94e4b0f9
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx
@@ -0,0 +1,325 @@
+import { useEffect, useState } from 'react';
+import { httpClient } from '@/app/infra/http/HttpClient';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { useTranslation } from 'react-i18next';
+
+import { Button } from '@/components/ui/button';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { DialogFooter } from '@/components/ui/dialog';
+import { toast } from 'sonner';
+import { extractI18nObject } from '@/i18n/I18nProvider';
+import { CustomApiError } from '@/app/infra/entities/common';
+
+const getFormSchema = (t: (key: string) => string) =>
+ z.object({
+ name: z.string().min(1, { message: t('models.providerNameRequired') }),
+ requester: z.string().min(1, { message: t('models.requesterRequired') }),
+ base_url: z.string(),
+ api_key: z.string().optional(),
+ });
+
+interface ProviderFormProps {
+ providerId?: string;
+ onFormSubmit: () => void;
+ onFormCancel: () => void;
+}
+
+export default function ProviderForm({
+ providerId,
+ onFormSubmit,
+ onFormCancel,
+}: ProviderFormProps) {
+ const { t } = useTranslation();
+ const formSchema = getFormSchema(t);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: '',
+ requester: '',
+ base_url: '',
+ api_key: '',
+ },
+ });
+
+ const [requesterList, setRequesterList] = useState<
+ {
+ label: string;
+ value: string;
+ category: string;
+ defaultUrl: string;
+ description: string;
+ }[]
+ >([]);
+
+ useEffect(() => {
+ loadRequesters();
+ if (providerId) {
+ loadProvider(providerId);
+ }
+ }, [providerId]);
+
+ async function loadRequesters() {
+ const resp = await httpClient.getProviderRequesters();
+ setRequesterList(
+ resp.requesters
+ .filter((item) => item.name !== 'space-chat-completions')
+ .map((item) => ({
+ label: extractI18nObject(item.label),
+ value: item.name,
+ category: item.spec.provider_category || 'manufacturer',
+ defaultUrl:
+ item.spec.config
+ .find((c) => c.name === 'base_url')
+ ?.default?.toString() || '',
+ description: extractI18nObject(item.description),
+ })),
+ );
+ }
+
+ async function loadProvider(id: string) {
+ const resp = await httpClient.getModelProvider(id);
+ const provider = resp.provider;
+
+ form.setValue('name', provider.name);
+ form.setValue('requester', provider.requester);
+ form.setValue('base_url', provider.base_url);
+ form.setValue('api_key', provider.api_keys?.[0] || '');
+ }
+
+ async function handleFormSubmit(values: z.infer) {
+ const data = {
+ name: values.name,
+ requester: values.requester,
+ base_url: values.base_url,
+ api_keys: values.api_key ? [values.api_key] : [],
+ };
+
+ try {
+ if (providerId) {
+ await httpClient.updateModelProvider(providerId, data);
+ toast.success(t('models.providerSaved'));
+ } else {
+ await httpClient.createModelProvider(data);
+ toast.success(t('models.providerCreated'));
+ }
+ onFormSubmit();
+ } catch (err) {
+ toast.error(t('models.providerSaveError') + (err as CustomApiError).msg);
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx b/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx
new file mode 100644
index 00000000..ac64d37d
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx
@@ -0,0 +1,233 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Plus, MessageSquareText, Cpu, Eye, Wrench, Check } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { useTranslation } from 'react-i18next';
+import { ExtraArg, ModelType, TestResult } from '../types';
+import ExtraArgsEditor from './ExtraArgsEditor';
+
+interface AddModelPopoverProps {
+ isOpen: boolean;
+ onOpen: () => void;
+ onClose: () => void;
+ onAddModel: (
+ modelType: ModelType,
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ onTestModel: (
+ name: string,
+ modelType: ModelType,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ isSubmitting: boolean;
+ isTesting: boolean;
+ testResult: TestResult | null;
+ onResetTestResult: () => void;
+}
+
+export default function AddModelPopover({
+ isOpen,
+ onOpen,
+ onClose,
+ onAddModel,
+ onTestModel,
+ isSubmitting,
+ isTesting,
+ testResult,
+ onResetTestResult,
+}: AddModelPopoverProps) {
+ const { t } = useTranslation();
+
+ const [tab, setTab] = useState('llm');
+ const [name, setName] = useState('');
+ const [abilities, setAbilities] = useState([]);
+ const [extraArgs, setExtraArgs] = useState([]);
+
+ // Reset form when popover opens
+ useEffect(() => {
+ if (isOpen) {
+ setTab('llm');
+ setName('');
+ setAbilities([]);
+ setExtraArgs([]);
+ onResetTestResult();
+ }
+ }, [isOpen]);
+
+ const handleAdd = async () => {
+ await onAddModel(tab, name, abilities, extraArgs);
+ };
+
+ const handleTest = async () => {
+ await onTestModel(name, tab, tab === 'llm' ? abilities : [], extraArgs);
+ };
+
+ const toggleAbility = (ability: string, checked: boolean) => {
+ if (checked) {
+ setAbilities([...abilities, ability]);
+ } else {
+ setAbilities(abilities.filter((a) => a !== ability));
+ }
+ };
+
+ return (
+ (open ? onOpen() : onClose())}
+ >
+
+
+
+ e.stopPropagation()}
+ >
+ setTab(v as ModelType)}>
+
+
+
+ {t('models.chat')}
+
+
+
+ {t('models.embedding')}
+
+
+
+
+
+
+ setName(e.target.value)}
+ />
+
+
+
+
+
+
+ toggleAbility('vision', checked as boolean)
+ }
+ />
+
+
+
+
+ toggleAbility('func_call', checked as boolean)
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setName(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/app/home/components/models-dialog/components/ExtraArgsEditor.tsx b/web/src/app/home/components/models-dialog/components/ExtraArgsEditor.tsx
new file mode 100644
index 00000000..e17a229e
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/components/ExtraArgsEditor.tsx
@@ -0,0 +1,114 @@
+'use client';
+
+import { Plus, X } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { useTranslation } from 'react-i18next';
+import { ExtraArg } from '../types';
+
+interface ExtraArgsEditorProps {
+ args: ExtraArg[];
+ onChange: (args: ExtraArg[]) => void;
+ disabled?: boolean;
+}
+
+export default function ExtraArgsEditor({
+ args,
+ onChange,
+ disabled = false,
+}: ExtraArgsEditorProps) {
+ const { t } = useTranslation();
+
+ const handleAdd = () => {
+ onChange([...args, { key: '', type: 'string', value: '' }]);
+ };
+
+ const handleRemove = (index: number) => {
+ onChange(args.filter((_, i) => i !== index));
+ };
+
+ const handleUpdate = (
+ index: number,
+ field: keyof ExtraArg,
+ value: string,
+ ) => {
+ const newArgs = [...args];
+ newArgs[index] = { ...newArgs[index], [field]: value };
+ onChange(newArgs);
+ };
+
+ return (
+
+
+
+ {!disabled && (
+
+ )}
+
+ {args.length === 0 ? (
+
{t('common.none')}
+ ) : (
+ args.map((arg, index) => (
+
+ handleUpdate(index, 'key', e.target.value)}
+ />
+
+ handleUpdate(index, 'value', e.target.value)}
+ />
+ {!disabled && (
+
+ )}
+
+ ))
+ )}
+
+ );
+}
diff --git a/web/src/app/home/components/models-dialog/components/ModelItem.tsx b/web/src/app/home/components/models-dialog/components/ModelItem.tsx
new file mode 100644
index 00000000..81649195
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/components/ModelItem.tsx
@@ -0,0 +1,292 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Trash2, Eye, Wrench, Check } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Badge } from '@/components/ui/badge';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { useTranslation } from 'react-i18next';
+import { LLMModel, EmbeddingModel } from '@/app/infra/entities/api';
+import { ExtraArg, ModelType, TestResult } from '../types';
+import ExtraArgsEditor from './ExtraArgsEditor';
+
+interface ModelItemProps {
+ model: LLMModel | EmbeddingModel;
+ modelType: ModelType;
+ isLangBotModels: boolean;
+ editModelPopoverOpen: string | null;
+ deleteConfirmOpen: string | null;
+ onOpenEditModel: (modelId: string) => void;
+ onCloseEditModel: () => void;
+ onOpenDeleteConfirm: (modelId: string) => void;
+ onCloseDeleteConfirm: () => void;
+ onDeleteModel: () => void;
+ onUpdateModel: (
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ onTestModel: (
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ isSubmitting: boolean;
+ isTesting: boolean;
+ testResult: TestResult | null;
+ onResetTestResult: () => void;
+}
+
+function convertExtraArgsToArray(extraArgs?: object): ExtraArg[] {
+ if (!extraArgs) return [];
+ return Object.entries(extraArgs).map(([key, value]) => {
+ let type: 'string' | 'number' | 'boolean' = 'string';
+ if (typeof value === 'number') type = 'number';
+ else if (typeof value === 'boolean') type = 'boolean';
+ return { key, type, value: String(value) };
+ });
+}
+
+export default function ModelItem({
+ model,
+ modelType,
+ isLangBotModels,
+ editModelPopoverOpen,
+ deleteConfirmOpen,
+ onOpenEditModel,
+ onCloseEditModel,
+ onOpenDeleteConfirm,
+ onCloseDeleteConfirm,
+ onDeleteModel,
+ onUpdateModel,
+ onTestModel,
+ isSubmitting,
+ isTesting,
+ testResult,
+ onResetTestResult,
+}: ModelItemProps) {
+ const { t } = useTranslation();
+
+ const [editName, setEditName] = useState(model.name);
+ const [editAbilities, setEditAbilities] = useState(
+ modelType === 'llm' ? (model as LLMModel).abilities || [] : [],
+ );
+ const [editExtraArgs, setEditExtraArgs] = useState(
+ convertExtraArgsToArray(model.extra_args),
+ );
+
+ const isEditOpen = editModelPopoverOpen === model.uuid;
+ const isDeleteOpen = deleteConfirmOpen === model.uuid;
+
+ // Reset form when popover opens
+ useEffect(() => {
+ if (isEditOpen) {
+ setEditName(model.name);
+ setEditAbilities(
+ modelType === 'llm' ? (model as LLMModel).abilities || [] : [],
+ );
+ setEditExtraArgs(convertExtraArgsToArray(model.extra_args));
+ onResetTestResult();
+ }
+ }, [isEditOpen]);
+
+ const handleSave = async () => {
+ await onUpdateModel(editName, editAbilities, editExtraArgs);
+ };
+
+ const handleTest = async () => {
+ await onTestModel(editName, editAbilities, editExtraArgs);
+ };
+
+ const toggleAbility = (ability: string, checked: boolean) => {
+ if (checked) {
+ setEditAbilities([...editAbilities, ability]);
+ } else {
+ setEditAbilities(editAbilities.filter((a) => a !== ability));
+ }
+ };
+
+ return (
+ {
+ if (open) {
+ onOpenEditModel(model.uuid);
+ } else {
+ onCloseEditModel();
+ }
+ }}
+ >
+
+
+
+ {model.name}
+
+ {modelType === 'llm' ? t('models.chat') : t('models.embedding')}
+
+ {modelType === 'llm' &&
+ (model as LLMModel).abilities?.includes('vision') && (
+
+
+
+ )}
+ {modelType === 'llm' &&
+ (model as LLMModel).abilities?.includes('func_call') && (
+
+
+
+ )}
+
+ {!isLangBotModels && (
+
+ open ? onOpenDeleteConfirm(model.uuid) : onCloseDeleteConfirm()
+ }
+ >
+
+
+
+ e.stopPropagation()}
+ >
+
+
{t('models.deleteConfirmation')}
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ setEditName(e.target.value)}
+ disabled={isLangBotModels}
+ />
+
+
+ {modelType === 'llm' && (
+
+
+
+
+
+ toggleAbility('vision', checked as boolean)
+ }
+ />
+
+
+
+
+ toggleAbility('func_call', checked as boolean)
+ }
+ />
+
+
+
+
+ )}
+
+
+
+
+ {!isLangBotModels && (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/web/src/app/home/components/models-dialog/components/ProviderCard.tsx b/web/src/app/home/components/models-dialog/components/ProviderCard.tsx
new file mode 100644
index 00000000..08eeb04c
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/components/ProviderCard.tsx
@@ -0,0 +1,398 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ Plus,
+ ChevronDown,
+ ChevronRight,
+ Trash2,
+ Settings,
+ LogIn,
+} from 'lucide-react';
+import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
+import { ModelProvider } from '@/app/infra/entities/api';
+import { Button } from '@/components/ui/button';
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from '@/components/ui/collapsible';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { useTranslation } from 'react-i18next';
+import langbotIcon from '@/app/assets/langbot-logo.webp';
+import { ExtraArg, ModelType, TestResult, ProviderModels } from '../types';
+import ModelItem from './ModelItem';
+import AddModelPopover from './AddModelPopover';
+
+interface ProviderCardProps {
+ provider: ModelProvider;
+ isLangBotModels?: boolean;
+ isExpanded: boolean;
+ isLoading: boolean;
+ models?: ProviderModels;
+ accountType: 'local' | 'space';
+ spaceCredits: number | null;
+ // Popover states
+ addModelPopoverOpen: string | null;
+ editModelPopoverOpen: string | null;
+ deleteConfirmOpen: string | null;
+ // Handlers
+ onToggle: () => void;
+ onEditProvider: () => void;
+ onDeleteProvider: () => void;
+ onSpaceLogin: () => void;
+ onOpenAddModel: () => void;
+ onCloseAddModel: () => void;
+ onAddModel: (
+ modelType: ModelType,
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ onOpenEditModel: (modelId: string) => void;
+ onCloseEditModel: () => void;
+ onUpdateModel: (
+ modelId: string,
+ modelType: ModelType,
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ onOpenDeleteConfirm: (modelId: string) => void;
+ onCloseDeleteConfirm: () => void;
+ onDeleteModel: (modelId: string, modelType: ModelType) => Promise;
+ onTestModel: (
+ name: string,
+ modelType: ModelType,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ isSubmitting: boolean;
+ isTesting: boolean;
+ testResult: TestResult | null;
+ onResetTestResult: () => void;
+}
+
+function maskApiKey(key: string): string {
+ if (!key) return '';
+ if (key.length <= 8) return '****';
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
+}
+
+export default function ProviderCard({
+ provider,
+ isLangBotModels = false,
+ isExpanded,
+ isLoading,
+ models,
+ accountType,
+ spaceCredits,
+ addModelPopoverOpen,
+ editModelPopoverOpen,
+ deleteConfirmOpen,
+ onToggle,
+ onEditProvider,
+ onDeleteProvider,
+ onSpaceLogin,
+ onOpenAddModel,
+ onCloseAddModel,
+ onAddModel,
+ onOpenEditModel,
+ onCloseEditModel,
+ onUpdateModel,
+ onOpenDeleteConfirm,
+ onCloseDeleteConfirm,
+ onDeleteModel,
+ onTestModel,
+ isSubmitting,
+ isTesting,
+ testResult,
+ onResetTestResult,
+}: ProviderCardProps) {
+ const { t } = useTranslation();
+ const [deleteProviderConfirmOpen, setDeleteProviderConfirmOpen] =
+ useState(false);
+
+ const canDelete =
+ !isLangBotModels &&
+ (provider.llm_count || 0) === 0 &&
+ (provider.embedding_count || 0) === 0;
+ const totalModels =
+ (provider.llm_count || 0) + (provider.embedding_count || 0);
+
+ return (
+
+
+
+
+
+ {isLangBotModels ? (
+
+

+
+ ) : (
+

+ )}
+
+
+ {provider.name}
+
+ {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 && (
+
+
+
+
+ e.stopPropagation()}
+ >
+
+
+ {t('models.deleteProviderConfirmation')}
+
+
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {totalModels > 0 ? (
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ {isExpanded
+ ? t('models.collapseModels')
+ : t('models.expandModels')}
+
+
+ ) : (
+
+ )}
+ {!isLangBotModels && (
+
+ )}
+
+
+
+
+ {isLoading ? (
+
+ {t('common.loading')}...
+
+ ) : models ? (
+
+ {models.llm.map((model) => (
+
onDeleteModel(model.uuid, 'llm')}
+ onUpdateModel={(name, abilities, extraArgs) =>
+ onUpdateModel(
+ model.uuid,
+ 'llm',
+ name,
+ abilities,
+ extraArgs,
+ )
+ }
+ onTestModel={(name, abilities, extraArgs) =>
+ onTestModel(name, 'llm', abilities, extraArgs)
+ }
+ isSubmitting={isSubmitting}
+ isTesting={isTesting}
+ testResult={testResult}
+ onResetTestResult={onResetTestResult}
+ />
+ ))}
+ {models.embedding.map((model) => (
+ onDeleteModel(model.uuid, 'embedding')}
+ onUpdateModel={(name, abilities, extraArgs) =>
+ onUpdateModel(
+ model.uuid,
+ 'embedding',
+ name,
+ abilities,
+ extraArgs,
+ )
+ }
+ onTestModel={(name, abilities, extraArgs) =>
+ onTestModel(name, 'embedding', abilities, extraArgs)
+ }
+ isSubmitting={isSubmitting}
+ isTesting={isTesting}
+ testResult={testResult}
+ onResetTestResult={onResetTestResult}
+ />
+ ))}
+ {models.llm.length === 0 && models.embedding.length === 0 && (
+
+ {t('models.noModels')}
+
+ )}
+
+ ) : (
+
+ {t('models.noModels')}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/web/src/app/home/components/models-dialog/components/index.ts b/web/src/app/home/components/models-dialog/components/index.ts
new file mode 100644
index 00000000..4e0a41cf
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/components/index.ts
@@ -0,0 +1,4 @@
+export { default as ExtraArgsEditor } from './ExtraArgsEditor';
+export { default as ModelItem } from './ModelItem';
+export { default as AddModelPopover } from './AddModelPopover';
+export { default as ProviderCard } from './ProviderCard';
diff --git a/web/src/app/home/components/models-dialog/types.ts b/web/src/app/home/components/models-dialog/types.ts
new file mode 100644
index 00000000..15217269
--- /dev/null
+++ b/web/src/app/home/components/models-dialog/types.ts
@@ -0,0 +1,102 @@
+import {
+ LLMModel,
+ EmbeddingModel,
+ ModelProvider,
+} from '@/app/infra/entities/api';
+
+export type ExtraArg = {
+ key: string;
+ type: 'string' | 'number' | 'boolean';
+ value: string;
+};
+
+export type ModelType = 'llm' | 'embedding';
+
+export interface ProviderModels {
+ llm: LLMModel[];
+ embedding: EmbeddingModel[];
+}
+
+export interface TestResult {
+ success: boolean;
+ duration: number;
+}
+
+export interface ModelItemProps {
+ model: LLMModel | EmbeddingModel;
+ modelType: ModelType;
+ providerUuid: string;
+ isLangBotModels: boolean;
+ isEditOpen: boolean;
+ isDeleteOpen: boolean;
+ onEditOpen: () => void;
+ onEditClose: () => void;
+ onDeleteOpen: () => void;
+ onDeleteClose: () => void;
+ onDelete: () => void;
+ onUpdate: (
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ onTest: (
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ isSubmitting: boolean;
+ isTesting: boolean;
+ testResult: TestResult | null;
+}
+
+export interface ProviderCardProps {
+ provider: ModelProvider;
+ isLangBotModels?: boolean;
+ isExpanded: boolean;
+ isLoading: boolean;
+ models?: ProviderModels;
+ accountType: 'local' | 'space';
+ spaceCredits: number | null;
+ requesterNameList: { label: string; value: string }[];
+ // Popover states
+ addModelPopoverOpen: string | null;
+ editModelPopoverOpen: string | null;
+ deleteConfirmOpen: string | null;
+ // Handlers
+ onToggle: () => void;
+ onEditProvider: () => void;
+ onDeleteProvider: () => void;
+ onSpaceLogin: () => void;
+ onOpenAddModel: () => void;
+ onCloseAddModel: () => void;
+ onAddModel: (
+ modelType: ModelType,
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ onOpenEditModel: (modelId: string) => void;
+ onCloseEditModel: () => void;
+ onUpdateModel: (
+ modelId: string,
+ modelType: ModelType,
+ name: string,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ onOpenDeleteConfirm: (modelId: string) => void;
+ onCloseDeleteConfirm: () => void;
+ onDeleteModel: (modelId: string, modelType: ModelType) => Promise;
+ onTestModel: (
+ name: string,
+ modelType: ModelType,
+ abilities: string[],
+ extraArgs: ExtraArg[],
+ ) => Promise;
+ isSubmitting: boolean;
+ isTesting: boolean;
+ testResult: TestResult | null;
+ onResetTestResult: () => void;
+}
+
+export const LANGBOT_MODELS_PROVIDER_REQUESTER = 'space-chat-completions';
diff --git a/web/src/app/home/components/password-change-dialog/PasswordChangeDialog.tsx b/web/src/app/home/components/password-change-dialog/PasswordChangeDialog.tsx
index 03a302af..279eb265 100644
--- a/web/src/app/home/components/password-change-dialog/PasswordChangeDialog.tsx
+++ b/web/src/app/home/components/password-change-dialog/PasswordChangeDialog.tsx
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -26,36 +26,39 @@ import {
import { Input } from '@/components/ui/input';
import { httpClient } from '@/app/infra/http/HttpClient';
-const getFormSchema = (t: (key: string) => string) =>
- z
- .object({
- currentPassword: z
- .string()
- .min(1, { message: t('common.currentPasswordRequired') }),
- newPassword: z
- .string()
- .min(1, { message: t('common.newPasswordRequired') }),
- confirmNewPassword: z
- .string()
- .min(1, { message: t('common.confirmPasswordRequired') }),
- })
- .refine((data) => data.newPassword === data.confirmNewPassword, {
- message: t('common.passwordsDoNotMatch'),
- path: ['confirmNewPassword'],
- });
-
interface PasswordChangeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
+ hasPassword?: boolean;
}
export default function PasswordChangeDialog({
open,
onOpenChange,
+ hasPassword = true,
}: PasswordChangeDialogProps) {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
- const formSchema = getFormSchema(t);
+
+ const getFormSchema = () =>
+ z
+ .object({
+ currentPassword: hasPassword
+ ? z.string().min(1, { message: t('common.currentPasswordRequired') })
+ : z.string().optional(),
+ newPassword: z
+ .string()
+ .min(1, { message: t('common.newPasswordRequired') }),
+ confirmNewPassword: z
+ .string()
+ .min(1, { message: t('common.confirmPasswordRequired') }),
+ })
+ .refine((data) => data.newPassword === data.confirmNewPassword, {
+ message: t('common.passwordsDoNotMatch'),
+ path: ['confirmNewPassword'],
+ });
+
+ const formSchema = getFormSchema();
const form = useForm>({
resolver: zodResolver(formSchema),
@@ -66,14 +69,30 @@ export default function PasswordChangeDialog({
},
});
+ // Reset form when dialog opens/closes or hasPassword changes
+ useEffect(() => {
+ if (open) {
+ form.reset({
+ currentPassword: '',
+ newPassword: '',
+ confirmNewPassword: '',
+ });
+ }
+ }, [open, hasPassword, form]);
+
const onSubmit = async (values: z.infer) => {
setIsSubmitting(true);
try {
- await httpClient.changePassword(
- values.currentPassword,
- values.newPassword,
- );
- toast.success(t('common.changePasswordSuccess'));
+ if (hasPassword) {
+ await httpClient.changePassword(
+ values.currentPassword!,
+ values.newPassword,
+ );
+ toast.success(t('common.changePasswordSuccess'));
+ } else {
+ await httpClient.setPassword(values.newPassword, undefined);
+ toast.success(t('account.passwordSetSuccess'));
+ }
form.reset();
onOpenChange(false);
} catch {
@@ -87,27 +106,33 @@ export default function PasswordChangeDialog({
diff --git a/web/src/app/home/models/LLMConfig.module.css b/web/src/app/home/models/LLMConfig.module.css
deleted file mode 100644
index ce6c689a..00000000
--- a/web/src/app/home/models/LLMConfig.module.css
+++ /dev/null
@@ -1,19 +0,0 @@
-.modelListContainer {
- width: 100%;
- padding-left: 0.8rem;
- padding-right: 0.8rem;
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
- gap: 2rem;
- justify-items: stretch;
- align-items: start;
-}
-
-.emptyContainer {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
-}
diff --git a/web/src/app/home/models/component/ChooseRequesterEntity.ts b/web/src/app/home/models/component/ChooseRequesterEntity.ts
deleted file mode 100644
index 69ca03df..00000000
--- a/web/src/app/home/models/component/ChooseRequesterEntity.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface IChooseRequesterEntity {
- label: string;
- value: string;
- provider_category?: string;
- description?: string;
-}
diff --git a/web/src/app/home/models/component/ICreateEmbeddingField.ts b/web/src/app/home/models/component/ICreateEmbeddingField.ts
deleted file mode 100644
index ea198f3f..00000000
--- a/web/src/app/home/models/component/ICreateEmbeddingField.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface ICreateEmbeddingField {
- name: string;
- model_provider: string;
- url: string;
- api_key: string;
- extra_args?: string[];
-}
diff --git a/web/src/app/home/models/component/ICreateLLMField.ts b/web/src/app/home/models/component/ICreateLLMField.ts
deleted file mode 100644
index 4ded490b..00000000
--- a/web/src/app/home/models/component/ICreateLLMField.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export interface ICreateLLMField {
- name: string;
- model_provider: string;
- url: string;
- api_key: string;
- abilities: string[];
- extra_args: string[];
-}
diff --git a/web/src/app/home/models/component/embedding-card/EmbeddingCard.module.css b/web/src/app/home/models/component/embedding-card/EmbeddingCard.module.css
deleted file mode 100644
index 8ff900a9..00000000
--- a/web/src/app/home/models/component/embedding-card/EmbeddingCard.module.css
+++ /dev/null
@@ -1,128 +0,0 @@
-.cardContainer {
- width: 100%;
- height: 10rem;
- background-color: #fff;
- border-radius: 10px;
- box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
- padding: 1.2rem;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-:global(.dark) .cardContainer {
- background-color: #1f1f22;
- box-shadow: 0;
-}
-
-.cardContainer:hover {
- box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
-}
-
-:global(.dark) .cardContainer:hover {
- box-shadow: 0;
-}
-
-.iconBasicInfoContainer {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: row;
- gap: 0.8rem;
- user-select: none;
-}
-
-.iconImage {
- width: 3.8rem;
- height: 3.8rem;
- margin: 0.2rem;
- border-radius: 8%;
-}
-
-.basicInfoContainer {
- display: flex;
- flex-direction: column;
- gap: 0.2rem;
- min-width: 0;
- width: 100%;
-}
-
-.basicInfoText {
- font-size: 1.4rem;
- font-weight: bold;
- color: #1a1a1a;
-}
-
-:global(.dark) .basicInfoText {
- color: #f0f0f0;
-}
-
-.providerContainer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- gap: 0.2rem;
-}
-
-.providerIcon {
- width: 1.2rem;
- height: 1.2rem;
- margin-top: 0.2rem;
- color: #626262;
-}
-
-:global(.dark) .providerIcon {
- color: #a0a0a0;
-}
-
-.providerLabel {
- font-size: 1.2rem;
- font-weight: 600;
- color: #626262;
-}
-
-:global(.dark) .providerLabel {
- color: #a0a0a0;
-}
-
-.baseURLContainer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- gap: 0.2rem;
- width: calc(100% - 3rem);
-}
-
-.baseURLIcon {
- width: 1.2rem;
- height: 1.2rem;
- color: #626262;
-}
-
-:global(.dark) .baseURLIcon {
- color: #a0a0a0;
-}
-
-.baseURLText {
- font-size: 1rem;
- width: 100%;
- color: #626262;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
-}
-
-:global(.dark) .baseURLText {
- color: #a0a0a0;
-}
-
-.bigText {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-size: 1.4rem;
- font-weight: bold;
- max-width: 100%;
-}
diff --git a/web/src/app/home/models/component/embedding-card/EmbeddingCard.tsx b/web/src/app/home/models/component/embedding-card/EmbeddingCard.tsx
deleted file mode 100644
index 8f2dfcd3..00000000
--- a/web/src/app/home/models/component/embedding-card/EmbeddingCard.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import styles from './EmbeddingCard.module.css';
-import { EmbeddingCardVO } from '@/app/home/models/component/embedding-card/EmbeddingCardVO';
-
-export default function EmbeddingCard({ cardVO }: { cardVO: EmbeddingCardVO }) {
- return (
-
-
-

-
-
- {/* 名称 */}
-
- {cardVO.name}
-
- {/* 厂商 */}
-
-
-
- {cardVO.providerLabel}
-
-
- {/* baseURL */}
- {cardVO.baseURL && (
-
-
-
{cardVO.baseURL}
-
- )}
-
-
-
- );
-}
diff --git a/web/src/app/home/models/component/embedding-card/EmbeddingCardVO.ts b/web/src/app/home/models/component/embedding-card/EmbeddingCardVO.ts
deleted file mode 100644
index f6d960f6..00000000
--- a/web/src/app/home/models/component/embedding-card/EmbeddingCardVO.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-export interface IEmbeddingCardVO {
- id: string;
- iconURL: string;
- name: string;
- providerLabel: string;
- baseURL: string;
-}
-
-export class EmbeddingCardVO implements IEmbeddingCardVO {
- id: string;
- iconURL: string;
- providerLabel: string;
- name: string;
- baseURL: string;
-
- constructor(props: IEmbeddingCardVO) {
- this.id = props.id;
- this.iconURL = props.iconURL;
- this.providerLabel = props.providerLabel;
- this.name = props.name;
- this.baseURL = props.baseURL;
- }
-}
diff --git a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx
deleted file mode 100644
index 41eeb635..00000000
--- a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx
+++ /dev/null
@@ -1,651 +0,0 @@
-import { ICreateEmbeddingField } from '@/app/home/models/component/ICreateEmbeddingField';
-import { useEffect, useState } from 'react';
-import { IChooseRequesterEntity } from '@/app/home/models/component/ChooseRequesterEntity';
-import { httpClient } from '@/app/infra/http/HttpClient';
-import { EmbeddingModel } from '@/app/infra/entities/api';
-import { UUID } from 'uuidjs';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-import { useTranslation } from 'react-i18next';
-
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogFooter,
-} from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@/components/ui/form';
-import { Input } from '@/components/ui/input';
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import { toast } from 'sonner';
-import { extractI18nObject } from '@/i18n/I18nProvider';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
-import { AlertCircle } from 'lucide-react';
-
-const getExtraArgSchema = (t: (key: string) => string) =>
- z
- .object({
- key: z.string().min(1, { message: t('models.keyNameRequired') }),
- type: z.enum(['string', 'number', 'boolean']),
- value: z.string(),
- })
- .superRefine((data, ctx) => {
- if (data.type === 'number' && isNaN(Number(data.value))) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('models.mustBeValidNumber'),
- path: ['value'],
- });
- }
- if (
- data.type === 'boolean' &&
- data.value !== 'true' &&
- data.value !== 'false'
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('models.mustBeTrueOrFalse'),
- path: ['value'],
- });
- }
- });
-
-const getFormSchema = (t: (key: string) => string) =>
- z.object({
- name: z.string().min(1, { message: t('models.modelNameRequired') }),
- model_provider: z
- .string()
- .min(1, { message: t('models.modelProviderRequired') }),
- url: z.string().optional(),
- api_key: z.string().optional(),
- extra_args: z.array(getExtraArgSchema(t)).optional(),
- });
-
-export default function EmbeddingForm({
- editMode,
- initEmbeddingId,
- onFormSubmit,
- onFormCancel,
- onEmbeddingDeleted,
-}: {
- editMode: boolean;
- initEmbeddingId?: string;
- onFormSubmit: () => void;
- onFormCancel: () => void;
- onEmbeddingDeleted: () => void;
-}) {
- const { t } = useTranslation();
- const formSchema = getFormSchema(t);
-
- const form = useForm
>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- name: '',
- model_provider: '',
- url: '',
- api_key: '',
- extra_args: [],
- },
- });
-
- const [extraArgs, setExtraArgs] = useState<
- { key: string; type: 'string' | 'number' | 'boolean'; value: string }[]
- >([]);
-
- const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
- const [requesterNameList, setRequesterNameList] = useState<
- IChooseRequesterEntity[]
- >([]);
- const [requesterDefaultURLList, setRequesterDefaultURLList] = useState<
- string[]
- >([]);
- const [modelTesting, setModelTesting] = useState(false);
- const [testErrorMessage, setTestErrorMessage] = useState(null);
- const [currentModelProvider, setCurrentModelProvider] = useState('');
-
- useEffect(() => {
- initEmbeddingModelFormComponent().then(() => {
- if (editMode && initEmbeddingId) {
- getEmbeddingConfig(initEmbeddingId).then((val) => {
- form.setValue('name', val.name);
- form.setValue('model_provider', val.model_provider);
- setCurrentModelProvider(val.model_provider);
- form.setValue('url', val.url);
- form.setValue('api_key', val.api_key);
- if (val.extra_args) {
- const args = val.extra_args.map((arg) => {
- const [key, value] = arg.split(':');
- let type: 'string' | 'number' | 'boolean' = 'string';
- if (!isNaN(Number(value))) {
- type = 'number';
- } else if (value === 'true' || value === 'false') {
- type = 'boolean';
- }
- return {
- key,
- type,
- value,
- };
- });
- setExtraArgs(args);
- form.setValue('extra_args', args);
- }
- });
- } else {
- form.reset();
- }
- });
- }, []);
-
- const addExtraArg = () => {
- setExtraArgs([...extraArgs, { key: '', type: 'string', value: '' }]);
- };
-
- const updateExtraArg = (
- index: number,
- field: 'key' | 'type' | 'value',
- value: string,
- ) => {
- const newArgs = [...extraArgs];
- newArgs[index] = {
- ...newArgs[index],
- [field]: value,
- };
- setExtraArgs(newArgs);
- form.setValue('extra_args', newArgs);
- };
-
- const removeExtraArg = (index: number) => {
- const newArgs = extraArgs.filter((_, i) => i !== index);
- setExtraArgs(newArgs);
- form.setValue('extra_args', newArgs);
- };
-
- async function initEmbeddingModelFormComponent() {
- const requesterNameList =
- await httpClient.getProviderRequesters('text-embedding');
- setRequesterNameList(
- requesterNameList.requesters.map((item) => {
- return {
- label: extractI18nObject(item.label),
- value: item.name,
- provider_category: item.spec.provider_category || 'manufacturer',
- description: extractI18nObject(item.description) || undefined,
- };
- }),
- );
- setRequesterDefaultURLList(
- requesterNameList.requesters.map((item) => {
- const config = item.spec.config;
- for (let i = 0; i < config.length; i++) {
- if (config[i].name == 'base_url') {
- return config[i].default?.toString() || '';
- }
- }
- return '';
- }),
- );
- }
-
- async function getEmbeddingConfig(
- id: string,
- ): Promise {
- const embeddingModel = await httpClient.getProviderEmbeddingModel(id);
-
- const fakeExtraArgs = [];
- const extraArgs = embeddingModel.model.extra_args as Record;
- for (const key in extraArgs) {
- fakeExtraArgs.push(`${key}:${extraArgs[key]}`);
- }
- return {
- name: embeddingModel.model.name,
- model_provider: embeddingModel.model.requester,
- url: embeddingModel.model.requester_config?.base_url,
- api_key: embeddingModel.model.api_keys[0],
- extra_args: fakeExtraArgs,
- };
- }
-
- function handleFormSubmit(value: z.infer) {
- const extraArgsObj: Record = {};
- value.extra_args?.forEach(
- (arg: { key: string; type: string; value: string }) => {
- if (arg.type === 'number') {
- extraArgsObj[arg.key] = Number(arg.value);
- } else if (arg.type === 'boolean') {
- extraArgsObj[arg.key] = arg.value === 'true';
- } else {
- extraArgsObj[arg.key] = arg.value;
- }
- },
- );
-
- const embeddingModel: EmbeddingModel = {
- uuid: editMode ? initEmbeddingId || '' : UUID.generate(),
- name: value.name,
- description: '',
- requester: value.model_provider,
- requester_config: {
- base_url: value.url || '',
- timeout: 120,
- },
- extra_args: extraArgsObj,
- api_keys: value.api_key ? [value.api_key] : [],
- };
-
- if (editMode) {
- onSaveEdit(embeddingModel).then(() => {
- form.reset();
- });
- } else {
- onCreateEmbedding(embeddingModel).then(() => {
- form.reset();
- });
- }
- }
-
- async function onCreateEmbedding(embeddingModel: EmbeddingModel) {
- try {
- await httpClient.createProviderEmbeddingModel(embeddingModel);
- onFormSubmit();
- toast.success(t('models.createSuccess'));
- } catch (err) {
- toast.error(t('models.createError') + (err as Error).message);
- }
- }
-
- async function onSaveEdit(embeddingModel: EmbeddingModel) {
- try {
- await httpClient.updateProviderEmbeddingModel(
- initEmbeddingId || '',
- embeddingModel,
- );
- onFormSubmit();
- toast.success(t('models.saveSuccess'));
- } catch (err) {
- toast.error(t('models.saveError') + (err as Error).message);
- }
- }
-
- function deleteModel() {
- if (initEmbeddingId) {
- httpClient
- .deleteProviderEmbeddingModel(initEmbeddingId)
- .then(() => {
- onEmbeddingDeleted();
- toast.success(t('models.deleteSuccess'));
- })
- .catch((err) => {
- toast.error(t('models.deleteError') + err.message);
- });
- }
- }
-
- function testEmbeddingModelInForm() {
- setModelTesting(true);
- setTestErrorMessage(null);
- const extraArgsObj: Record = {};
- form
- .getValues('extra_args')
- ?.forEach((arg: { key: string; type: string; value: string }) => {
- if (arg.type === 'number') {
- extraArgsObj[arg.key] = Number(arg.value);
- } else if (arg.type === 'boolean') {
- extraArgsObj[arg.key] = arg.value === 'true';
- } else {
- extraArgsObj[arg.key] = arg.value;
- }
- });
- const apiKey = form.getValues('api_key');
- httpClient
- .testEmbeddingModel('_', {
- uuid: '',
- name: form.getValues('name'),
- description: '',
- requester: form.getValues('model_provider'),
- requester_config: {
- base_url: form.getValues('url') ?? '',
- timeout: 120,
- },
- api_keys: apiKey ? [apiKey] : [],
- extra_args: extraArgsObj,
- })
- .then(() => {
- toast.success(t('models.testSuccess'));
- setTestErrorMessage(null);
- })
- .catch((err: { message?: string }) => {
- setTestErrorMessage(err?.message || t('models.testError'));
- })
- .finally(() => {
- setModelTesting(false);
- });
- }
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/web/src/app/home/models/component/llm-card/LLMCard.module.css b/web/src/app/home/models/component/llm-card/LLMCard.module.css
deleted file mode 100644
index c6eed0b7..00000000
--- a/web/src/app/home/models/component/llm-card/LLMCard.module.css
+++ /dev/null
@@ -1,173 +0,0 @@
-.cardContainer {
- width: 100%;
- height: 10rem;
- background-color: #fff;
- border-radius: 10px;
- box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
- padding: 1.2rem;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-:global(.dark) .cardContainer {
- background-color: #1f1f22;
- box-shadow: 0;
-}
-
-.cardContainer:hover {
- box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
-}
-
-:global(.dark) .cardContainer:hover {
- box-shadow: 0;
-}
-
-.iconBasicInfoContainer {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: row;
- gap: 0.8rem;
- user-select: none;
- /* background-color: aqua; */
-}
-
-.iconImage {
- width: 3.8rem;
- height: 3.8rem;
- margin: 0.2rem;
- border-radius: 8%;
-}
-
-.basicInfoContainer {
- display: flex;
- flex-direction: column;
- gap: 0.2rem;
- min-width: 0;
- width: 100%;
-}
-
-.basicInfoText {
- font-size: 1.4rem;
- font-weight: bold;
- color: #1a1a1a;
-}
-
-:global(.dark) .basicInfoText {
- color: #f0f0f0;
-}
-
-.providerContainer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- gap: 0.2rem;
-}
-
-.providerIcon {
- width: 1.2rem;
- height: 1.2rem;
- margin-top: 0.2rem;
- color: #626262;
-}
-
-:global(.dark) .providerIcon {
- color: #a0a0a0;
-}
-
-.providerLabel {
- font-size: 1.2rem;
- font-weight: 600;
- color: #626262;
-}
-
-:global(.dark) .providerLabel {
- color: #a0a0a0;
-}
-
-.baseURLContainer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- gap: 0.2rem;
- width: calc(100% - 3rem);
-}
-
-.baseURLIcon {
- width: 1.2rem;
- height: 1.2rem;
- color: #626262;
-}
-
-:global(.dark) .baseURLIcon {
- color: #a0a0a0;
-}
-
-.baseURLText {
- font-size: 1rem;
- width: 100%;
- color: #626262;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
-}
-
-:global(.dark) .baseURLText {
- color: #a0a0a0;
-}
-
-.abilitiesContainer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- gap: 0.4rem;
-}
-
-.abilityBadge {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- gap: 0.2rem;
- height: 1.5rem;
- padding: 0.5rem;
- border-radius: 0.8rem;
- background-color: #66baff80;
-}
-
-:global(.dark) .abilityBadge {
- background-color: rgba(34, 136, 238, 0.3);
-}
-
-.abilityIcon {
- width: 1rem;
- height: 1rem;
- color: #2288ee;
-}
-
-:global(.dark) .abilityIcon {
- color: #66baff;
-}
-
-.abilityLabel {
- font-size: 0.8rem;
- font-weight: 400;
- color: #2288ee;
-}
-
-:global(.dark) .abilityLabel {
- color: #66baff;
-}
-
-.bigText {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-size: 1.4rem;
- font-weight: bold;
- max-width: 100%;
-}
diff --git a/web/src/app/home/models/component/llm-card/LLMCard.tsx b/web/src/app/home/models/component/llm-card/LLMCard.tsx
deleted file mode 100644
index 90dc3fab..00000000
--- a/web/src/app/home/models/component/llm-card/LLMCard.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import styles from './LLMCard.module.css';
-import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO';
-import { useTranslation } from 'react-i18next';
-
-function AbilityBadges(abilities: string[]) {
- const { t } = useTranslation();
- const abilityBadges = {
- vision: (
-
-
-
- {t('models.visionAbility')}
-
-
- ),
- func_call: (
-
-
-
- {t('models.functionCallAbility')}
-
-
- ),
- };
-
- return abilities.map((ability) => {
- return abilityBadges[ability as keyof typeof abilityBadges];
- });
-}
-
-export default function LLMCard({ cardVO }: { cardVO: LLMCardVO }) {
- return (
-
-
-

-
-
- {/* 名称 */}
-
- {cardVO.name}
-
- {/* 厂商 */}
-
-
-
- {cardVO.providerLabel}
-
-
- {/* baseURL */}
-
-
-
{cardVO.baseURL}
-
- {/* 能力 */}
-
- {AbilityBadges(cardVO.abilities)}
-
-
-
-
- );
-}
diff --git a/web/src/app/home/models/component/llm-card/LLMCardVO.ts b/web/src/app/home/models/component/llm-card/LLMCardVO.ts
deleted file mode 100644
index 274cede1..00000000
--- a/web/src/app/home/models/component/llm-card/LLMCardVO.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export interface ILLMCardVO {
- id: string;
- iconURL: string;
- name: string;
- providerLabel: string;
- baseURL: string;
- abilities: string[];
-}
-
-export class LLMCardVO implements ILLMCardVO {
- id: string;
- iconURL: string;
- providerLabel: string;
- name: string;
- baseURL: string;
- abilities: string[];
-
- constructor(props: ILLMCardVO) {
- this.id = props.id;
- this.iconURL = props.iconURL;
- this.providerLabel = props.providerLabel;
- this.name = props.name;
- this.baseURL = props.baseURL;
- this.abilities = props.abilities;
- }
-}
diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx
deleted file mode 100644
index 35b5757f..00000000
--- a/web/src/app/home/models/component/llm-form/LLMForm.tsx
+++ /dev/null
@@ -1,698 +0,0 @@
-import { ICreateLLMField } from '@/app/home/models/component/ICreateLLMField';
-import { useEffect, useState } from 'react';
-import { IChooseRequesterEntity } from '@/app/home/models/component/ChooseRequesterEntity';
-import { httpClient } from '@/app/infra/http/HttpClient';
-import { LLMModel } from '@/app/infra/entities/api';
-import { UUID } from 'uuidjs';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-import { useTranslation } from 'react-i18next';
-
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogFooter,
-} from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@/components/ui/form';
-import { Input } from '@/components/ui/input';
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import { Checkbox } from '@/components/ui/checkbox';
-import { toast } from 'sonner';
-import { extractI18nObject } from '@/i18n/I18nProvider';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
-import { AlertCircle } from 'lucide-react';
-
-const getExtraArgSchema = (t: (key: string) => string) =>
- z
- .object({
- key: z.string().min(1, { message: t('models.keyNameRequired') }),
- type: z.enum(['string', 'number', 'boolean']),
- value: z.string(),
- })
- .superRefine((data, ctx) => {
- if (data.type === 'number' && isNaN(Number(data.value))) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('models.mustBeValidNumber'),
- path: ['value'],
- });
- }
- if (
- data.type === 'boolean' &&
- data.value !== 'true' &&
- data.value !== 'false'
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('models.mustBeTrueOrFalse'),
- path: ['value'],
- });
- }
- });
-
-const getFormSchema = (t: (key: string) => string) =>
- z.object({
- name: z.string().min(1, { message: t('models.modelNameRequired') }),
- model_provider: z
- .string()
- .min(1, { message: t('models.modelProviderRequired') }),
- url: z.string().min(1, { message: t('models.requestURLRequired') }),
- api_key: z.string().optional(),
- abilities: z.array(z.string()),
- extra_args: z.array(getExtraArgSchema(t)).optional(),
- });
-
-export default function LLMForm({
- editMode,
- initLLMId,
- onFormSubmit,
- onFormCancel,
- onLLMDeleted,
-}: {
- editMode: boolean;
- initLLMId?: string;
- onFormSubmit: () => void;
- onFormCancel: () => void;
- onLLMDeleted: () => void;
-}) {
- const { t } = useTranslation();
- const formSchema = getFormSchema(t);
-
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- name: '',
- model_provider: '',
- url: '',
- api_key: '',
- abilities: [],
- extra_args: [],
- },
- });
-
- const [extraArgs, setExtraArgs] = useState<
- { key: string; type: 'string' | 'number' | 'boolean'; value: string }[]
- >([]);
-
- const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
- const abilityOptions: { label: string; value: string }[] = [
- {
- label: t('models.visionAbility'),
- value: 'vision',
- },
- {
- label: t('models.functionCallAbility'),
- value: 'func_call',
- },
- ];
- const [requesterNameList, setRequesterNameList] = useState<
- IChooseRequesterEntity[]
- >([]);
- const [requesterDefaultURLList, setRequesterDefaultURLList] = useState<
- string[]
- >([]);
- const [modelTesting, setModelTesting] = useState(false);
- const [testErrorMessage, setTestErrorMessage] = useState(null);
- const [currentModelProvider, setCurrentModelProvider] = useState('');
-
- useEffect(() => {
- initLLMModelFormComponent().then(() => {
- if (editMode && initLLMId) {
- getLLMConfig(initLLMId).then((val) => {
- form.setValue('name', val.name);
- form.setValue('model_provider', val.model_provider);
- setCurrentModelProvider(val.model_provider);
- form.setValue('url', val.url);
- form.setValue('api_key', val.api_key);
- form.setValue(
- 'abilities',
- val.abilities as ('vision' | 'func_call')[],
- );
- // 转换extra_args为新格式
- if (val.extra_args) {
- const args = val.extra_args.map((arg) => {
- const [key, value] = arg.split(':');
- let type: 'string' | 'number' | 'boolean' = 'string';
- if (!isNaN(Number(value))) {
- type = 'number';
- } else if (value === 'true' || value === 'false') {
- type = 'boolean';
- }
- return {
- key,
- type,
- value,
- };
- });
- setExtraArgs(args);
- form.setValue('extra_args', args);
- }
- });
- } else {
- form.reset();
- }
- });
- }, []);
-
- const addExtraArg = () => {
- setExtraArgs([...extraArgs, { key: '', type: 'string', value: '' }]);
- };
-
- const updateExtraArg = (
- index: number,
- field: 'key' | 'type' | 'value',
- value: string,
- ) => {
- const newArgs = [...extraArgs];
- newArgs[index] = {
- ...newArgs[index],
- [field]: value,
- };
- setExtraArgs(newArgs);
- form.setValue('extra_args', newArgs);
- };
-
- const removeExtraArg = (index: number) => {
- const newArgs = extraArgs.filter((_, i) => i !== index);
- setExtraArgs(newArgs);
- form.setValue('extra_args', newArgs);
- };
-
- async function initLLMModelFormComponent() {
- const requesterNameList = await httpClient.getProviderRequesters('llm');
- setRequesterNameList(
- requesterNameList.requesters.map((item) => {
- return {
- label: extractI18nObject(item.label),
- value: item.name,
- provider_category: item.spec.provider_category || 'manufacturer',
- };
- }),
- );
- setRequesterDefaultURLList(
- requesterNameList.requesters.map((item) => {
- const config = item.spec.config;
- for (let i = 0; i < config.length; i++) {
- if (config[i].name == 'base_url') {
- return config[i].default?.toString() || '';
- }
- }
- return '';
- }),
- );
- }
-
- async function getLLMConfig(id: string): Promise {
- const llmModel = await httpClient.getProviderLLMModel(id);
-
- const fakeExtraArgs = [];
- const extraArgs = llmModel.model.extra_args as Record;
- for (const key in extraArgs) {
- fakeExtraArgs.push(`${key}:${extraArgs[key]}`);
- }
- return {
- name: llmModel.model.name,
- model_provider: llmModel.model.requester,
- url: llmModel.model.requester_config?.base_url,
- api_key: llmModel.model.api_keys[0],
- abilities: llmModel.model.abilities || [],
- extra_args: fakeExtraArgs,
- };
- }
-
- function handleFormSubmit(value: z.infer) {
- const extraArgsObj: Record = {};
- value.extra_args?.forEach(
- (arg: { key: string; type: string; value: string }) => {
- if (arg.type === 'number') {
- extraArgsObj[arg.key] = Number(arg.value);
- } else if (arg.type === 'boolean') {
- extraArgsObj[arg.key] = arg.value === 'true';
- } else {
- extraArgsObj[arg.key] = arg.value;
- }
- },
- );
-
- const llmModel: LLMModel = {
- uuid: editMode ? initLLMId || '' : UUID.generate(),
- name: value.name,
- description: '',
- requester: value.model_provider,
- requester_config: {
- base_url: value.url,
- timeout: 120,
- },
- extra_args: extraArgsObj,
- api_keys: value.api_key ? [value.api_key] : [],
- abilities: value.abilities,
- };
-
- if (editMode) {
- onSaveEdit(llmModel).then(() => {
- form.reset();
- });
- } else {
- onCreateLLM(llmModel).then(() => {
- form.reset();
- });
- }
- }
-
- async function onCreateLLM(llmModel: LLMModel) {
- try {
- await httpClient.createProviderLLMModel(llmModel);
- onFormSubmit();
- toast.success(t('models.createSuccess'));
- } catch (err) {
- toast.error(t('models.createError') + (err as Error).message);
- }
- }
-
- async function onSaveEdit(llmModel: LLMModel) {
- try {
- await httpClient.updateProviderLLMModel(initLLMId || '', llmModel);
- onFormSubmit();
- toast.success(t('models.saveSuccess'));
- } catch (err) {
- toast.error(t('models.saveError') + (err as Error).message);
- }
- }
-
- function deleteModel() {
- if (initLLMId) {
- httpClient
- .deleteProviderLLMModel(initLLMId)
- .then(() => {
- onLLMDeleted();
- toast.success(t('models.deleteSuccess'));
- })
- .catch((err) => {
- toast.error(t('models.deleteError') + err.message);
- });
- }
- }
-
- function testLLMModelInForm() {
- setModelTesting(true);
- setTestErrorMessage(null);
- const extraArgsObj: Record = {};
- form
- .getValues('extra_args')
- ?.forEach((arg: { key: string; type: string; value: string }) => {
- if (arg.type === 'number') {
- extraArgsObj[arg.key] = Number(arg.value);
- } else if (arg.type === 'boolean') {
- extraArgsObj[arg.key] = arg.value === 'true';
- } else {
- extraArgsObj[arg.key] = arg.value;
- }
- });
- const apiKey = form.getValues('api_key');
- httpClient
- .testLLMModel('_', {
- uuid: '',
- name: form.getValues('name'),
- description: '',
- requester: form.getValues('model_provider'),
- requester_config: {
- base_url: form.getValues('url'),
- timeout: 120,
- },
- api_keys: apiKey ? [apiKey] : [],
- abilities: form.getValues('abilities'),
- extra_args: extraArgsObj,
- })
- .then(() => {
- toast.success(t('models.testSuccess'));
- setTestErrorMessage(null);
- })
- .catch((err: { message?: string }) => {
- setTestErrorMessage(err?.message || t('models.testError'));
- })
- .finally(() => {
- setModelTesting(false);
- });
- }
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/web/src/app/home/models/page.tsx b/web/src/app/home/models/page.tsx
deleted file mode 100644
index 7c33918e..00000000
--- a/web/src/app/home/models/page.tsx
+++ /dev/null
@@ -1,268 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO';
-import styles from './LLMConfig.module.css';
-import LLMCard from '@/app/home/models/component/llm-card/LLMCard';
-import LLMForm from '@/app/home/models/component/llm-form/LLMForm';
-import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent';
-import { httpClient } from '@/app/infra/http/HttpClient';
-import { LLMModel } from '@/app/infra/entities/api';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import { toast } from 'sonner';
-import { useTranslation } from 'react-i18next';
-import { extractI18nObject } from '@/i18n/I18nProvider';
-import { EmbeddingCardVO } from '@/app/home/models/component/embedding-card/EmbeddingCardVO';
-import EmbeddingCard from '@/app/home/models/component/embedding-card/EmbeddingCard';
-import EmbeddingForm from '@/app/home/models/component/embedding-form/EmbeddingForm';
-
-export default function LLMConfigPage() {
- const { t } = useTranslation();
- const [cardList, setCardList] = useState([]);
- const [modalOpen, setModalOpen] = useState(false);
- const [isEditForm, setIsEditForm] = useState(false);
- const [nowSelectedLLM, setNowSelectedLLM] = useState(null);
- const [embeddingCardList, setEmbeddingCardList] = useState(
- [],
- );
- const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false);
- const [isEditEmbeddingForm, setIsEditEmbeddingForm] = useState(false);
- const [nowSelectedEmbedding, setNowSelectedEmbedding] =
- useState(null);
-
- useEffect(() => {
- getLLMModelList();
- getEmbeddingModelList();
- }, []);
-
- async function getLLMModelList() {
- const requesterNameListResp = await httpClient.getProviderRequesters('llm');
- const requesterNameList = requesterNameListResp.requesters.map((item) => {
- return {
- label: extractI18nObject(item.label),
- value: item.name,
- };
- });
-
- httpClient
- .getProviderLLMModels()
- .then((resp) => {
- const llmModelList: LLMCardVO[] = resp.models.map((model: LLMModel) => {
- return new LLMCardVO({
- id: model.uuid,
- iconURL: httpClient.getProviderRequesterIconURL(model.requester),
- name: model.name,
- providerLabel:
- requesterNameList.find((item) => item.value === model.requester)
- ?.label || model.requester.substring(0, 10),
- baseURL: model.requester_config?.base_url,
- abilities: model.abilities || [],
- });
- });
- setCardList(llmModelList);
- })
- .catch((err) => {
- console.error('get LLM model list error', err);
- toast.error(t('models.getModelListError') + err.message);
- });
- }
-
- function selectLLM(cardVO: LLMCardVO) {
- setIsEditForm(true);
- setNowSelectedLLM(cardVO);
- setModalOpen(true);
- }
- function handleCreateModelClick() {
- setIsEditForm(false);
- setNowSelectedLLM(null);
- setModalOpen(true);
- }
- function selectEmbedding(cardVO: EmbeddingCardVO) {
- setIsEditEmbeddingForm(true);
- setNowSelectedEmbedding(cardVO);
- setEmbeddingModalOpen(true);
- }
-
- function handleCreateEmbeddingModelClick() {
- setIsEditEmbeddingForm(false);
- setNowSelectedEmbedding(null);
- setEmbeddingModalOpen(true);
- }
- async function getEmbeddingModelList() {
- const requesterNameListResp =
- await httpClient.getProviderRequesters('text-embedding');
- const requesterNameList = requesterNameListResp.requesters.map((item) => {
- return {
- label: extractI18nObject(item.label),
- value: item.name,
- };
- });
-
- httpClient
- .getProviderEmbeddingModels()
- .then((resp) => {
- const embeddingModelList: EmbeddingCardVO[] = resp.models.map(
- (model: {
- uuid: string;
- requester: string;
- name: string;
- requester_config?: { base_url?: string };
- }) => {
- return new EmbeddingCardVO({
- id: model.uuid,
- iconURL: httpClient.getProviderRequesterIconURL(model.requester),
- name: model.name,
- providerLabel:
- requesterNameList.find((item) => item.value === model.requester)
- ?.label || model.requester.substring(0, 10),
- baseURL: model.requester_config?.base_url || '',
- });
- },
- );
- setEmbeddingCardList(embeddingModelList);
- })
- .catch((err) => {
- console.error('get Embedding model list error', err);
- toast.error(t('embedding.getModelListError') + err.message);
- });
- }
-
- return (
-
-
-
-
-
-
-
-
-
- {t('llm.llmModels')}
-
-
- {t('embedding.embeddingModels')}
-
-
-
-
-
-
- {t('llm.description')}
-
-
-
-
-
-
- {t('embedding.description')}
-
-
-
-
-
-
-
-
- {cardList.map((cardVO) => {
- return (
-
{
- selectLLM(cardVO);
- }}
- >
-
-
- );
- })}
-
-
-
-
-
-
- {embeddingCardList.map((cardVO) => {
- return (
-
{
- selectEmbedding(cardVO);
- }}
- >
-
-
- );
- })}
-
-
-
-
- );
-}
diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx
index 6f86d6ef..65bbc526 100644
--- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx
+++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx
@@ -181,7 +181,7 @@ export default function PipelineFormComponent({
toast.success(t('pipelines.createSuccess'));
})
.catch((err) => {
- toast.error(t('pipelines.createError') + err.message);
+ toast.error(t('pipelines.createError') + err.msg);
});
}
@@ -211,7 +211,7 @@ export default function PipelineFormComponent({
toast.success(t('pipelines.saveSuccess'));
})
.catch((err) => {
- toast.error(t('pipelines.saveError') + err.message);
+ toast.error(t('pipelines.saveError') + err.msg);
});
}
@@ -340,7 +340,7 @@ export default function PipelineFormComponent({
toast.success(t('pipelines.deleteSuccess'));
})
.catch((err) => {
- toast.error(t('pipelines.deleteError') + err.message);
+ toast.error(t('pipelines.deleteError') + err.msg);
});
}
};
@@ -360,7 +360,7 @@ export default function PipelineFormComponent({
onCancel();
})
.catch((err) => {
- toast.error(t('pipelines.createError') + err.message);
+ toast.error(t('pipelines.createError') + err.msg);
});
}
};
diff --git a/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx
index b29877c8..c15c349d 100644
--- a/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx
+++ b/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx
@@ -11,7 +11,7 @@ import {
Settings,
FileText,
} from 'lucide-react';
-import { getCloudServiceClientSync } from '@/app/infra/http';
+import { getCloudServiceClientSync, systemInfo } from '@/app/infra/http';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Button } from '@/components/ui/button';
import {
@@ -203,6 +203,7 @@ export default function PluginCardComponent({
} else if (cardVO.install_source === 'marketplace') {
window.open(
getCloudServiceClientSync().getPluginMarketplaceURL(
+ systemInfo.cloud_service_url,
cardVO.author,
cardVO.name,
),
diff --git a/web/src/app/home/plugins/components/plugin-sort/PluginSortDialog.tsx b/web/src/app/home/plugins/components/plugin-sort/PluginSortDialog.tsx
deleted file mode 100644
index 1c601e77..00000000
--- a/web/src/app/home/plugins/components/plugin-sort/PluginSortDialog.tsx
+++ /dev/null
@@ -1,214 +0,0 @@
-// 'use client';
-
-// import * as React from 'react';
-// import { useState, useEffect } from 'react';
-// import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO';
-// import { httpClient } from '@/app/infra/http/HttpClient';
-// import { PluginReorderElement } from '@/app/infra/entities/api';
-// import { toast } from 'sonner';
-// import {
-// Dialog,
-// DialogContent,
-// DialogHeader,
-// DialogTitle,
-// DialogFooter,
-// } from '@/components/ui/dialog';
-// import { Button } from '@/components/ui/button';
-// import {
-// DndContext,
-// closestCenter,
-// KeyboardSensor,
-// PointerSensor,
-// useSensor,
-// useSensors,
-// DragEndEvent,
-// } from '@dnd-kit/core';
-// import {
-// arrayMove,
-// SortableContext,
-// sortableKeyboardCoordinates,
-// useSortable,
-// verticalListSortingStrategy,
-// } from '@dnd-kit/sortable';
-// import { CSS } from '@dnd-kit/utilities';
-// import { useTranslation } from 'react-i18next';
-// import { extractI18nObject } from '@/i18n/I18nProvider';
-
-// interface PluginSortDialogProps {
-// open: boolean;
-// onOpenChange: (open: boolean) => void;
-// onSortComplete: () => void;
-// }
-
-// function SortablePluginItem({ plugin }: { plugin: PluginCardVO }) {
-// const { attributes, listeners, setNodeRef, transform, transition } =
-// useSortable({
-// id: `${plugin.author}-${plugin.name}`,
-// });
-
-// const style = {
-// transform: CSS.Transform.toString(transform),
-// transition,
-// };
-
-// return (
-//
-//
-//
-// {plugin.author}
-//
-//
{plugin.name}
-//
-// {plugin.description}
-//
-//
-//
-// );
-// }
-
-// export default function PluginSortDialog({
-// open,
-// onOpenChange,
-// onSortComplete,
-// }: PluginSortDialogProps) {
-// const { t } = useTranslation();
-// const [sortedPlugins, setSortedPlugins] = useState([]);
-// const [isLoading, setIsLoading] = useState(false);
-
-// function getPluginList() {
-// httpClient.getPlugins().then((value) => {
-// setSortedPlugins(
-// value.plugins.map((plugin) => {
-// return new PluginCardVO({
-// author: plugin.manifest.manifest.metadata.author ?? '',
-// description: extractI18nObject(
-// plugin.manifest.manifest.metadata.description ?? {
-// en_US: '',
-// zh_Hans: '',
-// },
-// ),
-// enabled: plugin.enabled,
-// name: plugin.manifest.manifest.metadata.name,
-// version: plugin.manifest.manifest.metadata.version ?? '',
-// status: plugin.status,
-// components: plugin.components,
-// install_source: plugin.install_source,
-// install_info: plugin.install_info,
-// priority: plugin.priority,
-// debug: plugin.debug,
-// });
-// }),
-// );
-// });
-// }
-
-// useEffect(() => {
-// if (open) {
-// getPluginList();
-// }
-// }, [open]);
-
-// const sensors = useSensors(
-// useSensor(PointerSensor),
-// useSensor(KeyboardSensor, {
-// coordinateGetter: sortableKeyboardCoordinates,
-// }),
-// );
-
-// function handleDragEnd(event: DragEndEvent) {
-// const { active, over } = event;
-
-// if (over && active.id !== over.id) {
-// setSortedPlugins((items) => {
-// const oldIndex = items.findIndex(
-// (item) => `${item.author}-${item.name}` === active.id,
-// );
-// const newIndex = items.findIndex(
-// (item) => `${item.author}-${item.name}` === over.id,
-// );
-
-// const newItems = arrayMove(items, oldIndex, newIndex);
-
-// return newItems;
-// });
-// }
-// }
-
-// function handleSave() {
-// setIsLoading(true);
-
-// const reorderElements: PluginReorderElement[] = sortedPlugins.map(
-// (plugin, index) => ({
-// author: plugin.author,
-// name: plugin.name,
-// priority: index,
-// }),
-// );
-
-// httpClient
-// .reorderPlugins(reorderElements)
-// .then(() => {
-// toast.success(t('plugins.pluginSortSuccess'));
-// onSortComplete();
-// onOpenChange(false);
-// })
-// .catch((err) => {
-// toast.error(t('plugins.pluginSortError') + err.message);
-// })
-// .finally(() => {
-// setIsLoading(false);
-// });
-// }
-
-// return (
-//
-// );
-// }
diff --git a/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx b/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx
index 937da834..2ae43a9c 100644
--- a/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx
+++ b/web/src/app/home/plugins/mcp-server/mcp-card/MCPCardComponent.tsx
@@ -41,7 +41,7 @@ export default function MCPCardComponent({
setSwitchEnable(true);
})
.catch((err) => {
- toast.error(t('mcp.modifyFailed') + err.message);
+ toast.error(t('mcp.modifyFailed') + err.msg);
setSwitchEnable(true);
});
}
@@ -76,7 +76,7 @@ export default function MCPCardComponent({
}, 1000);
})
.catch((err) => {
- toast.error(t('mcp.refreshFailed') + err.message);
+ toast.error(t('mcp.refreshFailed') + err.msg);
setTesting(false);
});
}
diff --git a/web/src/app/home/plugins/mcp-server/mcp-form/MCPFormDialog.tsx b/web/src/app/home/plugins/mcp-server/mcp-form/MCPFormDialog.tsx
index 2a0bf35a..72901d87 100644
--- a/web/src/app/home/plugins/mcp-server/mcp-form/MCPFormDialog.tsx
+++ b/web/src/app/home/plugins/mcp-server/mcp-form/MCPFormDialog.tsx
@@ -44,6 +44,7 @@ import {
MCPServer,
MCPSessionStatus,
} from '@/app/infra/entities/api';
+import { CustomApiError } from '@/app/infra/entities/common';
// Status Display Component - 在测试中、连接中或连接失败时使用
function StatusDisplay({
@@ -409,7 +410,8 @@ export default function MCPFormDialog({
} catch (err) {
clearInterval(interval);
setMcpTesting(false);
- const errorMsg = (err as Error).message || t('mcp.getTaskFailed');
+ const errorMsg =
+ (err as CustomApiError).msg || t('mcp.getTaskFailed');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
}
}, 1000);
diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx
index 822364d6..594f3829 100644
--- a/web/src/app/home/plugins/page.tsx
+++ b/web/src/app/home/plugins/page.tsx
@@ -282,7 +282,7 @@ export default function PluginConfigPage() {
watchTask(taskId);
})
.catch((err) => {
- setInstallError(err.message);
+ setInstallError(err.msg);
setPluginInstallStatus(PluginInstallStatus.ERROR);
});
} else if (installSource === 'local') {
@@ -293,7 +293,7 @@ export default function PluginConfigPage() {
watchTask(taskId);
})
.catch((err) => {
- setInstallError(err.message);
+ setInstallError(err.msg);
setPluginInstallStatus(PluginInstallStatus.ERROR);
});
} else if (installSource === 'marketplace') {
diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts
index 9ae309ac..c0e79fc0 100644
--- a/web/src/app/infra/entities/api/index.ts
+++ b/web/src/app/infra/entities/api/index.ts
@@ -41,20 +41,33 @@ export interface ApiRespProviderLLMModel {
model: LLMModel;
}
-export interface LLMModel {
- name: string;
- description: string;
+export interface ModelProvider {
uuid: string;
+ name: string;
requester: string;
- requester_config: {
- base_url: string;
- timeout: number;
- };
- extra_args?: object;
+ base_url: string;
api_keys: string[];
+ llm_count?: number;
+ embedding_count?: number;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface ApiRespModelProviders {
+ providers: ModelProvider[];
+}
+
+export interface ApiRespModelProvider {
+ provider: ModelProvider;
+}
+
+export interface LLMModel {
+ uuid: string;
+ name: string;
+ provider_uuid: string;
+ provider?: ModelProvider;
abilities?: string[];
- // created_at: string;
- // updated_at: string;
+ extra_args?: object;
}
export interface KnowledgeBase {
@@ -76,18 +89,11 @@ export interface ApiRespProviderEmbeddingModel {
}
export interface EmbeddingModel {
- name: string;
- description: string;
uuid: string;
- requester: string;
- requester_config: {
- base_url: string;
- timeout: number;
- };
+ name: string;
+ provider_uuid: string;
+ provider?: ModelProvider;
extra_args?: object;
- api_keys: string[];
- // created_at: string;
- // updated_at: string;
}
export interface ApiRespPipelines {
@@ -235,7 +241,8 @@ export interface ApiRespSystemInfo {
version: string;
cloud_service_url: string;
enable_marketplace: boolean;
- allow_change_password: boolean;
+ allow_modify_login_info: boolean;
+ disable_models_service: boolean;
}
export interface ApiRespPluginSystemStatus {
diff --git a/web/src/app/infra/entities/common.ts b/web/src/app/infra/entities/common.ts
index 64331738..729aa77a 100644
--- a/web/src/app/infra/entities/common.ts
+++ b/web/src/app/infra/entities/common.ts
@@ -19,3 +19,7 @@ export interface ComponentManifest {
};
spec: Record; // eslint-disable-line @typescript-eslint/no-explicit-any
}
+
+export interface CustomApiError {
+ msg?: string;
+}
diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts
index 8dd7f1a0..dd4b1feb 100644
--- a/web/src/app/infra/http/BackendClient.ts
+++ b/web/src/app/infra/http/BackendClient.ts
@@ -38,6 +38,9 @@ import {
ExternalKnowledgeBase,
ApiRespExternalKnowledgeBases,
ApiRespExternalKnowledgeBase,
+ ApiRespModelProviders,
+ ApiRespModelProvider,
+ ModelProvider,
} from '@/app/infra/entities/api';
import { Plugin } from '@/app/infra/entities/plugin';
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
@@ -54,7 +57,7 @@ export class BackendClient extends BaseHttpClient {
// ============ Provider API ============
public getProviderRequesters(
- model_type: string,
+ model_type?: string,
): Promise {
return this.get('/api/v1/provider/requesters', { type: model_type });
}
@@ -65,7 +68,6 @@ export class BackendClient extends BaseHttpClient {
public getProviderRequesterIconURL(name: string): string {
if (this.instance.defaults.baseURL === '/') {
- // 获取用户访问的URL
const url = window.location.href;
const baseURL = url.split('/').slice(0, 3).join('/');
return `${baseURL}/api/v1/provider/requesters/${name}/icon`;
@@ -76,9 +78,38 @@ export class BackendClient extends BaseHttpClient {
);
}
+ // ============ Model Providers ============
+ public getModelProviders(): Promise {
+ return this.get('/api/v1/provider/providers');
+ }
+
+ public getModelProvider(uuid: string): Promise {
+ return this.get(`/api/v1/provider/providers/${uuid}`);
+ }
+
+ public createModelProvider(
+ provider: Omit,
+ ): Promise<{ uuid: string }> {
+ return this.post('/api/v1/provider/providers', provider);
+ }
+
+ public updateModelProvider(
+ uuid: string,
+ provider: Partial,
+ ): Promise