Merge pull request #438 from janhq/chore/loading-indicator-when-active-model
chore: added loader starting and stopping model
This commit is contained in:
commit
e05c08b95f
@ -1,6 +1,9 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Button } from '@uikit'
|
import { Button } from '@uikit'
|
||||||
import ModelActionMenu from '../ModelActionMenu'
|
import ModelActionMenu from '../ModelActionMenu'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { stateModel } from '@helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export enum ModelActionType {
|
export enum ModelActionType {
|
||||||
Start = 'Start',
|
Start = 'Start',
|
||||||
@ -32,10 +35,14 @@ const ModelActionButton: React.FC<Props> = ({
|
|||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = modelActionMapper[type]
|
const styles = modelActionMapper[type]
|
||||||
|
// const { startingModel } = useStartStopModel()
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
onActionClick(type)
|
onActionClick(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = useAtomValue(stateModel)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className="whitespace-nowrap px-3 py-2 text-right">
|
<td className="whitespace-nowrap px-3 py-2 text-right">
|
||||||
<div className="flex items-center justify-end gap-x-4">
|
<div className="flex items-center justify-end gap-x-4">
|
||||||
@ -43,7 +50,8 @@ const ModelActionButton: React.FC<Props> = ({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
themes={styles.title === 'Start' ? 'accent' : 'default'}
|
themes={styles.title === 'Start' ? 'accent' : 'default'}
|
||||||
onClick={onClick}
|
onClick={() => onClick()}
|
||||||
|
loading={state.loading}
|
||||||
>
|
>
|
||||||
{styles.title} Model
|
{styles.title} Model
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -40,20 +40,20 @@ const ModelRow: React.FC<Props> = ({ model }) => {
|
|||||||
}, [model])
|
}, [model])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="bg-background/50 border-border border-b last:rounded-lg last:border-b-0">
|
<tr className="border-b border-border bg-background/50 last:rounded-lg last:border-b-0">
|
||||||
<td className="text-muted-foreground whitespace-nowrap px-3 font-semibold">
|
<td className="whitespace-nowrap px-3 font-semibold text-muted-foreground">
|
||||||
{model.name}
|
{model.name}
|
||||||
<span className="ml-2 font-semibold">v{model.version}</span>
|
<span className="ml-2 font-semibold">v{model.version}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-muted-foreground whitespace-nowrap px-3">
|
<td className="whitespace-nowrap px-3 text-muted-foreground">
|
||||||
<div className="flex flex-col justify-start">
|
<div className="flex flex-col justify-start">
|
||||||
<span>GGUF</span>
|
<span>GGUF</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-muted-foreground whitespace-nowrap px-3">
|
<td className="whitespace-nowrap px-3 text-muted-foreground">
|
||||||
{toGigabytes(model.size)}
|
{toGigabytes(model.size)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-muted-foreground whitespace-nowrap px-3">
|
<td className="whitespace-nowrap px-3 text-muted-foreground">
|
||||||
<ModelStatusComponent status={status} />
|
<ModelStatusComponent status={status} />
|
||||||
</td>
|
</td>
|
||||||
<ModelActionButton
|
<ModelActionButton
|
||||||
|
|||||||
@ -21,12 +21,12 @@ export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = {
|
|||||||
[ModelStatus.Active]: {
|
[ModelStatus.Active]: {
|
||||||
title: 'Active',
|
title: 'Active',
|
||||||
textColor: 'text-green-800',
|
textColor: 'text-green-800',
|
||||||
backgroundColor: 'bg-green-100 dark:bg-green-300',
|
backgroundColor: 'bg-green-100 dark:bg-green-300 text-green-700',
|
||||||
},
|
},
|
||||||
[ModelStatus.RunningInNitro]: {
|
[ModelStatus.RunningInNitro]: {
|
||||||
title: 'Running in Nitro',
|
title: 'Running in Nitro',
|
||||||
textColor: 'text-green-800',
|
textColor: 'text-green-800',
|
||||||
backgroundColor: 'bg-green-100 dark:bg-green-300',
|
backgroundColor: 'bg-green-100 dark:bg-green-300 text-green-700',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,11 @@ import useGetSystemResources from '@hooks/useGetSystemResources'
|
|||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
||||||
import { formatDownloadPercentage } from '@utils/converter'
|
import { formatDownloadPercentage } from '@utils/converter'
|
||||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||||
|
|
||||||
const BottomBar = () => {
|
const BottomBar = () => {
|
||||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||||
|
const stateModelStartStop = useAtomValue(stateModel)
|
||||||
const { ram, cpu } = useGetSystemResources()
|
const { ram, cpu } = useGetSystemResources()
|
||||||
const modelDownloadStates = useAtomValue(modelDownloadStateAtom)
|
const modelDownloadStates = useAtomValue(modelDownloadStateAtom)
|
||||||
|
|
||||||
@ -16,10 +17,28 @@ const BottomBar = () => {
|
|||||||
downloadStates.push(value)
|
downloadStates.push(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(stateModelStartStop)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 z-20 flex h-8 w-full items-center justify-between border-t border-border bg-background/50 px-4">
|
<div className="fixed bottom-0 left-0 z-20 flex h-8 w-full items-center justify-between border-t border-border bg-background/50 px-4">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
|
{stateModelStartStop.state === 'start' &&
|
||||||
|
stateModelStartStop.loading && (
|
||||||
|
<SystemItem
|
||||||
|
name="Starting:"
|
||||||
|
value={stateModelStartStop.model || '-'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{stateModelStartStop.state === 'stop' &&
|
||||||
|
stateModelStartStop.loading && (
|
||||||
|
<SystemItem
|
||||||
|
name="Stopping:"
|
||||||
|
value={stateModelStartStop.model || '-'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!stateModelStartStop.loading && (
|
||||||
<SystemItem name="Active model:" value={activeModel?.name || '-'} />
|
<SystemItem name="Active model:" value={activeModel?.name || '-'} />
|
||||||
|
)}
|
||||||
{downloadStates.length > 0 && (
|
{downloadStates.length > 0 && (
|
||||||
<SystemItem
|
<SystemItem
|
||||||
name="Downloading:"
|
name="Downloading:"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
|
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
||||||
export const selectedModelAtom = atom<AssistantModel | undefined>(undefined)
|
export const selectedModelAtom = atom<AssistantModel | undefined>(undefined)
|
||||||
|
|
||||||
export const activeAssistantModelAtom = atom<AssistantModel | undefined>(
|
export const activeAssistantModelAtom = atom<AssistantModel | undefined>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,27 +2,35 @@ import { executeSerial } from '@services/pluginService'
|
|||||||
import { ModelManagementService, InferenceService } from '@janhq/core'
|
import { ModelManagementService, InferenceService } from '@janhq/core'
|
||||||
import useInitModel from './useInitModel'
|
import useInitModel from './useInitModel'
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function useStartStopModel() {
|
export default function useStartStopModel() {
|
||||||
const { initModel } = useInitModel()
|
const { initModel } = useInitModel()
|
||||||
const setActiveModel = useSetAtom(activeAssistantModelAtom)
|
const setActiveModel = useSetAtom(activeAssistantModelAtom)
|
||||||
|
const setStateModel = useSetAtom(stateModel)
|
||||||
|
|
||||||
const startModel = async (modelId: string) => {
|
const startModel = async (modelId: string) => {
|
||||||
|
setStateModel({ state: 'start', loading: true, model: modelId })
|
||||||
const model = await executeSerial(
|
const model = await executeSerial(
|
||||||
ModelManagementService.GetModelById,
|
ModelManagementService.GetModelById,
|
||||||
modelId
|
modelId
|
||||||
)
|
)
|
||||||
if (!model) {
|
if (!model) {
|
||||||
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
||||||
|
setStateModel((prev) => ({ ...prev, loading: false }))
|
||||||
} else {
|
} else {
|
||||||
await initModel(model)
|
await initModel(model)
|
||||||
|
setStateModel((prev) => ({ ...prev, loading: false }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopModel = async (modelId: string) => {
|
const stopModel = async (modelId: string) => {
|
||||||
|
setStateModel({ state: 'stop', loading: true, model: modelId })
|
||||||
|
setTimeout(async () => {
|
||||||
await executeSerial(InferenceService.StopModel, modelId)
|
await executeSerial(InferenceService.StopModel, modelId)
|
||||||
setActiveModel(undefined)
|
setActiveModel(undefined)
|
||||||
|
setStateModel({ state: 'stop', loading: false, model: modelId })
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { startModel, stopModel }
|
return { startModel, stopModel }
|
||||||
|
|||||||
@ -18,10 +18,14 @@ const buttonVariants = cva(
|
|||||||
sm: 'h-6 px-2 text-xs rounded-md',
|
sm: 'h-6 px-2 text-xs rounded-md',
|
||||||
default: 'h-8 px-3',
|
default: 'h-8 px-3',
|
||||||
},
|
},
|
||||||
|
loading: {
|
||||||
|
true: 'pointer-events-none opacity-70',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
themes: 'default',
|
themes: 'default',
|
||||||
size: 'default',
|
size: 'default',
|
||||||
|
loading: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -33,14 +37,49 @@ export interface ButtonProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, themes, size, asChild = false, ...props }, ref) => {
|
(
|
||||||
|
{ className, themes, size, loading, asChild = false, children, ...props },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const Comp = asChild ? Slot : 'button'
|
const Comp = asChild ? Slot : 'button'
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={twMerge(buttonVariants({ themes, size, className }))}
|
className={twMerge(
|
||||||
|
buttonVariants({ themes, size, loading, className })
|
||||||
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
role="status"
|
||||||
|
className="mr-2 h-4 animate-spin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Comp>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user