diff --git a/core/src/browser/extensions/enginesManagement.ts b/core/src/browser/extensions/enginesManagement.ts index 66dff87df6..6986e8f480 100644 --- a/core/src/browser/extensions/enginesManagement.ts +++ b/core/src/browser/extensions/enginesManagement.ts @@ -5,6 +5,7 @@ import { EngineReleased, EngineConfig, DefaultEngineVariant, + Model, } from '../../types' import { BaseExtension, ExtensionTypeEnum } from '../extension' @@ -103,6 +104,11 @@ export abstract class EngineManagementExtension extends BaseExtension { engineConfig?: EngineConfig ): Promise<{ messages: string }> + /** + * Add a new remote model for a specific engine + */ + abstract addRemoteModel(model: Model): Promise + /** * @returns A Promise that resolves to an object of remote models list . */ diff --git a/core/src/types/model/modelEntity.ts b/core/src/types/model/modelEntity.ts index 7b67a8e942..482dfa1ac9 100644 --- a/core/src/types/model/modelEntity.ts +++ b/core/src/types/model/modelEntity.ts @@ -1,5 +1,3 @@ -import { FileMetadata } from '../file' - /** * Represents the information about a model. * @stored @@ -70,6 +68,11 @@ export type Model = { */ id: string + /** + * The model identifier, modern version of id. + */ + mode?: string + /** * Human-readable name that is used for UI. */ diff --git a/web/hooks/useEngineManagement.ts b/web/hooks/useEngineManagement.ts index 8367ecd20a..fec5eb0ed6 100644 --- a/web/hooks/useEngineManagement.ts +++ b/web/hooks/useEngineManagement.ts @@ -8,6 +8,8 @@ import { EngineConfig, events, EngineEvent, + Model, + ModelEvent, } from '@janhq/core' import { useAtom } from 'jotai' import { atomWithStorage } from 'jotai/utils' @@ -385,3 +387,31 @@ export const uninstallEngine = async ( throw error } } + +/** + * Add a new remote engine model + * @param name + * @param engine + * @returns + */ +export const addRemoteEngineModel = async (name: string, engine: string) => { + const extension = getExtension() + + if (!extension) { + throw new Error('Extension is not available') + } + + try { + // Call the extension's method + const response = await extension.addRemoteModel({ + id: name, + model: name, + engine: engine as InferenceEngine, + } as unknown as Model) + events.emit(ModelEvent.OnModelsUpdate, { fetch: true }) + return response + } catch (error) { + console.error('Failed to install engine variant:', error) + throw error + } +} diff --git a/web/screens/Settings/Engines/ModalAddModel.tsx b/web/screens/Settings/Engines/ModalAddModel.tsx new file mode 100644 index 0000000000..40c986e92c --- /dev/null +++ b/web/screens/Settings/Engines/ModalAddModel.tsx @@ -0,0 +1,155 @@ +import { memo, ReactNode, useState } from 'react' + +import { useForm } from 'react-hook-form' + +import Image from 'next/image' + +import { zodResolver } from '@hookform/resolvers/zod' + +import { InferenceEngine, Model } from '@janhq/core' + +import { Button, Input, Modal } from '@janhq/joi' +import { useAtomValue } from 'jotai' +import { PlusIcon } from 'lucide-react' + +import { z } from 'zod' + +import { + addRemoteEngineModel, + useGetEngines, + useGetRemoteModels, +} from '@/hooks/useEngineManagement' + +import { getLogoEngine, getTitleByEngine } from '@/utils/modelEngine' + +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' + +const modelSchema = z.object({ + modelName: z.string().min(1, 'Model name is required'), +}) + +const ModelAddModel = ({ engine }: { engine: string }) => { + const [open, setOpen] = useState(false) + const { mutate: mutateListEngines } = useGetRemoteModels(engine) + const { engines } = useGetEngines() + const models = useAtomValue(downloadedModelsAtom) + const { + register, + handleSubmit, + formState: { errors }, + setError, + } = useForm({ + resolver: zodResolver(modelSchema), + defaultValues: { + modelName: '', + }, + }) + + const onSubmit = async (data: z.infer) => { + if (models.some((e: Model) => e.id === data.modelName)) { + setError('modelName', { + type: 'manual', + message: 'Model already exists', + }) + return + } + await addRemoteEngineModel(data.modelName, engine) + mutateListEngines() + + setOpen(false) + } + + // Helper to render labels with asterisks for required fields + const renderLabel = ( + prefix: ReactNode, + label: string, + isRequired: boolean, + desc?: string + ) => ( + <> + + {prefix} + {label} + +

+ {desc} + {isRequired && *} +

+ + ) + + return ( + +

Add Model

+ + } + fullPage + open={open} + onOpenChange={() => setOpen(!open)} + trigger={ + + } + className="w-[500px]" + content={ +
+
+
+ + + {errors.modelName && ( +

+ {errors.modelName.message} +

+ )} + +
+ +
+ + +
+
+
+ } + /> + ) +} + +export default memo(ModelAddModel) diff --git a/web/screens/Settings/Engines/RemoteEngineSettings.tsx b/web/screens/Settings/Engines/RemoteEngineSettings.tsx index ea2b90b164..83aa07ba44 100644 --- a/web/screens/Settings/Engines/RemoteEngineSettings.tsx +++ b/web/screens/Settings/Engines/RemoteEngineSettings.tsx @@ -27,6 +27,8 @@ import { updateEngine, useGetEngines } from '@/hooks/useEngineManagement' import { getTitleByEngine } from '@/utils/modelEngine' +import ModalAddModel from './ModalAddModel' + import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' const RemoteEngineSettings = ({ @@ -194,10 +196,11 @@ const RemoteEngineSettings = ({
-
+
Model
+