Add tooltip on every action start and stop button while server is running

This commit is contained in:
Faisal Amir 2024-01-16 16:23:07 +07:00
parent 8b9d8e8301
commit 056cc8e722
9 changed files with 215 additions and 72 deletions

View File

@ -8,7 +8,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@janhq/uikit' } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { FaGithub, FaDiscord } from 'react-icons/fa' import { FaGithub, FaDiscord } from 'react-icons/fa'
@ -32,6 +32,8 @@ import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import useGetSystemResources from '@/hooks/useGetSystemResources' import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useMainViewState } from '@/hooks/useMainViewState' import { useMainViewState } from '@/hooks/useMainViewState'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
const menuLinks = [ const menuLinks = [
{ {
name: 'Discord', name: 'Discord',
@ -53,6 +55,7 @@ const BottomBar = () => {
const { setMainViewState } = useMainViewState() const { setMainViewState } = useMainViewState()
const { downloadStates } = useDownloadState() const { downloadStates } = useDownloadState()
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom) const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
const [serverEnabled] = useAtom(serverEnabledAtom)
return ( return (
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3"> <div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
@ -63,6 +66,7 @@ const BottomBar = () => {
) : null} ) : null}
</div> </div>
{!serverEnabled && (
<Badge <Badge
themes="secondary" themes="secondary"
className="cursor-pointer rounded-md border-none font-medium" className="cursor-pointer rounded-md border-none font-medium"
@ -71,6 +75,7 @@ const BottomBar = () => {
My Models My Models
<ShortCut menu="E" /> <ShortCut menu="E" />
</Badge> </Badge>
)}
{stateModel.state === 'start' && stateModel.loading && ( {stateModel.state === 'start' && stateModel.loading && (
<SystemItem <SystemItem

View File

@ -22,10 +22,13 @@ import { useActiveModel } from '@/hooks/useActiveModel'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState' import { useMainViewState } from '@/hooks/useMainViewState'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
export default function CommandListDownloadedModel() { export default function CommandListDownloadedModel() {
const { setMainViewState } = useMainViewState() const { setMainViewState } = useMainViewState()
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const { activeModel, startModel, stopModel } = useActiveModel() const { activeModel, startModel, stopModel } = useActiveModel()
const [serverEnabled] = useAtom(serverEnabledAtom)
const [showSelectModelModal, setShowSelectModelModal] = useAtom( const [showSelectModelModal, setShowSelectModelModal] = useAtom(
showSelectModelModalAtom showSelectModelModalAtom
) )
@ -39,7 +42,7 @@ export default function CommandListDownloadedModel() {
} }
const isNotDownloadedModel = downloadedModels.length === 0 const isNotDownloadedModel = downloadedModels.length === 0
if (isNotDownloadedModel) return null if (isNotDownloadedModel || serverEnabled) return null
return ( return (
<Fragment> <Fragment>

View File

@ -6,6 +6,7 @@ import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from './useGetDownloadedModels' import { useGetDownloadedModels } from './useGetDownloadedModels'
import { LAST_USED_MODEL_ID } from './useRecommendedModel' import { LAST_USED_MODEL_ID } from './useRecommendedModel'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
export const activeModelAtom = atom<Model | undefined>(undefined) export const activeModelAtom = atom<Model | undefined>(undefined)

View File

@ -17,7 +17,6 @@
"@hookform/resolvers": "^3.3.2", "@hookform/resolvers": "^3.3.2",
"@janhq/core": "link:./core", "@janhq/core": "link:./core",
"@janhq/uikit": "link:./uikit", "@janhq/uikit": "link:./uikit",
"@janhq/server": "link:./server",
"autoprefixer": "10.4.16", "autoprefixer": "10.4.16",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",

View File

@ -2,7 +2,15 @@
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { Model } from '@janhq/core' import { Model } from '@janhq/core'
import { Badge, Button } from '@janhq/uikit' import {
Badge,
Button,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
@ -23,6 +31,8 @@ import { useMainViewState } from '@/hooks/useMainViewState'
import { toGibibytes } from '@/utils/converter' import { toGibibytes } from '@/utils/converter'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
type Props = { type Props = {
@ -37,6 +47,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
const { modelDownloadStateAtom, downloadStates } = useDownloadState() const { modelDownloadStateAtom, downloadStates } = useDownloadState()
const { requestCreateNewThread } = useCreateNewThread() const { requestCreateNewThread } = useCreateNewThread()
const totalRam = useAtomValue(totalRamAtom) const totalRam = useAtomValue(totalRamAtom)
const serverEnabled = useAtomValue(serverEnabledAtom)
const downloadAtom = useMemo( const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id]), () => atom((get) => get(modelDownloadStateAtom)[model.id]),
@ -68,13 +79,26 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
if (isDownloaded) { if (isDownloaded) {
downloadButton = ( downloadButton = (
<Tooltip>
<TooltipTrigger>
<Button <Button
themes="secondaryBlue" themes="secondaryBlue"
className="min-w-[98px]" className="min-w-[98px]"
onClick={onUseModelClick} onClick={onUseModelClick}
disabled={serverEnabled}
> >
Use Use
</Button> </Button>
</TooltipTrigger>
{serverEnabled && (
<TooltipPortal>
<TooltipContent side="top">
<span>Threads are disabled while the server is running</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
)}
</Tooltip>
) )
} }

View File

@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { useEffect, useState } from 'react'
import React from 'react'
import { useServerLog } from '@/hooks/useServerLog'
const Logs = () => {
const { getServerLog } = useServerLog()
const [logs, setLogs] = useState([])
useEffect(() => {
getServerLog().then((log) => {
setLogs(log.split(/\r?\n|\r|\n/g))
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [logs])
return (
<div className="p-4">
<code className="text-xs">
{logs.map((log, i) => {
return (
<p key={i} className="my-2 leading-relaxed">
{log}
</p>
)
})}
</code>
</div>
)
}
export default Logs

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */ 'use client'
import { useEffect, useState } from 'react'
import React from 'react'
import { import {
Button, Button,
@ -25,8 +26,11 @@ import { ExternalLinkIcon, InfoIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import CardSidebar from '@/containers/CardSidebar' import CardSidebar from '@/containers/CardSidebar'
import DropdownListSidebar from '@/containers/DropdownListSidebar' import DropdownListSidebar, {
selectedModelAtom,
} from '@/containers/DropdownListSidebar'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useServerLog } from '@/hooks/useServerLog' import { useServerLog } from '@/hooks/useServerLog'
import { getConfigurationsData } from '@/utils/componentSettings' import { getConfigurationsData } from '@/utils/componentSettings'
@ -37,6 +41,8 @@ import ModelSetting from '../Chat/ModelSetting'
import settingComponentBuilder from '../Chat/ModelSetting/settingComponentBuilder' import settingComponentBuilder from '../Chat/ModelSetting/settingComponentBuilder'
import { showRightSideBarAtom } from '../Chat/Sidebar' import { showRightSideBarAtom } from '../Chat/Sidebar'
import Logs from './Logs'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom' import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom'
@ -49,14 +55,10 @@ const LocalServerScreen = () => {
const modelRuntimeParams = toRuntimeParams(activeModelParams) const modelRuntimeParams = toRuntimeParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData(modelEngineParams) const componentDataEngineSetting = getConfigurationsData(modelEngineParams)
const componentDataRuntimeSetting = getConfigurationsData(modelRuntimeParams) const componentDataRuntimeSetting = getConfigurationsData(modelRuntimeParams)
const { getServerLog, openServerLog, clearServerLog } = useServerLog() const { openServerLog, clearServerLog } = useServerLog()
const [logs, setLogs] = useState([]) const { activeModel, startModel } = useActiveModel()
useEffect(() => { const [selectedModel] = useAtom(selectedModelAtom)
getServerLog().then((log) => {
setLogs(log.split(/\r?\n|\r|\n/g))
})
}, [getServerLog, logs])
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
@ -78,6 +80,9 @@ const LocalServerScreen = () => {
window.core?.api?.stopServer() window.core?.api?.stopServer()
setServerEnabled(false) setServerEnabled(false)
} else { } else {
if (!activeModel) {
startModel(String(selectedModel?.id))
}
window.core?.api?.startServer() window.core?.api?.startServer()
setServerEnabled(true) setServerEnabled(true)
} }
@ -105,7 +110,11 @@ const LocalServerScreen = () => {
</SelectContent> </SelectContent>
</Select> </Select>
<Input className="w-[60px] flex-shrink-0" value="1337" /> <Input
className="w-[60px] flex-shrink-0"
value="1337"
disabled={serverEnabled}
/>
</div> </div>
<div> <div>
<label <label
@ -195,17 +204,7 @@ const LocalServerScreen = () => {
</Button> </Button>
</div> </div>
</div> </div>
<div className="p-4"> <Logs />
<code className="text-xs">
{logs.map((log, i) => {
return (
<p key={i} className="my-2 leading-relaxed">
{log}
</p>
)
})}
</code>
</div>
</div> </div>
{/* Right bar */} {/* Right bar */}

View File

@ -1,8 +1,16 @@
import { useState } from 'react' import { useState } from 'react'
import { InferenceEngine, Model } from '@janhq/core' import { InferenceEngine, Model } from '@janhq/core'
import { Badge } from '@janhq/uikit' import {
Badge,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { useAtom } from 'jotai'
import { import {
MoreVerticalIcon, MoreVerticalIcon,
Trash2Icon, Trash2Icon,
@ -10,6 +18,8 @@ import {
StopCircleIcon, StopCircleIcon,
} from 'lucide-react' } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { useActiveModel } from '@/hooks/useActiveModel' import { useActiveModel } from '@/hooks/useActiveModel'
import { useClickOutside } from '@/hooks/useClickOutside' import { useClickOutside } from '@/hooks/useClickOutside'
@ -17,6 +27,8 @@ import useDeleteModel from '@/hooks/useDeleteModel'
import { toGibibytes } from '@/utils/converter' import { toGibibytes } from '@/utils/converter'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
type RowModelProps = { type RowModelProps = {
data: Model data: Model
} }
@ -33,6 +45,8 @@ export default function RowModel(props: RowModelProps) {
const isActiveModel = stateModel.model === props.data.id const isActiveModel = stateModel.model === props.data.id
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const isRemoteModel = const isRemoteModel =
props.data.engine === InferenceEngine.openai || props.data.engine === InferenceEngine.openai ||
props.data.engine === InferenceEngine.triton_trtllm props.data.engine === InferenceEngine.triton_trtllm
@ -40,10 +54,14 @@ export default function RowModel(props: RowModelProps) {
const onModelActionClick = (modelId: string) => { const onModelActionClick = (modelId: string) => {
if (activeModel && activeModel.id === modelId) { if (activeModel && activeModel.id === modelId) {
stopModel() stopModel()
window.core?.api?.stopServer()
setServerEnabled(false)
} else { } else {
if (serverEnabled) {
startModel(modelId) startModel(modelId)
} }
} }
}
return ( return (
<tr className="relative border-b border-border last:border-none"> <tr className="relative border-b border-border last:border-none">
@ -113,15 +131,26 @@ export default function RowModel(props: RowModelProps) {
className="absolute right-4 top-10 z-20 w-52 overflow-hidden rounded-lg border border-border bg-background py-2 shadow-lg" className="absolute right-4 top-10 z-20 w-52 overflow-hidden rounded-lg border border-border bg-background py-2 shadow-lg"
ref={setMenu} ref={setMenu}
> >
<Tooltip>
<TooltipTrigger className="w-full">
<div <div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary" className={twMerge(
'flex items-center space-x-2 px-4 py-2 hover:bg-secondary',
serverEnabled &&
activeModel &&
activeModel.id !== props.data.id &&
'pointer-events-none cursor-not-allowed opacity-40'
)}
onClick={() => { onClick={() => {
onModelActionClick(props.data.id) onModelActionClick(props.data.id)
setMore(false) setMore(false)
}} }}
> >
{activeModel && activeModel.id === props.data.id ? ( {activeModel && activeModel.id === props.data.id ? (
<StopCircleIcon size={16} className="text-muted-foreground" /> <StopCircleIcon
size={16}
className="text-muted-foreground"
/>
) : ( ) : (
<PlayIcon size={16} className="text-muted-foreground" /> <PlayIcon size={16} className="text-muted-foreground" />
)} )}
@ -130,6 +159,21 @@ export default function RowModel(props: RowModelProps) {
&nbsp;Model &nbsp;Model
</span> </span>
</div> </div>
</TooltipTrigger>
{serverEnabled && (
<TooltipPortal>
<TooltipContent side="top">
<span>
{activeModel && activeModel.id === props.data.id
? 'The API server is running, change model will stop the server'
: 'Threads are disabled while the server is running'}
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
)}
</Tooltip>
<div <div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary" className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
onClick={() => { onClick={() => {

View File

@ -1,11 +1,22 @@
import { ScrollArea, Progress, Badge, Button } from '@janhq/uikit' import {
ScrollArea,
Progress,
Badge,
Button,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { useActiveModel } from '@/hooks/useActiveModel' import { useActiveModel } from '@/hooks/useActiveModel'
import { toGibibytes } from '@/utils/converter' import { toGibibytes } from '@/utils/converter'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { import {
cpuUsageAtom, cpuUsageAtom,
totalRamAtom, totalRamAtom,
@ -19,6 +30,7 @@ export default function SystemMonitorScreen() {
const usedRam = useAtomValue(usedRamAtom) const usedRam = useAtomValue(usedRamAtom)
const cpuUsage = useAtomValue(cpuUsageAtom) const cpuUsage = useAtomValue(cpuUsageAtom)
const { activeModel, stateModel, stopModel } = useActiveModel() const { activeModel, stateModel, stopModel } = useActiveModel()
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
return ( return (
<div className="flex h-full w-full bg-background dark:bg-background"> <div className="flex h-full w-full bg-background dark:bg-background">
@ -94,17 +106,38 @@ export default function SystemMonitorScreen() {
<Badge themes="secondary">v{activeModel.version}</Badge> <Badge themes="secondary">v{activeModel.version}</Badge>
</td> </td>
<td className="px-6 py-2 text-center"> <td className="px-6 py-2 text-center">
<Tooltip>
<TooltipTrigger className="w-full">
<Button <Button
block block
themes={ themes={
stateModel.state === 'stop' ? 'danger' : 'primary' stateModel.state === 'stop'
? 'danger'
: 'primary'
} }
className="w-16" className="w-16"
loading={stateModel.loading} loading={stateModel.loading}
onClick={() => stopModel()} onClick={() => {
stopModel()
window.core?.api?.stopServer()
setServerEnabled(false)
}}
> >
Stop Stop
</Button> </Button>
</TooltipTrigger>
{serverEnabled && (
<TooltipPortal>
<TooltipContent side="top">
<span>
The API server is running, stop the model will
also stop the server
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
)}
</Tooltip>
</td> </td>
</tr> </tr>
</tbody> </tbody>