commit
97a497858d
31
.github/workflows/jan-electron-build-nightly.yml
vendored
31
.github/workflows/jan-electron-build-nightly.yml
vendored
@ -1,6 +1,12 @@
|
||||
name: Jan Build Electron App Nightly or Manual
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'docs/**'
|
||||
schedule:
|
||||
- cron: '0 20 * * 1,2,3' # At 8 PM UTC on Monday, Tuesday, and Wednesday which is 3 AM UTC+7 Tuesday, Wednesday, and Thursday
|
||||
workflow_dispatch:
|
||||
@ -23,12 +29,20 @@ jobs:
|
||||
- name: Set public provider
|
||||
id: set-public-provider
|
||||
run: |
|
||||
if [ ${{ github.event == 'workflow_dispatch' }} ]; then
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "::set-output name=public_provider::${{ github.event.inputs.public_provider }}"
|
||||
echo "::set-output name=ref::${{ github.ref }}"
|
||||
else
|
||||
echo "::set-output name=public_provider::cloudflare-r2"
|
||||
echo "::set-output name=ref::refs/heads/dev"
|
||||
if [ "${{ github.event_name }}" == "schedule" ]; then
|
||||
echo "::set-output name=public_provider::cloudflare-r2"
|
||||
echo "::set-output name=ref::refs/heads/dev"
|
||||
elif [ "${{ github.event_name }}" == "push" ]; then
|
||||
echo "::set-output name=public_provider::cloudflare-r2"
|
||||
echo "::set-output name=ref::${{ github.ref }}"
|
||||
else
|
||||
echo "::set-output name=public_provider::none"
|
||||
echo "::set-output name=ref::${{ github.ref }}"
|
||||
fi
|
||||
fi
|
||||
# Job create Update app version based on latest release tag with build number and save to output
|
||||
get-update-version:
|
||||
@ -73,6 +87,17 @@ jobs:
|
||||
push_to_branch: dev
|
||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||
|
||||
noti-discord-pre-release-and-update-url-readme:
|
||||
needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider]
|
||||
secrets: inherit
|
||||
if: github.event_name == 'push'
|
||||
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
|
||||
with:
|
||||
ref: refs/heads/dev
|
||||
build_reason: Pre-release
|
||||
push_to_branch: dev
|
||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||
|
||||
noti-discord-manual-and-update-url-readme:
|
||||
needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider]
|
||||
secrets: inherit
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
name: Jan Build Electron Pre Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "!README.md"
|
||||
|
||||
jobs:
|
||||
|
||||
# Job create Update app version based on latest release tag with build number and save to output
|
||||
get-update-version:
|
||||
uses: ./.github/workflows/template-get-update-version.yml
|
||||
|
||||
build-macos:
|
||||
uses: ./.github/workflows/template-build-macos.yml
|
||||
secrets: inherit
|
||||
needs: [get-update-version]
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
public_provider: cloudflare-r2
|
||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||
|
||||
build-windows-x64:
|
||||
uses: ./.github/workflows/template-build-windows-x64.yml
|
||||
secrets: inherit
|
||||
needs: [get-update-version]
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
public_provider: cloudflare-r2
|
||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||
|
||||
build-linux-x64:
|
||||
uses: ./.github/workflows/template-build-linux-x64.yml
|
||||
secrets: inherit
|
||||
needs: [get-update-version]
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
public_provider: cloudflare-r2
|
||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||
|
||||
noti-discord-nightly-and-update-url-readme:
|
||||
needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version]
|
||||
secrets: inherit
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
|
||||
with:
|
||||
ref: refs/heads/dev
|
||||
build_reason: Nightly
|
||||
push_to_branch: dev
|
||||
new_version: ${{ needs.get-update-version.outputs.new_version }}
|
||||
@ -2,38 +2,36 @@ import fs from 'fs'
|
||||
import util from 'util'
|
||||
import { getAppLogPath, getServerLogPath } from './utils'
|
||||
|
||||
export const log = function (message: string) {
|
||||
const appLogPath = getAppLogPath()
|
||||
export const log = (message: string) => {
|
||||
const path = getAppLogPath()
|
||||
if (!message.startsWith('[')) {
|
||||
message = `[APP]::${message}`
|
||||
}
|
||||
|
||||
message = `${new Date().toISOString()} ${message}`
|
||||
|
||||
if (fs.existsSync(appLogPath)) {
|
||||
var log_file = fs.createWriteStream(appLogPath, {
|
||||
flags: 'a',
|
||||
})
|
||||
log_file.write(util.format(message) + '\n')
|
||||
log_file.close()
|
||||
console.debug(message)
|
||||
}
|
||||
writeLog(message, path)
|
||||
}
|
||||
|
||||
export const logServer = function (message: string) {
|
||||
const serverLogPath = getServerLogPath()
|
||||
export const logServer = (message: string) => {
|
||||
const path = getServerLogPath()
|
||||
if (!message.startsWith('[')) {
|
||||
message = `[SERVER]::${message}`
|
||||
}
|
||||
|
||||
message = `${new Date().toISOString()} ${message}`
|
||||
writeLog(message, path)
|
||||
}
|
||||
|
||||
if (fs.existsSync(serverLogPath)) {
|
||||
var log_file = fs.createWriteStream(serverLogPath, {
|
||||
const writeLog = (message: string, logPath: string) => {
|
||||
if (!fs.existsSync(logPath)) {
|
||||
fs.writeFileSync(logPath, message)
|
||||
} else {
|
||||
const logFile = fs.createWriteStream(logPath, {
|
||||
flags: 'a',
|
||||
})
|
||||
log_file.write(util.format(message) + '\n')
|
||||
log_file.close()
|
||||
logFile.write(util.format(message) + '\n')
|
||||
logFile.close()
|
||||
console.debug(message)
|
||||
}
|
||||
}
|
||||
|
||||
6
core/src/types/config/appConfigEvent.ts
Normal file
6
core/src/types/config/appConfigEvent.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* App configuration event name
|
||||
*/
|
||||
export enum AppConfigurationEventName {
|
||||
OnConfigurationUpdate = 'OnConfigurationUpdate',
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from './appConfigEntity'
|
||||
export * from './appConfigEvent'
|
||||
|
||||
@ -20,6 +20,8 @@ import {
|
||||
MessageEvent,
|
||||
ModelEvent,
|
||||
InferenceEvent,
|
||||
AppConfigurationEventName,
|
||||
joinPath,
|
||||
} from "@janhq/core";
|
||||
import { requestInference } from "./helpers/sse";
|
||||
import { ulid } from "ulid";
|
||||
@ -71,6 +73,20 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
|
||||
events.on(InferenceEvent.OnInferenceStopped, () => {
|
||||
JanInferenceOpenAIExtension.handleInferenceStopped(this);
|
||||
});
|
||||
|
||||
const settingsFilePath = await joinPath([
|
||||
JanInferenceOpenAIExtension._engineDir,
|
||||
JanInferenceOpenAIExtension._engineMetadataFileName,
|
||||
]);
|
||||
|
||||
events.on(
|
||||
AppConfigurationEventName.OnConfigurationUpdate,
|
||||
(settingsKey: string) => {
|
||||
// Update settings on changes
|
||||
if (settingsKey === settingsFilePath)
|
||||
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,7 +198,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
|
||||
},
|
||||
error: async (err) => {
|
||||
if (instance.isCancelled || message.content.length > 0) {
|
||||
message.status = MessageStatus.Error;
|
||||
message.status = MessageStatus.Stopped;
|
||||
events.emit(MessageEvent.OnMessageUpdate, message);
|
||||
return;
|
||||
}
|
||||
@ -194,7 +210,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
|
||||
},
|
||||
};
|
||||
message.content = [messageContent];
|
||||
message.status = MessageStatus.Ready;
|
||||
message.status = MessageStatus.Error;
|
||||
events.emit(MessageEvent.OnMessageUpdate, message);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.input {
|
||||
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
|
||||
@apply disabled:cursor-not-allowed disabled:bg-zinc-100;
|
||||
@apply disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
|
||||
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
||||
@apply file:border-0 file:bg-transparent file:font-medium;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.select {
|
||||
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1;
|
||||
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
|
||||
@apply disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
|
||||
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
||||
|
||||
&-caret {
|
||||
|
||||
@ -13,6 +13,8 @@ import { useClickOutside } from '@/hooks/useClickOutside'
|
||||
|
||||
import { usePath } from '@/hooks/usePath'
|
||||
|
||||
import { openFileTitle } from '@/utils/titleUtils'
|
||||
|
||||
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
interface Props {
|
||||
@ -38,13 +40,6 @@ export default function CardSidebar({
|
||||
|
||||
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||
|
||||
let openFolderTitle: string = 'Open Containing Folder'
|
||||
if (isMac) {
|
||||
openFolderTitle = 'Show in Finder'
|
||||
} else if (isWindows) {
|
||||
openFolderTitle = 'Show in File Explorer'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
@ -118,7 +113,7 @@ export default function CardSidebar({
|
||||
{title === 'Model' ? (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-black dark:text-muted-foreground">
|
||||
{openFolderTitle}
|
||||
{openFileTitle()}
|
||||
</span>
|
||||
<span className="mt-1 text-muted-foreground">
|
||||
Opens thread.json. Changes affect this thread only.
|
||||
@ -126,7 +121,7 @@ export default function CardSidebar({
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-bold text-black dark:text-muted-foreground">
|
||||
Show in Finder
|
||||
{openFileTitle()}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -36,23 +36,27 @@ import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||
import {
|
||||
ModelParams,
|
||||
activeThreadAtom,
|
||||
getActiveThreadIdAtom,
|
||||
setThreadModelParamsAtom,
|
||||
threadStatesAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
||||
|
||||
export default function DropdownListSidebar() {
|
||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||
// TODO: Move all of the unscoped logics outside of the component
|
||||
const DropdownListSidebar = ({
|
||||
strictedThread = true,
|
||||
}: {
|
||||
strictedThread?: boolean
|
||||
}) => {
|
||||
const activeThread = useAtomValue(activeThreadAtom)
|
||||
const threadStates = useAtomValue(threadStatesAtom)
|
||||
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
|
||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||
const { activeModel, stateModel } = useActiveModel()
|
||||
|
||||
const { stateModel } = useActiveModel()
|
||||
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
||||
const { setMainViewState } = useMainViewState()
|
||||
|
||||
const [loader, setLoader] = useState(0)
|
||||
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
||||
|
||||
/**
|
||||
@ -65,38 +69,41 @@ export default function DropdownListSidebar() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedModel(selectedModel || activeModel || recommendedModel)
|
||||
if (!activeThread) return
|
||||
|
||||
if (activeThread) {
|
||||
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
||||
if (finishInit) return
|
||||
const modelParams: ModelParams = {
|
||||
...recommendedModel?.parameters,
|
||||
...recommendedModel?.settings,
|
||||
/**
|
||||
* This is to set default value for these settings instead of maximum value
|
||||
* Should only apply when model.json has these settings
|
||||
*/
|
||||
...(recommendedModel?.parameters.max_tokens && {
|
||||
max_tokens: defaultValue(recommendedModel?.parameters.max_tokens),
|
||||
}),
|
||||
...(recommendedModel?.settings.ctx_len && {
|
||||
ctx_len: defaultValue(recommendedModel?.settings.ctx_len),
|
||||
}),
|
||||
}
|
||||
setThreadModelParams(activeThread.id, modelParams)
|
||||
let model = downloadedModels.find(
|
||||
(model) => model.id === activeThread.assistants[0].model.id
|
||||
)
|
||||
if (!model) {
|
||||
model = recommendedModel
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
setSelectedModel(model)
|
||||
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
||||
if (finishInit) return
|
||||
const modelParams: ModelParams = {
|
||||
...model?.parameters,
|
||||
...model?.settings,
|
||||
/**
|
||||
* This is to set default value for these settings instead of maximum value
|
||||
* Should only apply when model.json has these settings
|
||||
*/
|
||||
...(model?.parameters.max_tokens && {
|
||||
max_tokens: defaultValue(model?.parameters.max_tokens),
|
||||
}),
|
||||
...(model?.settings.ctx_len && {
|
||||
ctx_len: defaultValue(model?.settings.ctx_len),
|
||||
}),
|
||||
}
|
||||
setThreadModelParams(activeThread.id, modelParams)
|
||||
}, [
|
||||
recommendedModel,
|
||||
activeThread,
|
||||
setSelectedModel,
|
||||
setThreadModelParams,
|
||||
threadStates,
|
||||
downloadedModels,
|
||||
setThreadModelParams,
|
||||
setSelectedModel,
|
||||
])
|
||||
|
||||
const [loader, setLoader] = useState(0)
|
||||
|
||||
// This is fake loader please fix this when we have realtime percentage when load model
|
||||
useEffect(() => {
|
||||
if (stateModel.loading) {
|
||||
@ -132,25 +139,25 @@ export default function DropdownListSidebar() {
|
||||
setServerEnabled(false)
|
||||
}
|
||||
|
||||
if (activeThreadId) {
|
||||
if (activeThread) {
|
||||
const modelParams = {
|
||||
...model?.parameters,
|
||||
...model?.settings,
|
||||
}
|
||||
setThreadModelParams(activeThreadId, modelParams)
|
||||
setThreadModelParams(activeThread.id, modelParams)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
downloadedModels,
|
||||
serverEnabled,
|
||||
activeThreadId,
|
||||
activeModel,
|
||||
activeThread,
|
||||
setSelectedModel,
|
||||
setServerEnabled,
|
||||
setThreadModelParams,
|
||||
]
|
||||
)
|
||||
|
||||
if (!activeThread) {
|
||||
if (strictedThread && !activeThread) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -236,10 +243,9 @@ export default function DropdownListSidebar() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<OpenAiKeyInput
|
||||
selectedModel={selectedModel}
|
||||
serverEnabled={serverEnabled}
|
||||
/>
|
||||
<OpenAiKeyInput />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DropdownListSidebar
|
||||
|
||||
@ -27,6 +27,8 @@ import { usePath } from '@/hooks/usePath'
|
||||
|
||||
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
|
||||
|
||||
import { openFileTitle } from '@/utils/titleUtils'
|
||||
|
||||
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
const TopBar = () => {
|
||||
@ -162,7 +164,7 @@ const TopBar = () => {
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<span className="font-medium text-black dark:text-muted-foreground">
|
||||
Show in Finder
|
||||
{openFileTitle()}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@ -207,7 +209,7 @@ const TopBar = () => {
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-black dark:text-muted-foreground">
|
||||
Show in Finder
|
||||
{openFileTitle()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -27,6 +27,7 @@ const BaseLayout = (props: PropsWithChildren) => {
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
|
||||
setMainViewState(MainViewState.Settings)
|
||||
localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION)
|
||||
}
|
||||
}, [setMainViewState])
|
||||
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { InferenceEngine, Model } from '@janhq/core'
|
||||
import { InferenceEngine } from '@janhq/core'
|
||||
import { Input } from '@janhq/uikit'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { useEngineSettings } from '@/hooks/useEngineSettings'
|
||||
|
||||
type Props = {
|
||||
selectedModel?: Model
|
||||
serverEnabled: boolean
|
||||
}
|
||||
import { selectedModelAtom } from '../DropdownListSidebar'
|
||||
|
||||
const OpenAiKeyInput: React.FC<Props> = ({ selectedModel, serverEnabled }) => {
|
||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||
|
||||
const OpenAiKeyInput: React.FC = () => {
|
||||
const selectedModel = useAtomValue(selectedModelAtom)
|
||||
const serverEnabled = useAtomValue(serverEnabledAtom)
|
||||
const [openAISettings, setOpenAISettings] = useState<
|
||||
{ api_key: string } | undefined
|
||||
>(undefined)
|
||||
@ -20,8 +23,7 @@ const OpenAiKeyInput: React.FC<Props> = ({ selectedModel, serverEnabled }) => {
|
||||
readOpenAISettings().then((settings) => {
|
||||
setOpenAISettings(settings)
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, [readOpenAISettings])
|
||||
|
||||
if (!selectedModel || selectedModel.engine !== InferenceEngine.openai) {
|
||||
return null
|
||||
|
||||
@ -49,6 +49,14 @@ export default function useDeleteThread() {
|
||||
threadId,
|
||||
messages.filter((msg) => msg.role === ChatCompletionRole.System)
|
||||
)
|
||||
|
||||
thread.metadata = {
|
||||
...thread.metadata,
|
||||
lastMessage: undefined,
|
||||
}
|
||||
await extensionManager
|
||||
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
||||
?.saveThread(thread)
|
||||
updateThreadLastMessage(threadId, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { fs, joinPath } from '@janhq/core'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { fs, joinPath, events, AppConfigurationEventName } from '@janhq/core'
|
||||
|
||||
export const useEngineSettings = () => {
|
||||
const readOpenAISettings = async () => {
|
||||
const readOpenAISettings = useCallback(async () => {
|
||||
if (
|
||||
!(await fs.existsSync(await joinPath(['file://engines', 'openai.json'])))
|
||||
)
|
||||
@ -14,17 +16,24 @@ export const useEngineSettings = () => {
|
||||
return typeof settings === 'object' ? settings : JSON.parse(settings)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const saveOpenAISettings = async ({
|
||||
apiKey,
|
||||
}: {
|
||||
apiKey: string | undefined
|
||||
}) => {
|
||||
const settings = await readOpenAISettings()
|
||||
const settingFilePath = await joinPath(['file://engines', 'openai.json'])
|
||||
|
||||
settings.api_key = apiKey
|
||||
await fs.writeFileSync(
|
||||
await joinPath(['file://engines', 'openai.json']),
|
||||
JSON.stringify(settings)
|
||||
|
||||
await fs.writeFileSync(settingFilePath, JSON.stringify(settings))
|
||||
|
||||
// Sec: Don't attach the settings data to the event
|
||||
events.emit(
|
||||
AppConfigurationEventName.OnConfigurationUpdate,
|
||||
settingFilePath
|
||||
)
|
||||
}
|
||||
return { readOpenAISettings, saveOpenAISettings }
|
||||
|
||||
@ -11,20 +11,23 @@ export const usePath = () => {
|
||||
const selectedModel = useAtomValue(selectedModelAtom)
|
||||
|
||||
const onReviewInFinder = async (type: string) => {
|
||||
if (!activeThread) return
|
||||
const activeThreadState = threadStates[activeThread.id]
|
||||
if (!activeThreadState.isFinishInit) {
|
||||
alert('Thread is not started yet')
|
||||
return
|
||||
// TODO: this logic should be refactored.
|
||||
if (type !== 'Model') {
|
||||
if (!activeThread) return
|
||||
const activeThreadState = threadStates[activeThread.id]
|
||||
if (!activeThreadState.isFinishInit) {
|
||||
alert('Thread is not started yet')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const userSpace = await getJanDataFolderPath()
|
||||
let filePath = undefined
|
||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||
const assistantId = activeThread?.assistants[0]?.assistant_id
|
||||
switch (type) {
|
||||
case 'Engine':
|
||||
case 'Thread':
|
||||
filePath = await joinPath(['threads', activeThread.id])
|
||||
filePath = await joinPath(['threads', activeThread?.id ?? ''])
|
||||
break
|
||||
case 'Model':
|
||||
if (!selectedModel) return
|
||||
@ -44,20 +47,27 @@ export const usePath = () => {
|
||||
}
|
||||
|
||||
const onViewJson = async (type: string) => {
|
||||
if (!activeThread) return
|
||||
const activeThreadState = threadStates[activeThread.id]
|
||||
if (!activeThreadState.isFinishInit) {
|
||||
alert('Thread is not started yet')
|
||||
return
|
||||
// TODO: this logic should be refactored.
|
||||
if (type !== 'Model') {
|
||||
if (!activeThread) return
|
||||
const activeThreadState = threadStates[activeThread.id]
|
||||
if (!activeThreadState.isFinishInit) {
|
||||
alert('Thread is not started yet')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const userSpace = await getJanDataFolderPath()
|
||||
let filePath = undefined
|
||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||
const assistantId = activeThread?.assistants[0]?.assistant_id
|
||||
switch (type) {
|
||||
case 'Engine':
|
||||
case 'Thread':
|
||||
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
|
||||
filePath = await joinPath([
|
||||
'threads',
|
||||
activeThread?.id ?? '',
|
||||
'thread.json',
|
||||
])
|
||||
break
|
||||
case 'Model':
|
||||
if (!selectedModel) return
|
||||
|
||||
@ -43,9 +43,7 @@ export default function useRecommendedModel() {
|
||||
Model | undefined
|
||||
> => {
|
||||
const models = await getAndSortDownloadedModels()
|
||||
if (!activeThread) {
|
||||
return
|
||||
}
|
||||
if (!activeThread) return
|
||||
|
||||
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
||||
if (finishInit) {
|
||||
|
||||
@ -1,32 +1,15 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { fs, AppConfiguration } from '@janhq/core'
|
||||
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
import { useMainViewState } from './useMainViewState'
|
||||
|
||||
const isSameDirectoryAtom = atom(false)
|
||||
const isDirectoryConfirmAtom = atom(false)
|
||||
const isErrorSetNewDestAtom = atom(false)
|
||||
const currentPathAtom = atom('')
|
||||
const newDestinationPathAtom = atom('')
|
||||
|
||||
export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination'
|
||||
|
||||
export function useVaultDirectory() {
|
||||
const [isSameDirectory, setIsSameDirectory] = useAtom(isSameDirectoryAtom)
|
||||
const { setMainViewState } = useMainViewState()
|
||||
const [isDirectoryConfirm, setIsDirectoryConfirm] = useAtom(
|
||||
isDirectoryConfirmAtom
|
||||
)
|
||||
const [isErrorSetNewDest, setIsErrorSetNewDest] = useAtom(
|
||||
isErrorSetNewDestAtom
|
||||
)
|
||||
const [currentPath, setCurrentPath] = useAtom(currentPathAtom)
|
||||
const [newDestinationPath, setNewDestinationPath] = useAtom(
|
||||
newDestinationPathAtom
|
||||
)
|
||||
const [isSameDirectory, setIsSameDirectory] = useState(false)
|
||||
const [isDirectoryConfirm, setIsDirectoryConfirm] = useState(false)
|
||||
const [isErrorSetNewDest, setIsErrorSetNewDest] = useState(false)
|
||||
const [currentPath, setCurrentPath] = useState('')
|
||||
const [newDestinationPath, setNewDestinationPath] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
window.core?.api
|
||||
@ -34,7 +17,6 @@ export function useVaultDirectory() {
|
||||
?.then((appConfig: AppConfiguration) => {
|
||||
setCurrentPath(appConfig.data_folder)
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const setNewDestination = async () => {
|
||||
|
||||
@ -199,10 +199,8 @@ const Sidebar: React.FC = () => {
|
||||
</div>
|
||||
</CardSidebar>
|
||||
<CardSidebar title="Model">
|
||||
<div className="px-2">
|
||||
<div className="mt-4">
|
||||
<DropdownListSidebar />
|
||||
</div>
|
||||
<div className="px-2 pt-4">
|
||||
<DropdownListSidebar />
|
||||
|
||||
{componentDataRuntimeSetting.length > 0 && (
|
||||
<div className="mt-6">
|
||||
|
||||
@ -3,19 +3,26 @@ import { useEffect, useState } from 'react'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { useServerLog } from '@/hooks/useServerLog'
|
||||
|
||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||
|
||||
const Logs = () => {
|
||||
const { getServerLog } = useServerLog()
|
||||
const serverEnabled = useAtomValue(serverEnabledAtom)
|
||||
const [logs, setLogs] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
getServerLog().then((log) => {
|
||||
if (typeof log?.split === 'function') setLogs(log.split(/\r?\n|\r|\n/g))
|
||||
if (typeof log?.split === 'function') {
|
||||
setLogs(log.split(/\r?\n|\r|\n/g))
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [logs])
|
||||
}, [logs, serverEnabled])
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
|
||||
@ -29,6 +29,7 @@ import { ExternalLinkIcon, InfoIcon } from 'lucide-react'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import CardSidebar from '@/containers/CardSidebar'
|
||||
|
||||
import DropdownListSidebar, {
|
||||
selectedModelAtom,
|
||||
} from '@/containers/DropdownListSidebar'
|
||||
@ -58,7 +59,7 @@ const portAtom = atom('1337')
|
||||
const LocalServerScreen = () => {
|
||||
const [errorRangePort, setErrorRangePort] = useState(false)
|
||||
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
||||
const showing = useAtomValue(showRightSideBarAtom)
|
||||
const showRightSideBar = useAtomValue(showRightSideBarAtom)
|
||||
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||
|
||||
const modelEngineParams = toSettingParams(activeModelParams)
|
||||
@ -66,7 +67,7 @@ const LocalServerScreen = () => {
|
||||
|
||||
const { openServerLog, clearServerLog } = useServerLog()
|
||||
const { startModel, stateModel } = useActiveModel()
|
||||
const [selectedModel] = useAtom(selectedModelAtom)
|
||||
const selectedModel = useAtomValue(selectedModelAtom)
|
||||
|
||||
const [isCorsEnabled, setIsCorsEnabled] = useAtom(corsEnabledAtom)
|
||||
const [isVerboseEnabled, setIsVerboseEnabled] = useAtom(verboseEnabledAtom)
|
||||
@ -116,7 +117,7 @@ const LocalServerScreen = () => {
|
||||
<Button
|
||||
block
|
||||
themes={serverEnabled ? 'danger' : 'primary'}
|
||||
disabled={stateModel.loading || errorRangePort}
|
||||
disabled={stateModel.loading || errorRangePort || !selectedModel}
|
||||
onClick={() => {
|
||||
if (serverEnabled) {
|
||||
window.core?.api?.stopServer()
|
||||
@ -176,6 +177,7 @@ const LocalServerScreen = () => {
|
||||
'w-[70px] flex-shrink-0',
|
||||
errorRangePort && 'border-danger'
|
||||
)}
|
||||
type="number"
|
||||
value={port}
|
||||
onChange={(e) => {
|
||||
handleChangePort(e.target.value)
|
||||
@ -275,7 +277,7 @@ const LocalServerScreen = () => {
|
||||
|
||||
{/* Middle Bar */}
|
||||
<ScrollToBottom className="relative flex h-full w-full flex-col overflow-auto bg-background">
|
||||
<div className="sticky top-0 flex items-center justify-between bg-zinc-100 px-4 py-2 dark:bg-secondary/30">
|
||||
<div className="sticky top-0 flex items-center justify-between bg-zinc-100 px-4 py-2 dark:bg-zinc-600">
|
||||
<h2 className="font-bold">Server Logs</h2>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
@ -345,15 +347,13 @@ const LocalServerScreen = () => {
|
||||
<div
|
||||
className={twMerge(
|
||||
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background transition-all duration-100 dark:bg-background/20',
|
||||
showing
|
||||
showRightSideBar
|
||||
? 'w-80 translate-x-0 opacity-100'
|
||||
: 'w-0 translate-x-full opacity-0'
|
||||
)}
|
||||
>
|
||||
<div className="px-4">
|
||||
<div className="mt-4">
|
||||
<DropdownListSidebar />
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<DropdownListSidebar strictedThread={false} />
|
||||
|
||||
{componentDataEngineSetting.filter(
|
||||
(x) => x.name === 'prompt_template'
|
||||
|
||||
@ -11,20 +11,23 @@ import {
|
||||
Button,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
export const showDirectoryConfirmModalAtom = atom(false)
|
||||
|
||||
type Props = {
|
||||
destinationPath: string
|
||||
onUserConfirmed: () => void
|
||||
}
|
||||
|
||||
const ModalChangeDirectory: React.FC<Props> = ({
|
||||
destinationPath,
|
||||
onUserConfirmed,
|
||||
}) => {
|
||||
const [show, setShow] = useAtom(showDirectoryConfirmModalAtom)
|
||||
|
||||
const ModalChangeDirectory = () => {
|
||||
const {
|
||||
isDirectoryConfirm,
|
||||
setIsDirectoryConfirm,
|
||||
applyNewDestination,
|
||||
newDestinationPath,
|
||||
} = useVaultDirectory()
|
||||
return (
|
||||
<Modal
|
||||
open={isDirectoryConfirm}
|
||||
onOpenChange={() => setIsDirectoryConfirm(false)}
|
||||
>
|
||||
<Modal open={show} onOpenChange={setShow}>
|
||||
<ModalPortal />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
@ -32,18 +35,16 @@ const ModalChangeDirectory = () => {
|
||||
</ModalHeader>
|
||||
<p className="text-muted-foreground">
|
||||
Are you sure you want to relocate Jan data folder to{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{newDestinationPath}
|
||||
</span>
|
||||
<span className="font-medium text-foreground">{destinationPath}</span>
|
||||
? A restart will be required afterward.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<div className="flex gap-x-2">
|
||||
<ModalClose asChild onClick={() => setIsDirectoryConfirm(false)}>
|
||||
<ModalClose asChild onClick={() => setShow(false)}>
|
||||
<Button themes="ghost">Cancel</Button>
|
||||
</ModalClose>
|
||||
<ModalClose asChild>
|
||||
<Button onClick={applyNewDestination} autoFocus>
|
||||
<Button onClick={onUserConfirmed} autoFocus>
|
||||
Yes, Proceed
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
@ -10,16 +10,15 @@ import {
|
||||
ModalClose,
|
||||
Button,
|
||||
} from '@janhq/uikit'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
|
||||
export const showChangeFolderErrorAtom = atom(false)
|
||||
|
||||
const ModalErrorSetDestGlobal = () => {
|
||||
const { isErrorSetNewDest, setIsErrorSetNewDest } = useVaultDirectory()
|
||||
const [show, setShow] = useAtom(showChangeFolderErrorAtom)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isErrorSetNewDest}
|
||||
onOpenChange={() => setIsErrorSetNewDest(false)}
|
||||
>
|
||||
<Modal open={show} onOpenChange={setShow}>
|
||||
<ModalPortal />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
@ -31,7 +30,7 @@ const ModalErrorSetDestGlobal = () => {
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<div className="flex gap-x-2">
|
||||
<ModalClose asChild onClick={() => setIsErrorSetNewDest(false)}>
|
||||
<ModalClose asChild onClick={() => setShow(false)}>
|
||||
<Button themes="danger">Got it</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
|
||||
@ -11,16 +11,15 @@ import {
|
||||
Button,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
export const showSamePathModalAtom = atom(false)
|
||||
|
||||
const ModalSameDirectory = () => {
|
||||
const { isSameDirectory, setIsSameDirectory, setNewDestination } =
|
||||
useVaultDirectory()
|
||||
const [show, setShow] = useAtom(showSamePathModalAtom)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isSameDirectory}
|
||||
onOpenChange={() => setIsSameDirectory(false)}
|
||||
>
|
||||
<Modal open={show} onOpenChange={setShow}>
|
||||
<ModalPortal />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
@ -31,11 +30,11 @@ const ModalSameDirectory = () => {
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<div className="flex gap-x-2">
|
||||
<ModalClose asChild onClick={() => setIsSameDirectory(false)}>
|
||||
<ModalClose asChild onClick={() => setShow(false)}>
|
||||
<Button themes="ghost">Cancel</Button>
|
||||
</ModalClose>
|
||||
<ModalClose asChild>
|
||||
<Button themes="danger" onClick={setNewDestination} autoFocus>
|
||||
<Button themes="danger" onClick={() => setShow(false)} autoFocus>
|
||||
Choose a different folder
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
@ -1,17 +1,72 @@
|
||||
import { Fragment, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { fs, AppConfiguration } from '@janhq/core'
|
||||
import { Button, Input } from '@janhq/uikit'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { PencilIcon, FolderOpenIcon } from 'lucide-react'
|
||||
|
||||
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
|
||||
import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory'
|
||||
|
||||
import ModalChangeDirectory from './ModalChangeDirectory'
|
||||
import ModalErrorSetDestGlobal from './ModalErrorSetDestGlobal'
|
||||
import ModalSameDirectory from './ModalSameDirectory'
|
||||
import ModalChangeDirectory, {
|
||||
showDirectoryConfirmModalAtom,
|
||||
} from './ModalChangeDirectory'
|
||||
import ModalErrorSetDestGlobal, {
|
||||
showChangeFolderErrorAtom,
|
||||
} from './ModalErrorSetDestGlobal'
|
||||
import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory'
|
||||
|
||||
const DataFolder = () => {
|
||||
const { currentPath, setNewDestination } = useVaultDirectory()
|
||||
const [janDataFolderPath, setJanDataFolderPath] = useState('')
|
||||
const setShowDirectoryConfirm = useSetAtom(showDirectoryConfirmModalAtom)
|
||||
const setShowSameDirectory = useSetAtom(showSamePathModalAtom)
|
||||
const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom)
|
||||
const [destinationPath, setDestinationPath] = useState(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
window.core?.api
|
||||
?.getAppConfigurations()
|
||||
?.then((appConfig: AppConfiguration) => {
|
||||
setJanDataFolderPath(appConfig.data_folder)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onChangeFolderClick = useCallback(async () => {
|
||||
const destFolder = await window.core?.api?.selectDirectory()
|
||||
if (!destFolder) return
|
||||
|
||||
if (destFolder === janDataFolderPath) {
|
||||
setShowSameDirectory(true)
|
||||
return
|
||||
}
|
||||
|
||||
setDestinationPath(destFolder)
|
||||
setShowDirectoryConfirm(true)
|
||||
}, [janDataFolderPath, setShowSameDirectory, setShowDirectoryConfirm])
|
||||
|
||||
const onUserConfirmed = useCallback(async () => {
|
||||
if (!destinationPath) return
|
||||
try {
|
||||
const appConfiguration: AppConfiguration =
|
||||
await window.core?.api?.getAppConfigurations()
|
||||
const currentJanDataFolder = appConfiguration.data_folder
|
||||
appConfiguration.data_folder = destinationPath
|
||||
await fs.syncFile(currentJanDataFolder, destinationPath)
|
||||
await window.core?.api?.updateAppConfiguration(appConfiguration)
|
||||
|
||||
console.debug(
|
||||
`File sync finished from ${currentJanDataFolder} to ${destinationPath}`
|
||||
)
|
||||
|
||||
localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true')
|
||||
await window.core?.api?.relaunch()
|
||||
} catch (e) {
|
||||
console.error(`Error: ${e}`)
|
||||
setShowChangeFolderError(true)
|
||||
}
|
||||
}, [destinationPath, setShowChangeFolderError])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Fragment>
|
||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||
<div className="flex-shrink-0 space-y-1.5">
|
||||
<div className="flex gap-x-2">
|
||||
@ -26,7 +81,11 @@ const DataFolder = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<div className="relative">
|
||||
<Input value={currentPath} className="w-[240px] pr-8" disabled />
|
||||
<Input
|
||||
value={janDataFolderPath}
|
||||
className="w-[240px] pr-8"
|
||||
disabled
|
||||
/>
|
||||
<FolderOpenIcon
|
||||
size={16}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2"
|
||||
@ -36,16 +95,19 @@ const DataFolder = () => {
|
||||
size="sm"
|
||||
themes="outline"
|
||||
className="h-9 w-9 p-0"
|
||||
onClick={setNewDestination}
|
||||
onClick={onChangeFolderClick}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ModalSameDirectory />
|
||||
<ModalChangeDirectory />
|
||||
<ModalChangeDirectory
|
||||
destinationPath={destinationPath ?? ''}
|
||||
onUserConfirmed={onUserConfirmed}
|
||||
/>
|
||||
<ModalErrorSetDestGlobal />
|
||||
</>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -182,6 +182,30 @@ const Advanced = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Open app directory */}
|
||||
{window.electronAPI && (
|
||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="text-sm font-semibold capitalize">
|
||||
Open App Directory
|
||||
</h6>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap leading-relaxed">
|
||||
Open the directory where your app data, like conversation history
|
||||
and model configurations, is located.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
themes="secondaryBlue"
|
||||
onClick={() => window.core?.api?.openAppDirectory()}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Claer log */}
|
||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||
<div className="flex-shrink-0 space-y-1.5">
|
||||
|
||||
@ -56,10 +56,8 @@ export default function RowModel(props: RowModelProps) {
|
||||
stopModel()
|
||||
window.core?.api?.stopServer()
|
||||
setServerEnabled(false)
|
||||
} else {
|
||||
if (serverEnabled) {
|
||||
startModel(modelId)
|
||||
}
|
||||
} else if (!serverEnabled) {
|
||||
startModel(modelId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +180,7 @@ export default function RowModel(props: RowModelProps) {
|
||||
)}
|
||||
onClick={() => {
|
||||
setTimeout(async () => {
|
||||
if (serverEnabled) {
|
||||
if (!serverEnabled) {
|
||||
await stopModel()
|
||||
deleteModel(props.data)
|
||||
}
|
||||
|
||||
@ -37,6 +37,19 @@ export default function SystemMonitorScreen() {
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="h-full p-8" data-test-id="testid-system-monitor">
|
||||
<div className="grid grid-cols-2 gap-8 lg:grid-cols-3">
|
||||
<div className="rounded-xl border border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-base font-bold uppercase">
|
||||
cpu ({cpuUsage}%)
|
||||
</h4>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{cpuUsage}% of 100%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Progress className="mb-2 h-10 rounded-md" value={cpuUsage} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-base font-bold uppercase">
|
||||
@ -53,19 +66,6 @@ export default function SystemMonitorScreen() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-base font-bold uppercase">
|
||||
cpu ({cpuUsage}%)
|
||||
</h4>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{cpuUsage}% of 100%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Progress className="mb-2 h-10 rounded-md" value={cpuUsage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeModel && (
|
||||
|
||||
9
web/utils/titleUtils.ts
Normal file
9
web/utils/titleUtils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const openFileTitle = (): string => {
|
||||
if (isMac) {
|
||||
return 'Show in Finder'
|
||||
} else if (isWindows) {
|
||||
return 'Show in File Explorer'
|
||||
} else {
|
||||
return 'Open Containing Folder'
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user