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' } from '@/components/ui/popover'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import { useDownloadStore } from '@/hooks/useDownloadStore' import { useDownloadStore } from '@/hooks/useDownloadStore'
import { useModelProvider } from '@/hooks/useModelProvider'
import { abortDownload } from '@/services/models' import { abortDownload } from '@/services/models'
import { getProviders } from '@/services/providers'
import { DownloadEvent, DownloadState, events } from '@janhq/core' import { DownloadEvent, DownloadState, events } from '@janhq/core'
import { IconX } from '@tabler/icons-react' import { IconX } from '@tabler/icons-react'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo } from 'react'
export function DownloadManagement() { export function DownloadManagement() {
const { setProviders } = useModelProvider()
const { downloads, updateProgress, removeDownload } = useDownloadStore() const { downloads, updateProgress, removeDownload } = useDownloadStore()
const downloadCount = useMemo( const downloadCount = useMemo(
() => Object.keys(downloads).length, () => Object.keys(downloads).length,
@ -72,8 +75,9 @@ export function DownloadManagement() {
async (state: DownloadState) => { async (state: DownloadState) => {
console.debug('onFileDownloadSuccess', state) console.debug('onFileDownloadSuccess', state)
removeDownload(state.modelId) removeDownload(state.modelId)
getProviders().then(setProviders)
}, },
[removeDownload] [removeDownload, setProviders]
) )
useEffect(() => { useEffect(() => {

View File

@ -9,22 +9,37 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@/components/ui/dialog' } 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 { IconTrash } from '@tabler/icons-react'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
type DialoDeleteModelProps = { type DialogDeleteModelProps = {
provider: ModelProvider provider: ModelProvider
modelId?: string modelId?: string
} }
export const DialoDeleteModel = ({ export const DialogDeleteModel = ({
provider, provider,
modelId, modelId,
}: DialoDeleteModelProps) => { }: DialogDeleteModelProps) => {
const [selectedModelId, setSelectedModelId] = useState<string>('') 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 // Initialize with the provided model ID or the first model if available
useEffect(() => { useEffect(() => {
@ -68,16 +83,7 @@ export const DialoDeleteModel = ({
</Button> </Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <DialogClose asChild>
<Button <Button variant="destructive" size="sm" onClick={removeModel}>
variant="destructive"
size="sm"
onClick={() => {
toast.success('Delete Model', {
id: `delete-model-${selectedModel.id}`,
description: `Model ${selectedModel.id} has been permanently deleted.`,
})
}}
>
Delete Delete
</Button> </Button>
</DialogClose> </DialogClose>

View File

@ -21,6 +21,9 @@ type LocalApiServerState = {
// Verbose server logs // Verbose server logs
verboseLogs: boolean verboseLogs: boolean
setVerboseLogs: (value: boolean) => void setVerboseLogs: (value: boolean) => void
// Server status
serverStatus: 'running' | 'stopped' | 'pending'
setServerStatus: (value: 'running' | 'stopped' | 'pending') => void
} }
export const useLocalApiServer = create<LocalApiServerState>()( export const useLocalApiServer = create<LocalApiServerState>()(
@ -38,6 +41,8 @@ export const useLocalApiServer = create<LocalApiServerState>()(
setCorsEnabled: (value) => set({ corsEnabled: value }), setCorsEnabled: (value) => set({ corsEnabled: value }),
verboseLogs: true, verboseLogs: true,
setVerboseLogs: (value) => set({ verboseLogs: value }), setVerboseLogs: (value) => set({ verboseLogs: value }),
serverStatus: 'stopped',
setServerStatus: (value) => set({ serverStatus: value }),
}), }),
{ {
name: localStoregeKey.settingLocalApiServer, name: localStoregeKey.settingLocalApiServer,

View File

@ -13,6 +13,7 @@ type ModelProviderState = {
providerName: string, providerName: string,
modelName: string modelName: string
) => Model | undefined ) => Model | undefined
deleteModel: (modelId: string) => void
} }
export const useModelProvider = create<ModelProviderState>()( export const useModelProvider = create<ModelProviderState>()(
@ -31,7 +32,9 @@ export const useModelProvider = create<ModelProviderState>()(
const models = existingProvider?.models || [] const models = existingProvider?.models || []
const mergedModels = [ const mergedModels = [
...(provider?.models ?? []), ...(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 { return {
...provider, ...provider,
@ -98,6 +101,19 @@ export const useModelProvider = create<ModelProviderState>()(
return modelObject 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, name: localStoregeKey.modelProvider,

View File

@ -19,8 +19,44 @@ export const Route = createFileRoute(route.settings.local_api_server as any)({
function LocalAPIServer() { function LocalAPIServer() {
const { t } = useTranslation() const { t } = useTranslation()
const { corsEnabled, setCorsEnabled, verboseLogs, setVerboseLogs } = const {
useLocalApiServer() 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 () => { const handleOpenLogs = async () => {
try { try {
@ -78,7 +114,10 @@ function LocalAPIServer() {
Start an OpenAI-compatible local HTTP server. Start an OpenAI-compatible local HTTP server.
</p> </p>
</div> </div>
<Button>Start Server</Button> <Button onClick={toggleAPIServer}>
{`${serverStatus === 'running' ? 'Stop' : 'Start'}`}{' '}
Server
</Button>
</div> </div>
} }
> >

View File

@ -18,7 +18,7 @@ import { RenderMarkdown } from '@/containers/RenderMarkdown'
import { DialogEditModel } from '@/containers/dialogs/EditModel' import { DialogEditModel } from '@/containers/dialogs/EditModel'
import { DialogAddModel } from '@/containers/dialogs/AddModel' import { DialogAddModel } from '@/containers/dialogs/AddModel'
import { ModelSetting } from '@/containers/ModelSetting' 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 Joyride, { CallBackProps, STATUS } from 'react-joyride'
import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide' import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide'
import { route } from '@/constants/routes' import { route } from '@/constants/routes'
@ -250,7 +250,7 @@ function ProviderDetail() {
{model.settings && ( {model.settings && (
<ModelSetting provider={provider} model={model} /> <ModelSetting provider={provider} model={model} />
)} )}
<DialoDeleteModel <DialogDeleteModel
provider={provider} provider={provider}
modelId={model.id} 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. * Adds a new model source.
* @param source The source to add. * @param source The source to add.
@ -137,3 +147,23 @@ export const abortDownload = async (id: string) => {
throw error 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 { models as providerModels } from 'token.js'
import { mockModelProvider } from '@/mock/data' import { mockModelProvider } from '@/mock/data'
import { EngineManager, ModelManager } from '@janhq/core' import { EngineManager } from '@janhq/core'
import { ModelCapabilities } from '@/types/models' import { ModelCapabilities } from '@/types/models'
import { modelSettings } from '@/lib/predefined' import { modelSettings } from '@/lib/predefined'
import { fetchModels } from './models'
export const getProviders = async (): Promise<ModelProvider[]> => { export const getProviders = async (): Promise<ModelProvider[]> => {
const builtinProviders = mockModelProvider.map((provider) => { const builtinProviders = mockModelProvider.map((provider) => {
@ -42,8 +43,9 @@ export const getProviders = async (): Promise<ModelProvider[]> => {
for (const [key, value] of EngineManager.instance().engines) { for (const [key, value] of EngineManager.instance().engines) {
// TODO: Remove this when the cortex extension is removed // TODO: Remove this when the cortex extension is removed
const providerName = key === 'cortex' ? 'llama.cpp' : key const providerName = key === 'cortex' ? 'llama.cpp' : key
const models = const models =
Array.from(ModelManager.instance().models.values()).filter( ((await fetchModels()) ?? []).filter(
(model) => (model) =>
(model.engine === 'llama-cpp' ? 'llama.cpp' : model.engine) === (model.engine === 'llama-cpp' ? 'llama.cpp' : model.engine) ===
providerName && providerName &&