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> = ({ export const AvatarEmoji: React.FC<AvatarEmojiProps> = ({
avatar, avatar,
fallback = '👋',
imageClassName = 'w-5 h-5 object-contain', imageClassName = 'w-5 h-5 object-contain',
textClassName = 'text-base', textClassName = 'text-base',
}) => { }) => {
@ -27,5 +26,5 @@ export const AvatarEmoji: React.FC<AvatarEmojiProps> = ({
return <img src={avatar} alt="Custom avatar" className={imageClassName} /> 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 { teamEmoji } from '@/utils/teamEmoji'
import { AvatarEmoji } from '@/containers/AvatarEmoji' import { AvatarEmoji } from '@/containers/AvatarEmoji'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils' import { cn, isDev } from '@/lib/utils'
interface AddEditAssistantProps { interface AddEditAssistantProps {
open: boolean open: boolean
@ -235,12 +235,10 @@ export default function AddEditAssistant({
> >
<AvatarEmoji <AvatarEmoji
avatar={avatar} avatar={avatar}
fallback={
<IconMoodSmile size={18} className="text-main-view-fg/50" />
}
imageClassName="w-5 h-5 object-contain" imageClassName="w-5 h-5 object-contain"
textClassName="" textClassName=""
/> />
<IconMoodSmile size={18} className="text-main-view-fg/50" />
</div> </div>
<div className="relative" ref={emojiPickerRef}> <div className="relative" ref={emojiPickerRef}>
<EmojiPicker <EmojiPicker
@ -248,7 +246,7 @@ export default function AddEditAssistant({
theme={isDark ? ('dark' as Theme) : ('light' as Theme)} theme={isDark ? ('dark' as Theme) : ('light' as Theme)}
className="!absolute !z-40 !overflow-y-auto top-2" className="!absolute !z-40 !overflow-y-auto top-2"
height={350} height={350}
customEmojis={teamEmoji} customEmojis={isDev() ? teamEmoji : []}
lazyLoadEmojis lazyLoadEmojis
previewConfig={{ showPreview: false }} previewConfig={{ showPreview: false }}
onEmojiClick={(emojiData: EmojiClickData) => { 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 defaultLightAppPrimaryBgColor: RgbaColor = { r: 219, g: 88, b: 44, a: 1 }
const defaultAppAccentBgColor: RgbaColor = { r: 45, g: 120, b: 220, 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 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 = { const defaultLightAppDestructiveBgColor: RgbaColor = {
r: 220, r: 217,
g: 45, g: 95,
b: 45, b: 95,
a: 1, a: 1,
} }
const defaultDarkLeftPanelTextColor: string = '#FFF' const defaultDarkLeftPanelTextColor: string = '#FFF'

View File

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

View File

@ -163,39 +163,6 @@ export const isCompletionResponse = (
return 'choices' in response 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. * @fileoverview Helper function to stop a model.
* This function unloads the model from the provider. * 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 { cn, getProviderTitle } from '@/lib/utils'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { importModel } from '@/services/models' import {
getActiveModels,
importModel,
startModel,
stopModel,
} from '@/services/models'
import { import {
createFileRoute, createFileRoute,
Link, Link,
@ -27,9 +32,11 @@ import { route } from '@/constants/routes'
import DeleteProvider from '@/containers/dialogs/DeleteProvider' import DeleteProvider from '@/containers/dialogs/DeleteProvider'
import { updateSettings } from '@/services/providers' import { updateSettings } from '@/services/providers'
import { Button } from '@/components/ui/button' 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 { getProviders } from '@/services/providers'
import { toast } from 'sonner' import { toast } from 'sonner'
import { ActiveModel } from '@/types/models'
import { useEffect, useState } from 'react'
// as route.threadsDetail // as route.threadsDetail
export const Route = createFileRoute('/settings/providers/$providerName')({ export const Route = createFileRoute('/settings/providers/$providerName')({
@ -67,12 +74,26 @@ const steps = [
function ProviderDetail() { function ProviderDetail() {
const { step } = useSearch({ from: Route.id }) const { step } = useSearch({ from: Route.id })
const [activeModels, setActiveModels] = useState<ActiveModel[]>([])
const [loadingModels, setLoadingModels] = useState<string[]>([])
const { providerName } = useParams({ from: Route.id }) const { providerName } = useParams({ from: Route.id })
const { getProviderByName, setProviders, updateProvider } = useModelProvider() const { getProviderByName, setProviders, updateProvider } = useModelProvider()
const provider = getProviderByName(providerName) const provider = getProviderByName(providerName)
const isSetup = step === 'setup_remote_provider' const isSetup = step === 'setup_remote_provider'
const navigate = useNavigate() 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 handleJoyrideCallback = (data: CallBackProps) => {
const { status } = data 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 ( return (
<> <>
<Joyride <Joyride
@ -301,6 +354,38 @@ function ProviderDetail() {
} }
actions={ actions={
<div className="flex items-center gap-1"> <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 <DialogEditModel
provider={provider} provider={provider}
modelId={model.id} modelId={model.id}
@ -323,7 +408,7 @@ function ProviderDetail() {
<h6 className="font-medium text-base">No model found</h6> <h6 className="font-medium text-base">No model found</h6>
</div> </div>
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed"> <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; any models yet, visit the&nbsp;
<Link to={route.hub}>Hub</Link> <Link to={route.hub}>Hub</Link>
&nbsp;to download. &nbsp;to download.

View File

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

View File

@ -1,4 +1,5 @@
import { ExtensionManager } from '@/lib/extension' import { ExtensionManager } from '@/lib/extension'
import { normalizeProvider } from '@/lib/models'
import { EngineManager, ExtensionTypeEnum, ModelExtension } from '@janhq/core' import { EngineManager, ExtensionTypeEnum, ModelExtension } from '@janhq/core'
import { Model as CoreModel } 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. * Configures the proxy options for model downloads.
* @param param0 * @param param0