chore: update model handlers on the new frontend (#5011)

* chore: provide model handlers to new frontend

* chore: add API server function to the new front end
This commit is contained in:
Louis 2025-05-19 10:39:43 +07:00 committed by GitHub
parent 74c2c59c90
commit 2345ff172d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 124 additions and 22 deletions

View File

@ -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(() => {

View File

@ -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<string>('')
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 = ({
</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="destructive"
size="sm"
onClick={() => {
toast.success('Delete Model', {
id: `delete-model-${selectedModel.id}`,
description: `Model ${selectedModel.id} has been permanently deleted.`,
})
}}
>
<Button variant="destructive" size="sm" onClick={removeModel}>
Delete
</Button>
</DialogClose>

View File

@ -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<LocalApiServerState>()(
@ -38,6 +41,8 @@ export const useLocalApiServer = create<LocalApiServerState>()(
setCorsEnabled: (value) => set({ corsEnabled: value }),
verboseLogs: true,
setVerboseLogs: (value) => set({ verboseLogs: value }),
serverStatus: 'stopped',
setServerStatus: (value) => set({ serverStatus: value }),
}),
{
name: localStoregeKey.settingLocalApiServer,

View File

@ -13,6 +13,7 @@ type ModelProviderState = {
providerName: string,
modelName: string
) => Model | undefined
deleteModel: (modelId: string) => void
}
export const useModelProvider = create<ModelProviderState>()(
@ -31,7 +32,9 @@ export const useModelProvider = create<ModelProviderState>()(
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<ModelProviderState>()(
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,

View File

@ -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.
</p>
</div>
<Button>Start Server</Button>
<Button onClick={toggleAPIServer}>
{`${serverStatus === 'running' ? 'Stop' : 'Start'}`}{' '}
Server
</Button>
</div>
}
>

View File

@ -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 && (
<ModelSetting provider={provider} model={model} />
)}
<DialoDeleteModel
<DialogDeleteModel
provider={provider}
modelId={model.id}
/>

View File

@ -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<ModelExtension>(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<ModelExtension>(
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
}
}

View File

@ -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<ModelProvider[]> => {
const builtinProviders = mockModelProvider.map((provider) => {
@ -42,8 +43,9 @@ export const getProviders = async (): Promise<ModelProvider[]> => {
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 &&