Merge pull request #1830 from janhq/main

Sync Release 0.4.5 to dev
This commit is contained in:
Louis 2024-01-29 12:45:35 +07:00 committed by GitHub
commit 97a497858d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 361 additions and 256 deletions

View File

@ -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

View File

@ -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 }}

View File

@ -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)
}
}

View File

@ -0,0 +1,6 @@
/**
* App configuration event name
*/
export enum AppConfigurationEventName {
OnConfigurationUpdate = 'OnConfigurationUpdate',
}

View File

@ -1 +1,2 @@
export * from './appConfigEntity'
export * from './appConfigEvent'

View File

@ -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);
},
});

View File

@ -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;
}

View File

@ -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 {

View File

@ -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>
)}
</>

View File

@ -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

View File

@ -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>

View File

@ -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])

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 }

View File

@ -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

View File

@ -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) {

View File

@ -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 () => {

View File

@ -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">

View File

@ -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">

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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">

View File

@ -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)
}

View File

@ -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
View 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'
}
}