Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: server manager #42

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions apps/desktop/src/features/inference-server/instance-creator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cn } from "@localai/theme/utils"
import { SpinnerButton } from "@localai/ui/button"
import { useState } from "react"

export const InstanceCreator = () => {
const [isLoading, setIsLoading] = useState(false)

const handleClick = async () => {
setIsLoading(true)
// Add your logic here to handle adding a new instance
// For example:
await createNewInstance()
setIsLoading(false)
}

return (
<div className="flex items-center justify-end gap-2">
<SpinnerButton
isSpinning={isLoading}
className={cn(
"w-48 justify-center border",
"bg-gray-5 text-gray-11 ring-gray-9 ring-2 hover:text-gray-12"
)}
onClick={handleClick}>
{isLoading ? "..." : "Create New Instance"}
</SpinnerButton>
</div>
)
}
function createNewInstance() {
throw new Error("Function not implemented.")
}
77 changes: 77 additions & 0 deletions apps/desktop/src/features/inference-server/server-list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { cn } from "@localai/theme/utils"
import { Button } from "@localai/ui/button"
import { Input } from "@localai/ui/input"
import { Textarea } from "@localai/ui/textarea"
import {
ChevronLeftIcon,
ChevronRightIcon,
GearIcon
} from "@radix-ui/react-icons"
import dedent from "ts-dedent"

import { useToggle } from "~features/layout/use-toggle"
import { type ModelMetadata } from "~features/model-downloader/model-file"
import { useGlobal } from "~providers/global"
import { ModelProvider } from "~providers/model"

import { ServerConfig } from "./server-config"

export const ServerListItem = ({ model }: { model: ModelMetadata }) => {
const {
activeModelState: [activeModel]
} = useGlobal()
const [isConfigVisible, toggleConfig] = useToggle(false)
return (
<ModelProvider model={model}>
<div
className={cn(
"flex flex-col gap-4 rounded-md p-2 pl-3",
"text-gray-11 hover:text-gray-12",
"transition-colors group",
activeModel?.path === model.path
? "border border-green-7 hover:border-green-8"
: "border border-gray-7 hover:border-gray-8"
)}>
<div className="flex justify-between w-full">
<Button className="gap-0" onClick={() => toggleConfig()}>
{!isConfigVisible && <ChevronLeftIcon />}

<GearIcon
className={cn(
"will-change-transform",
"transition-transform",
isConfigVisible ? "rotate-180" : "-rotate-180"
)}
/>
{isConfigVisible && <ChevronRightIcon />}
</Button>
</div>
<ServerConfig />
{isConfigVisible && (
<>
<div
className={cn(
"transition-all",
isConfigVisible ? "w-1/4 opacity-100" : "w-0 opacity-0",
"border-l border-l-gray-6"
)}>
<div className="p-4 flex flex-col gap-6 overflow-auto items-start w-full">
<Textarea
rows={8}
title="Prompt template (WIP)"
defaultValue={dedent`
<BOT>: {SYSTEM}
<HUMAN>: {PROMPT}
<BOT>:
`}
/>
<Input placeholder="Temperature (WIP)" defaultValue={0.47} />
<Input placeholder="Max Tokens (WIP)" defaultValue={0.47} />
</div>
</div>
</>
)}
</div>
</ModelProvider>
)
}
4 changes: 4 additions & 0 deletions apps/desktop/src/features/layout/index.tsx
Original file line number Diff line number Diff line change
@@ -72,6 +72,10 @@ const BottomBar = () => {
on={<MoonIcon />}
off={<SunIcon />}
/>
<NavButton route={Route.ServerManager}>
<Home /> Server Manager
</NavButton>

</div>
)
}
3 changes: 3 additions & 0 deletions apps/desktop/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { useMemo } from "react"

import { Route, useGlobal } from "~providers/global"
import { ModelManagerView } from "~views/model-manager"
import { ServerManagerView } from "~views/server-manager"
import { ThreadView } from "~views/thread"

// Since NextJS router doesn't work with SPA yet, use manual router for now.
@@ -18,6 +19,8 @@ function IndexPage() {
case Route.ModelManager:
default:
return <ModelManagerView />
case Route.ServerManager:
return <ServerManagerView />
}
}, [currentRoute])
}
11 changes: 10 additions & 1 deletion apps/desktop/src/providers/global.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,8 @@ import { getCachedIntegrity } from "~providers/model"

