feat: start and stop model (#5133)

* feat: start and stop model

* refactor: clean up start models

---------

Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
Faisal Amir 2025-05-29 13:23:12 +07:00 committed by GitHub
parent 72d1192499
commit 1b3f16b3e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 131 additions and 49 deletions

View File

@ -19,7 +19,6 @@ interface AvatarEmojiProps {
export const AvatarEmoji: React.FC<AvatarEmojiProps> = ({
avatar,
fallback = '👋',
imageClassName = 'w-5 h-5 object-contain',
textClassName = 'text-base',
}) => {
@ -27,5 +26,5 @@ export const AvatarEmoji: React.FC<AvatarEmojiProps> = ({
return <img src={avatar} alt="Custom avatar" className={imageClassName} />
}
return <span className={textClassName}>{avatar || fallback}</span>
return <span className={textClassName}>{avatar}</span>
}

View File

@ -29,7 +29,7 @@ import { useTheme } from '@/hooks/useTheme'
import { teamEmoji } from '@/utils/teamEmoji'
import { AvatarEmoji } from '@/containers/AvatarEmoji'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { cn, isDev } from '@/lib/utils'
interface AddEditAssistantProps {
open: boolean
@ -235,12 +235,10 @@ export default function AddEditAssistant({
>
<AvatarEmoji
avatar={avatar}
fallback={
<IconMoodSmile size={18} className="text-main-view-fg/50" />
}
imageClassName="w-5 h-5 object-contain"
textClassName=""
/>
<IconMoodSmile size={18} className="text-main-view-fg/50" />
</div>
<div className="relative" ref={emojiPickerRef}>
<EmojiPicker
@ -248,7 +246,7 @@ export default function AddEditAssistant({
theme={isDark ? ('dark' as Theme) : ('light' as Theme)}
className="!absolute !z-40 !overflow-y-auto top-2"
height={350}
customEmojis={teamEmoji}
customEmojis={isDev() ? teamEmoji : []}
lazyLoadEmojis
previewConfig={{ showPreview: false }}
onEmojiClick={(emojiData: EmojiClickData) => {

View File

@ -54,11 +54,11 @@ const defaultAppPrimaryBgColor: RgbaColor = { r: 219, g: 88, b: 44, a: 1 }
const defaultLightAppPrimaryBgColor: RgbaColor = { r: 219, g: 88, b: 44, a: 1 }
const defaultAppAccentBgColor: RgbaColor = { r: 45, g: 120, b: 220, a: 1 }
const defaultLightAppAccentBgColor: RgbaColor = { r: 45, g: 120, b: 220, a: 1 }
const defaultAppDestructiveBgColor: RgbaColor = { r: 220, g: 45, b: 45, a: 1 }
const defaultAppDestructiveBgColor: RgbaColor = { r: 144, g: 60, b: 60, a: 1 }
const defaultLightAppDestructiveBgColor: RgbaColor = {
r: 220,
g: 45,
b: 45,
r: 217,
g: 95,
b: 95,
a: 1,
}
const defaultDarkLeftPanelTextColor: string = '#FFF'

View File

@ -15,7 +15,6 @@ import {
newUserThreadContent,
postMessageProcessing,
sendCompletion,
startModel,
} from '@/lib/completion'
import { CompletionMessagesBuilder } from '@/lib/messages'
import { ChatCompletionMessageToolCall } from 'openai/resources'
@ -25,7 +24,7 @@ import { getTools } from '@/services/mcp'
import { MCPTool } from '@/types/completion'
import { listen } from '@tauri-apps/api/event'
import { SystemEvent } from '@/types/events'
import { stopModel } from '@/services/models'
import { stopModel, startModel } from '@/services/models'
export const useChat = () => {
const { prompt, setPrompt } = usePrompt()

View File

@ -163,39 +163,6 @@ export const isCompletionResponse = (
return 'choices' in response
}
/**
* @fileoverview Helper function to start a model.
* This function loads the model from the provider.
* @deprecated This function is deprecated and will be removed in the future.
* Provider's chat function will handle loading the model.
* @param provider
* @param model
* @returns
*/
export const startModel = async (
provider: ProviderObject,
model: string,
abortController?: AbortController
): Promise<void> => {
const providerObj = EngineManager.instance().get(
normalizeProvider(provider.provider)
)
const modelObj = provider.models.find((m) => m.id === model)
if (providerObj && modelObj)
return providerObj?.loadModel(
{
id: modelObj.id,
settings: Object.fromEntries(
Object.entries(modelObj.settings ?? {}).map(([key, value]) => [
key,
value.controller_props?.value, // assuming each setting is { value: ... }
])
),
},
abortController
)
}
/**
* @fileoverview Helper function to stop a model.
* This function unloads the model from the provider.

View File

@ -5,7 +5,12 @@ import { useModelProvider } from '@/hooks/useModelProvider'
import { cn, getProviderTitle } from '@/lib/utils'
import { Switch } from '@/components/ui/switch'
import { open } from '@tauri-apps/plugin-dialog'
import { importModel } from '@/services/models'
import {
getActiveModels,
importModel,
startModel,
stopModel,
} from '@/services/models'
import {
createFileRoute,
Link,
@ -27,9 +32,11 @@ import { route } from '@/constants/routes'
import DeleteProvider from '@/containers/dialogs/DeleteProvider'
import { updateSettings } from '@/services/providers'
import { Button } from '@/components/ui/button'
import { IconFolderPlus } from '@tabler/icons-react'
import { IconFolderPlus, IconLoader } from '@tabler/icons-react'
import { getProviders } from '@/services/providers'
import { toast } from 'sonner'
import { ActiveModel } from '@/types/models'
import { useEffect, useState } from 'react'
// as route.threadsDetail
export const Route = createFileRoute('/settings/providers/$providerName')({
@ -67,12 +74,26 @@ const steps = [
function ProviderDetail() {
const { step } = useSearch({ from: Route.id })
const [activeModels, setActiveModels] = useState<ActiveModel[]>([])
const [loadingModels, setLoadingModels] = useState<string[]>([])
const { providerName } = useParams({ from: Route.id })
const { getProviderByName, setProviders, updateProvider } = useModelProvider()
const provider = getProviderByName(providerName)
const isSetup = step === 'setup_remote_provider'
const navigate = useNavigate()
useEffect(() => {
// Initial data fetch
getActiveModels().then(setActiveModels)
// Set up interval for real-time updates
const intervalId = setInterval(() => {
getActiveModels().then(setActiveModels)
}, 5000)
return () => clearInterval(intervalId)
}, [setActiveModels])
const handleJoyrideCallback = (data: CallBackProps) => {
const { status } = data
@ -83,6 +104,38 @@ function ProviderDetail() {
}
}
const handleStartModel = (modelId: string) => {
// Add model to loading state
setLoadingModels((prev) => [...prev, modelId])
if (provider)
startModel(provider, modelId)
.then(() => {
setActiveModels((prevModels) => [
...prevModels,
{ id: modelId } as ActiveModel,
])
})
.catch((error) => {
console.error('Error starting model:', error)
})
.finally(() => {
// Remove model from loading state
setLoadingModels((prev) => prev.filter((id) => id !== modelId))
})
}
const handleStopModel = (modelId: string) => {
stopModel(modelId)
.then(() => {
setActiveModels((prevModels) =>
prevModels.filter((model) => model.id !== modelId)
)
})
.catch((error) => {
console.error('Error stopping model:', error)
})
}
return (
<>
<Joyride
@ -301,6 +354,38 @@ function ProviderDetail() {
}
actions={
<div className="flex items-center gap-1">
{provider && provider.provider === 'llama.cpp' && (
<div className="mr-1">
{activeModels.some(
(activeModel) => activeModel.id === model.id
) ? (
<Button
size="sm"
variant="destructive"
onClick={() => handleStopModel(model.id)}
>
Stop
</Button>
) : (
<Button
size="sm"
disabled={loadingModels.includes(model.id)}
onClick={() => handleStartModel(model.id)}
>
{loadingModels.includes(model.id) ? (
<div className="flex items-center gap-2">
<IconLoader
size={16}
className="animate-spin"
/>
</div>
) : (
'Start'
)}
</Button>
)}
</div>
)}
<DialogEditModel
provider={provider}
modelId={model.id}
@ -323,7 +408,7 @@ function ProviderDetail() {
<h6 className="font-medium text-base">No model found</h6>
</div>
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
Available models will be listed here. If you dont have
Available models will be listed here. If you don't have
any models yet, visit the&nbsp;
<Link to={route.hub}>Hub</Link>
&nbsp;to download.

View File

@ -175,6 +175,7 @@ function SystemMonitor() {
<span className="text-main-view-fg">
<Button
variant="destructive"
size="sm"
onClick={() => stopRunningModel(model.id)}
>
Stop

View File

@ -1,4 +1,5 @@
import { ExtensionManager } from '@/lib/extension'
import { normalizeProvider } from '@/lib/models'
import { EngineManager, ExtensionTypeEnum, ModelExtension } from '@janhq/core'
import { Model as CoreModel } from '@janhq/core'
@ -259,6 +260,38 @@ export const stopModel = async (model: string, provider?: string) => {
}
}
/**
* @fileoverview Helper function to start a model.
* This function loads the model from the provider.
* Provider's chat function will handle loading the model.
* @param provider
* @param model
* @returns
*/
export const startModel = async (
provider: ProviderObject,
model: string,
abortController?: AbortController
): Promise<void> => {
const providerObj = EngineManager.instance().get(
normalizeProvider(provider.provider)
)
const modelObj = provider.models.find((m) => m.id === model)
if (providerObj && modelObj)
return providerObj?.loadModel(
{
id: modelObj.id,
settings: Object.fromEntries(
Object.entries(modelObj.settings ?? {}).map(([key, value]) => [
key,
value.controller_props?.value, // assuming each setting is { value: ... }
])
),
},
abortController
)
}
/**
* Configures the proxy options for model downloads.
* @param param0