diff --git a/web-app/src/containers/DownloadManegement.tsx b/web-app/src/containers/DownloadManegement.tsx index 4aecfc5fd..89cd0ddfe 100644 --- a/web-app/src/containers/DownloadManegement.tsx +++ b/web-app/src/containers/DownloadManegement.tsx @@ -5,12 +5,15 @@ import { } from '@/components/ui/popover' import { Progress } from '@/components/ui/progress' import { useDownloadStore } from '@/hooks/useDownloadStore' +import { useModelProvider } from '@/hooks/useModelProvider' import { abortDownload } from '@/services/models' +import { getProviders } from '@/services/providers' import { DownloadEvent, DownloadState, events } from '@janhq/core' import { IconX } from '@tabler/icons-react' import { useCallback, useEffect, useMemo } from 'react' export function DownloadManagement() { + const { setProviders } = useModelProvider() const { downloads, updateProgress, removeDownload } = useDownloadStore() const downloadCount = useMemo( () => Object.keys(downloads).length, @@ -72,8 +75,9 @@ export function DownloadManagement() { async (state: DownloadState) => { console.debug('onFileDownloadSuccess', state) removeDownload(state.modelId) + getProviders().then(setProviders) }, - [removeDownload] + [removeDownload, setProviders] ) useEffect(() => { diff --git a/web-app/src/containers/dialogs/DeleteModel.tsx b/web-app/src/containers/dialogs/DeleteModel.tsx index 9d428a502..60e86debe 100644 --- a/web-app/src/containers/dialogs/DeleteModel.tsx +++ b/web-app/src/containers/dialogs/DeleteModel.tsx @@ -9,22 +9,37 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog' +import { useModelProvider } from '@/hooks/useModelProvider' +import { deleteModel } from '@/services/models' +import { getProviders } from '@/services/providers' import { IconTrash } from '@tabler/icons-react' import { useState, useEffect } from 'react' import { toast } from 'sonner' -type DialoDeleteModelProps = { +type DialogDeleteModelProps = { provider: ModelProvider modelId?: string } -export const DialoDeleteModel = ({ +export const DialogDeleteModel = ({ provider, modelId, -}: DialoDeleteModelProps) => { +}: DialogDeleteModelProps) => { const [selectedModelId, setSelectedModelId] = useState('') + const { setProviders, deleteModel: deleteModelCache } = useModelProvider() + + const removeModel = async () => { + deleteModelCache(selectedModelId) + deleteModel(selectedModelId).then(() => { + getProviders().then(setProviders) + toast.success('Delete Model', { + id: `delete-model-${selectedModel?.id}`, + description: `Model ${selectedModel?.id} has been permanently deleted.`, + }) + }) + } // Initialize with the provided model ID or the first model if available useEffect(() => { @@ -68,16 +83,7 @@ export const DialoDeleteModel = ({ - diff --git a/web-app/src/hooks/useLocalApiServer.ts b/web-app/src/hooks/useLocalApiServer.ts index 65d1c1efa..efb6df71b 100644 --- a/web-app/src/hooks/useLocalApiServer.ts +++ b/web-app/src/hooks/useLocalApiServer.ts @@ -21,6 +21,9 @@ type LocalApiServerState = { // Verbose server logs verboseLogs: boolean setVerboseLogs: (value: boolean) => void + // Server status + serverStatus: 'running' | 'stopped' | 'pending' + setServerStatus: (value: 'running' | 'stopped' | 'pending') => void } export const useLocalApiServer = create()( @@ -38,6 +41,8 @@ export const useLocalApiServer = create()( setCorsEnabled: (value) => set({ corsEnabled: value }), verboseLogs: true, setVerboseLogs: (value) => set({ verboseLogs: value }), + serverStatus: 'stopped', + setServerStatus: (value) => set({ serverStatus: value }), }), { name: localStoregeKey.settingLocalApiServer, diff --git a/web-app/src/hooks/useModelProvider.ts b/web-app/src/hooks/useModelProvider.ts index 04288fea1..24a2084cc 100644 --- a/web-app/src/hooks/useModelProvider.ts +++ b/web-app/src/hooks/useModelProvider.ts @@ -13,6 +13,7 @@ type ModelProviderState = { providerName: string, modelName: string ) => Model | undefined + deleteModel: (modelId: string) => void } export const useModelProvider = create()( @@ -31,7 +32,9 @@ export const useModelProvider = create()( const models = existingProvider?.models || [] const mergedModels = [ ...(provider?.models ?? []), - ...models.filter((e) => !provider?.models.some((m) => m.id === e.id)), + ...models.filter( + (e) => !provider?.models.some((m) => m.id === e.id) + ), ] return { ...provider, @@ -98,6 +101,19 @@ export const useModelProvider = create()( return modelObject }, + deleteModel: (modelId: string) => { + set((state) => ({ + providers: state.providers.map((provider) => { + const models = provider.models.filter( + (model) => model.id !== modelId + ) + return { + ...provider, + models, + } + }), + })) + }, }), { name: localStoregeKey.modelProvider, diff --git a/web-app/src/routes/settings/local-api-server.tsx b/web-app/src/routes/settings/local-api-server.tsx index 98839ba4f..07e0e7e29 100644 --- a/web-app/src/routes/settings/local-api-server.tsx +++ b/web-app/src/routes/settings/local-api-server.tsx @@ -19,8 +19,44 @@ export const Route = createFileRoute(route.settings.local_api_server as any)({ function LocalAPIServer() { const { t } = useTranslation() - const { corsEnabled, setCorsEnabled, verboseLogs, setVerboseLogs } = - useLocalApiServer() + const { + corsEnabled, + setCorsEnabled, + verboseLogs, + setVerboseLogs, + serverHost, + serverPort, + apiPrefix, + serverStatus, + setServerStatus, + } = useLocalApiServer() + + const toggleAPIServer = async () => { + setServerStatus('pending') + if (serverStatus === 'stopped') { + window.core?.api + ?.startServer({ + host: serverHost, + port: serverPort, + prefix: apiPrefix, + isCorsEnabled: corsEnabled, + isVerboseEnabled: verboseLogs, + }) + .then(() => { + setServerStatus('running') + }) + } else { + window.core?.api + ?.stopServer() + .then(() => { + setServerStatus('stopped') + }) + .catch((error: unknown) => { + console.error('Error stopping server:', error) + setServerStatus('stopped') + }) + } + } const handleOpenLogs = async () => { try { @@ -78,7 +114,10 @@ function LocalAPIServer() { Start an OpenAI-compatible local HTTP server.

- + } > diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 83dc77269..3e8e8a0df 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -18,7 +18,7 @@ import { RenderMarkdown } from '@/containers/RenderMarkdown' import { DialogEditModel } from '@/containers/dialogs/EditModel' import { DialogAddModel } from '@/containers/dialogs/AddModel' import { ModelSetting } from '@/containers/ModelSetting' -import { DialoDeleteModel } from '@/containers/dialogs/DeleteModel' +import { DialogDeleteModel } from '@/containers/dialogs/DeleteModel' import Joyride, { CallBackProps, STATUS } from 'react-joyride' import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide' import { route } from '@/constants/routes' @@ -250,7 +250,7 @@ function ProviderDetail() { {model.settings && ( )} - diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index a6420f8e4..575b882d5 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -35,6 +35,16 @@ export const fetchModelSources = async () => { } } +/** + * Fetches the model hub. + * @returns A promise that resolves to the model hub. + */ +export const fetchModelHub = async () => { + return ExtensionManager.getInstance() + .get(ExtensionTypeEnum.Model) + ?.fetchModelsHub() +} + /** * Adds a new model source. * @param source The source to add. @@ -137,3 +147,23 @@ export const abortDownload = async (id: string) => { throw error } } + +/** + * Deletes a model. + * @param id + * @returns + */ +export const deleteModel = async (id: string) => { + const extension = ExtensionManager.getInstance().get( + ExtensionTypeEnum.Model + ) + + if (!extension) throw new Error('Model extension not found') + + try { + return await extension.deleteModel(id) + } catch (error) { + console.error('Failed to delete model:', error) + throw error + } +} diff --git a/web-app/src/services/providers.ts b/web-app/src/services/providers.ts index 484b5255a..4c3b9e7b7 100644 --- a/web-app/src/services/providers.ts +++ b/web-app/src/services/providers.ts @@ -1,8 +1,9 @@ import { models as providerModels } from 'token.js' import { mockModelProvider } from '@/mock/data' -import { EngineManager, ModelManager } from '@janhq/core' +import { EngineManager } from '@janhq/core' import { ModelCapabilities } from '@/types/models' import { modelSettings } from '@/lib/predefined' +import { fetchModels } from './models' export const getProviders = async (): Promise => { const builtinProviders = mockModelProvider.map((provider) => { @@ -42,8 +43,9 @@ export const getProviders = async (): Promise => { for (const [key, value] of EngineManager.instance().engines) { // TODO: Remove this when the cortex extension is removed const providerName = key === 'cortex' ? 'llama.cpp' : key + const models = - Array.from(ModelManager.instance().models.values()).filter( + ((await fetchModels()) ?? []).filter( (model) => (model.engine === 'llama-cpp' ? 'llama.cpp' : model.engine) === providerName &&