export enum Route {
ModelManager = "model-manager",
Thread = "thread"
Thread = "thread",
ServerManager = "ServerManager"
}

let _prefix: string
@@ -146,6 +147,14 @@ const useGlobalProvider = () => {
}
}, [activeThread, activeRoute])

useEffect(() => {
if (routeState[0] === Route.ServerManager) {
setTitle("Server Manager")
} else if (activeThreadState[0]) {
setTitle(activeThreadState[0].name.slice(0, -2))
}
}, [activeThreadState[0], routeState[0]])

return {
getWindow: () => windowRef.current,
loadModel,
20 changes: 20 additions & 0 deletions apps/desktop/src/providers/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client"

import { createProvider } from "puro"
import { useContext } from "react"

import type { ModelMetadata } from "~features/model-downloader/model-file"

/**
* Requires a global provider
*/
const useServerProvider = ({ model }: { model: ModelMetadata }) => {
return {
model
}
}

const { BaseContext, Provider } = createProvider(useServerProvider)

export const useServer = () => useContext(BaseContext)
export const ServerProvider = Provider
103 changes: 103 additions & 0 deletions apps/desktop/src/views/server-manager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Button, SpinnerButton } from "@localai/ui/button"
import { Input } from "@localai/ui/input"
import {
DotsHorizontalIcon,
OpenInNewWindowIcon,
ReloadIcon
} from "@radix-ui/react-icons"
import { open as dialogOpen } from "@tauri-apps/api/dialog"

import { InstanceCreator } from "~features/inference-server/instance-creator"
import { ServerListItem } from "~features/inference-server/server-list-item"
import { InvokeCommand, invoke } from "~features/invoke"
import { ViewBody, ViewContainer, ViewHeader } from "~features/layout/view"
import { ChatSideBarToggle } from "~features/thread/side-bar"
import { useGlobal } from "~providers/global"

export function ServerManagerView() {
const {
activeModelState: [activeModel],
modelsDirectoryState: {
isRefreshing,
modelsDirectory,
models,
updateModelsDirectory
}
} = useGlobal()

return (
<ViewContainer className="relative z-50">
<ViewHeader>
<ChatSideBarToggle />
<div className="flex w-full">
{!!modelsDirectory && (
<SpinnerButton
className="w-10 p-3 rounded-r-none"
Icon={ReloadIcon}
isSpinning={isRefreshing}
title="Refresh Server Instance Directory"
onClick={async () => {
await updateModelsDirectory()
}}
/>
)}
<Input
className="w-full lg:w-96 rounded-none border-gray-3"
value={modelsDirectory}
readOnly
placeholder="Server directory"
/>

<Button
title="Change server directory"
className="w-10 p-3 rounded-none"
onClick={async () => {
const selected = (await dialogOpen({
directory: true,
multiple: false
})) as string

if (!selected) {
return
}
await updateModelsDirectory(selected)
}}>
<DotsHorizontalIcon />
</Button>

<Button
title="Open server directory"
className="w-10 p-3 rounded-l-none"
onClick={() => {
invoke(InvokeCommand.OpenDirectory, {
path: modelsDirectory
})
}}>
<OpenInNewWindowIcon />
</Button>
</div>
<InstanceCreator />
</ViewHeader>
<ViewBody className="flex flex-col p-4 gap-2">
{models.length === 0 && (
<p className="text-gray-9 italic pointer-events-none text-center">
{`Here you can create and save custom instances of your models to use as servers. To begin, just click the "+" button.`}
</p>
)}
<div className="flex flex-col p-2 gap-6">
{models
.sort((a, b) =>
activeModel?.path === a.path
? -1
: activeModel?.path === b.path
? 1
: 0
)
.map((model) => (
<ServerListItem key={model.name} model={model} />
))}
</div>
</ViewBody>
</ViewContainer>
)
}
1 change: 1 addition & 0 deletions models/_shared.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ export enum LicenseType {
}

export type ModelInfo = {
path: any
name?: string
size: number
downloadUrl: string