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:
parent
72d1192499
commit
1b3f16b3e1
@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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 don’t have
|
Available models will be listed here. If you don't have
|
||||||
any models yet, visit the
|
any models yet, visit the
|
||||||
<Link to={route.hub}>Hub</Link>
|
<Link to={route.hub}>Hub</Link>
|
||||||
to download.
|
to download.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user