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 name: Jan Build Electron App Nightly or Manual
on: on:
push:
branches:
- main
paths-ignore:
- 'README.md'
- 'docs/**'
schedule: 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 - 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: workflow_dispatch:
@ -23,12 +29,20 @@ jobs:
- name: Set public provider - name: Set public provider
id: set-public-provider id: set-public-provider
run: | 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=public_provider::${{ github.event.inputs.public_provider }}"
echo "::set-output name=ref::${{ github.ref }}" echo "::set-output name=ref::${{ github.ref }}"
else else
echo "::set-output name=public_provider::cloudflare-r2" if [ "${{ github.event_name }}" == "schedule" ]; then
echo "::set-output name=ref::refs/heads/dev" 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 fi
# Job create Update app version based on latest release tag with build number and save to output # Job create Update app version based on latest release tag with build number and save to output
get-update-version: get-update-version:
@ -73,6 +87,17 @@ jobs:
push_to_branch: dev push_to_branch: dev
new_version: ${{ needs.get-update-version.outputs.new_version }} 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: noti-discord-manual-and-update-url-readme:
needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider] needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider]
secrets: inherit 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 util from 'util'
import { getAppLogPath, getServerLogPath } from './utils' import { getAppLogPath, getServerLogPath } from './utils'
export const log = function (message: string) { export const log = (message: string) => {
const appLogPath = getAppLogPath() const path = getAppLogPath()
if (!message.startsWith('[')) { if (!message.startsWith('[')) {
message = `[APP]::${message}` message = `[APP]::${message}`
} }
message = `${new Date().toISOString()} ${message}` message = `${new Date().toISOString()} ${message}`
if (fs.existsSync(appLogPath)) { writeLog(message, path)
var log_file = fs.createWriteStream(appLogPath, {
flags: 'a',
})
log_file.write(util.format(message) + '\n')
log_file.close()
console.debug(message)
}
} }
export const logServer = function (message: string) { export const logServer = (message: string) => {
const serverLogPath = getServerLogPath() const path = getServerLogPath()
if (!message.startsWith('[')) { if (!message.startsWith('[')) {
message = `[SERVER]::${message}` message = `[SERVER]::${message}`
} }
message = `${new Date().toISOString()} ${message}` message = `${new Date().toISOString()} ${message}`
writeLog(message, path)
}
if (fs.existsSync(serverLogPath)) { const writeLog = (message: string, logPath: string) => {
var log_file = fs.createWriteStream(serverLogPath, { if (!fs.existsSync(logPath)) {
fs.writeFileSync(logPath, message)
} else {
const logFile = fs.createWriteStream(logPath, {
flags: 'a', flags: 'a',
}) })
log_file.write(util.format(message) + '\n') logFile.write(util.format(message) + '\n')
log_file.close() logFile.close()
console.debug(message) 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 './appConfigEntity'
export * from './appConfigEvent'

View File

@ -20,6 +20,8 @@ import {
MessageEvent, MessageEvent,
ModelEvent, ModelEvent,
InferenceEvent, InferenceEvent,
AppConfigurationEventName,
joinPath,
} from "@janhq/core"; } from "@janhq/core";
import { requestInference } from "./helpers/sse"; import { requestInference } from "./helpers/sse";
import { ulid } from "ulid"; import { ulid } from "ulid";
@ -71,6 +73,20 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
events.on(InferenceEvent.OnInferenceStopped, () => { events.on(InferenceEvent.OnInferenceStopped, () => {
JanInferenceOpenAIExtension.handleInferenceStopped(this); 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) => { error: async (err) => {
if (instance.isCancelled || message.content.length > 0) { if (instance.isCancelled || message.content.length > 0) {
message.status = MessageStatus.Error; message.status = MessageStatus.Stopped;
events.emit(MessageEvent.OnMessageUpdate, message); events.emit(MessageEvent.OnMessageUpdate, message);
return; return;
} }
@ -194,7 +210,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
}, },
}; };
message.content = [messageContent]; message.content = [messageContent];
message.status = MessageStatus.Ready; message.status = MessageStatus.Error;
events.emit(MessageEvent.OnMessageUpdate, message); events.emit(MessageEvent.OnMessageUpdate, message);
}, },
}); });

View File

@ -1,6 +1,6 @@
.input { .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 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 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; @apply file:border-0 file:bg-transparent file:font-medium;
} }

View File

@ -1,5 +1,6 @@
.select { .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; @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
&-caret { &-caret {

View File

@ -13,6 +13,8 @@ import { useClickOutside } from '@/hooks/useClickOutside'
import { usePath } from '@/hooks/usePath' import { usePath } from '@/hooks/usePath'
import { openFileTitle } from '@/utils/titleUtils'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
interface Props { interface Props {
@ -38,13 +40,6 @@ export default function CardSidebar({
useClickOutside(() => setMore(false), null, [menu, toggle]) 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 ( return (
<div <div
className={twMerge( className={twMerge(
@ -118,7 +113,7 @@ export default function CardSidebar({
{title === 'Model' ? ( {title === 'Model' ? (
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground"> <span className="font-medium text-black dark:text-muted-foreground">
{openFolderTitle} {openFileTitle()}
</span> </span>
<span className="mt-1 text-muted-foreground"> <span className="mt-1 text-muted-foreground">
Opens thread.json. Changes affect this thread only. Opens thread.json. Changes affect this thread only.
@ -126,7 +121,7 @@ export default function CardSidebar({
</div> </div>
) : ( ) : (
<span className="text-bold text-black dark:text-muted-foreground"> <span className="text-bold text-black dark:text-muted-foreground">
Show in Finder {openFileTitle()}
</span> </span>
)} )}
</> </>

View File

@ -36,23 +36,27 @@ import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { import {
ModelParams, ModelParams,
activeThreadAtom, activeThreadAtom,
getActiveThreadIdAtom,
setThreadModelParamsAtom, setThreadModelParamsAtom,
threadStatesAtom, threadStatesAtom,
} from '@/helpers/atoms/Thread.atom' } from '@/helpers/atoms/Thread.atom'
export const selectedModelAtom = atom<Model | undefined>(undefined) export const selectedModelAtom = atom<Model | undefined>(undefined)
export default function DropdownListSidebar() { // TODO: Move all of the unscoped logics outside of the component
const activeThreadId = useAtomValue(getActiveThreadIdAtom) const DropdownListSidebar = ({
strictedThread = true,
}: {
strictedThread?: boolean
}) => {
const activeThread = useAtomValue(activeThreadAtom) const activeThread = useAtomValue(activeThreadAtom)
const threadStates = useAtomValue(threadStatesAtom) const threadStates = useAtomValue(threadStatesAtom)
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
const { activeModel, stateModel } = useActiveModel()
const { stateModel } = useActiveModel()
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom) const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const { setMainViewState } = useMainViewState() const { setMainViewState } = useMainViewState()
const [loader, setLoader] = useState(0)
const { recommendedModel, downloadedModels } = useRecommendedModel() const { recommendedModel, downloadedModels } = useRecommendedModel()
/** /**
@ -65,38 +69,41 @@ export default function DropdownListSidebar() {
} }
useEffect(() => { useEffect(() => {
setSelectedModel(selectedModel || activeModel || recommendedModel) if (!activeThread) return
if (activeThread) { let model = downloadedModels.find(
const finishInit = threadStates[activeThread.id].isFinishInit ?? true (model) => model.id === activeThread.assistants[0].model.id
if (finishInit) return )
const modelParams: ModelParams = { if (!model) {
...recommendedModel?.parameters, model = recommendedModel
...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)
} }
// 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, recommendedModel,
activeThread, activeThread,
setSelectedModel,
setThreadModelParams,
threadStates, threadStates,
downloadedModels,
setThreadModelParams,
setSelectedModel,
]) ])
const [loader, setLoader] = useState(0)
// This is fake loader please fix this when we have realtime percentage when load model // This is fake loader please fix this when we have realtime percentage when load model
useEffect(() => { useEffect(() => {
if (stateModel.loading) { if (stateModel.loading) {
@ -132,25 +139,25 @@ export default function DropdownListSidebar() {
setServerEnabled(false) setServerEnabled(false)
} }
if (activeThreadId) { if (activeThread) {
const modelParams = { const modelParams = {
...model?.parameters, ...model?.parameters,
...model?.settings, ...model?.settings,
} }
setThreadModelParams(activeThreadId, modelParams) setThreadModelParams(activeThread.id, modelParams)
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps
[ [
downloadedModels, downloadedModels,
serverEnabled, serverEnabled,
activeThreadId, activeThread,
activeModel, setSelectedModel,
setServerEnabled,
setThreadModelParams, setThreadModelParams,
] ]
) )
if (!activeThread) { if (strictedThread && !activeThread) {
return null return null
} }
@ -236,10 +243,9 @@ export default function DropdownListSidebar() {
</Select> </Select>
</div> </div>
<OpenAiKeyInput <OpenAiKeyInput />
selectedModel={selectedModel}
serverEnabled={serverEnabled}
/>
</> </>
) )
} }
export default DropdownListSidebar

View File

@ -27,6 +27,8 @@ import { usePath } from '@/hooks/usePath'
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
import { openFileTitle } from '@/utils/titleUtils'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
const TopBar = () => { const TopBar = () => {
@ -162,7 +164,7 @@ const TopBar = () => {
className="text-muted-foreground" className="text-muted-foreground"
/> />
<span className="font-medium text-black dark:text-muted-foreground"> <span className="font-medium text-black dark:text-muted-foreground">
Show in Finder {openFileTitle()}
</span> </span>
</div> </div>
<div <div
@ -207,7 +209,7 @@ const TopBar = () => {
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground"> <span className="font-medium text-black dark:text-muted-foreground">
Show in Finder {openFileTitle()}
</span> </span>
</div> </div>
</div> </div>

View File

@ -27,6 +27,7 @@ const BaseLayout = (props: PropsWithChildren) => {
useEffect(() => { useEffect(() => {
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') { if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
setMainViewState(MainViewState.Settings) setMainViewState(MainViewState.Settings)
localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION)
} }
}, [setMainViewState]) }, [setMainViewState])

View File

@ -1,16 +1,19 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { InferenceEngine, Model } from '@janhq/core' import { InferenceEngine } from '@janhq/core'
import { Input } from '@janhq/uikit' import { Input } from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { useEngineSettings } from '@/hooks/useEngineSettings' import { useEngineSettings } from '@/hooks/useEngineSettings'
type Props = { import { selectedModelAtom } from '../DropdownListSidebar'
selectedModel?: Model
serverEnabled: boolean
}
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< const [openAISettings, setOpenAISettings] = useState<
{ api_key: string } | undefined { api_key: string } | undefined
>(undefined) >(undefined)
@ -20,8 +23,7 @@ const OpenAiKeyInput: React.FC<Props> = ({ selectedModel, serverEnabled }) => {
readOpenAISettings().then((settings) => { readOpenAISettings().then((settings) => {
setOpenAISettings(settings) setOpenAISettings(settings)
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps }, [readOpenAISettings])
}, [])
if (!selectedModel || selectedModel.engine !== InferenceEngine.openai) { if (!selectedModel || selectedModel.engine !== InferenceEngine.openai) {
return null return null

View File

@ -49,6 +49,14 @@ export default function useDeleteThread() {
threadId, threadId,
messages.filter((msg) => msg.role === ChatCompletionRole.System) messages.filter((msg) => msg.role === ChatCompletionRole.System)
) )
thread.metadata = {
...thread.metadata,
lastMessage: undefined,
}
await extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.saveThread(thread)
updateThreadLastMessage(threadId, undefined) 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 = () => { export const useEngineSettings = () => {
const readOpenAISettings = async () => { const readOpenAISettings = useCallback(async () => {
if ( if (
!(await fs.existsSync(await joinPath(['file://engines', 'openai.json']))) !(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 typeof settings === 'object' ? settings : JSON.parse(settings)
} }
return {} return {}
} }, [])
const saveOpenAISettings = async ({ const saveOpenAISettings = async ({
apiKey, apiKey,
}: { }: {
apiKey: string | undefined apiKey: string | undefined
}) => { }) => {
const settings = await readOpenAISettings() const settings = await readOpenAISettings()
const settingFilePath = await joinPath(['file://engines', 'openai.json'])
settings.api_key = apiKey settings.api_key = apiKey
await fs.writeFileSync(
await joinPath(['file://engines', 'openai.json']), await fs.writeFileSync(settingFilePath, JSON.stringify(settings))
JSON.stringify(settings)
// Sec: Don't attach the settings data to the event
events.emit(
AppConfigurationEventName.OnConfigurationUpdate,
settingFilePath
) )
} }
return { readOpenAISettings, saveOpenAISettings } return { readOpenAISettings, saveOpenAISettings }

View File

@ -11,20 +11,23 @@ export const usePath = () => {
const selectedModel = useAtomValue(selectedModelAtom) const selectedModel = useAtomValue(selectedModelAtom)
const onReviewInFinder = async (type: string) => { const onReviewInFinder = async (type: string) => {
if (!activeThread) return // TODO: this logic should be refactored.
const activeThreadState = threadStates[activeThread.id] if (type !== 'Model') {
if (!activeThreadState.isFinishInit) { if (!activeThread) return
alert('Thread is not started yet') const activeThreadState = threadStates[activeThread.id]
return if (!activeThreadState.isFinishInit) {
alert('Thread is not started yet')
return
}
} }
const userSpace = await getJanDataFolderPath() const userSpace = await getJanDataFolderPath()
let filePath = undefined let filePath = undefined
const assistantId = activeThread.assistants[0]?.assistant_id const assistantId = activeThread?.assistants[0]?.assistant_id
switch (type) { switch (type) {
case 'Engine': case 'Engine':
case 'Thread': case 'Thread':
filePath = await joinPath(['threads', activeThread.id]) filePath = await joinPath(['threads', activeThread?.id ?? ''])
break break
case 'Model': case 'Model':
if (!selectedModel) return if (!selectedModel) return
@ -44,20 +47,27 @@ export const usePath = () => {
} }
const onViewJson = async (type: string) => { const onViewJson = async (type: string) => {
if (!activeThread) return // TODO: this logic should be refactored.
const activeThreadState = threadStates[activeThread.id] if (type !== 'Model') {
if (!activeThreadState.isFinishInit) { if (!activeThread) return
alert('Thread is not started yet') const activeThreadState = threadStates[activeThread.id]
return if (!activeThreadState.isFinishInit) {
alert('Thread is not started yet')
return
}
} }
const userSpace = await getJanDataFolderPath() const userSpace = await getJanDataFolderPath()
let filePath = undefined let filePath = undefined
const assistantId = activeThread.assistants[0]?.assistant_id const assistantId = activeThread?.assistants[0]?.assistant_id
switch (type) { switch (type) {
case 'Engine': case 'Engine':
case 'Thread': case 'Thread':
filePath = await joinPath(['threads', activeThread.id, 'thread.json']) filePath = await joinPath([
'threads',
activeThread?.id ?? '',
'thread.json',
])
break break
case 'Model': case 'Model':
if (!selectedModel) return if (!selectedModel) return

View File

@ -43,9 +43,7 @@ export default function useRecommendedModel() {
Model | undefined Model | undefined
> => { > => {
const models = await getAndSortDownloadedModels() const models = await getAndSortDownloadedModels()
if (!activeThread) { if (!activeThread) return
return
}
const finishInit = threadStates[activeThread.id].isFinishInit ?? true const finishInit = threadStates[activeThread.id].isFinishInit ?? true
if (finishInit) { if (finishInit) {

View File

@ -1,32 +1,15 @@
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { fs, AppConfiguration } from '@janhq/core' 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 const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination'
export function useVaultDirectory() { export function useVaultDirectory() {
const [isSameDirectory, setIsSameDirectory] = useAtom(isSameDirectoryAtom) const [isSameDirectory, setIsSameDirectory] = useState(false)
const { setMainViewState } = useMainViewState() const [isDirectoryConfirm, setIsDirectoryConfirm] = useState(false)
const [isDirectoryConfirm, setIsDirectoryConfirm] = useAtom( const [isErrorSetNewDest, setIsErrorSetNewDest] = useState(false)
isDirectoryConfirmAtom const [currentPath, setCurrentPath] = useState('')
) const [newDestinationPath, setNewDestinationPath] = useState('')
const [isErrorSetNewDest, setIsErrorSetNewDest] = useAtom(
isErrorSetNewDestAtom
)
const [currentPath, setCurrentPath] = useAtom(currentPathAtom)
const [newDestinationPath, setNewDestinationPath] = useAtom(
newDestinationPathAtom
)
useEffect(() => { useEffect(() => {
window.core?.api window.core?.api
@ -34,7 +17,6 @@ export function useVaultDirectory() {
?.then((appConfig: AppConfiguration) => { ?.then((appConfig: AppConfiguration) => {
setCurrentPath(appConfig.data_folder) setCurrentPath(appConfig.data_folder)
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const setNewDestination = async () => { const setNewDestination = async () => {

View File

@ -199,10 +199,8 @@ const Sidebar: React.FC = () => {
</div> </div>
</CardSidebar> </CardSidebar>
<CardSidebar title="Model"> <CardSidebar title="Model">
<div className="px-2"> <div className="px-2 pt-4">
<div className="mt-4"> <DropdownListSidebar />
<DropdownListSidebar />
</div>
{componentDataRuntimeSetting.length > 0 && ( {componentDataRuntimeSetting.length > 0 && (
<div className="mt-6"> <div className="mt-6">

View File

@ -3,19 +3,26 @@ import { useEffect, useState } from 'react'
import React from 'react' import React from 'react'
import { useAtomValue } from 'jotai'
import { useServerLog } from '@/hooks/useServerLog' import { useServerLog } from '@/hooks/useServerLog'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
const Logs = () => { const Logs = () => {
const { getServerLog } = useServerLog() const { getServerLog } = useServerLog()
const serverEnabled = useAtomValue(serverEnabledAtom)
const [logs, setLogs] = useState([]) const [logs, setLogs] = useState([])
useEffect(() => { useEffect(() => {
getServerLog().then((log) => { 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [logs]) }, [logs, serverEnabled])
return ( return (
<div className="overflow-hidden"> <div className="overflow-hidden">

View File

@ -29,6 +29,7 @@ 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, { import DropdownListSidebar, {
selectedModelAtom, selectedModelAtom,
} from '@/containers/DropdownListSidebar' } from '@/containers/DropdownListSidebar'
@ -58,7 +59,7 @@ const portAtom = atom('1337')
const LocalServerScreen = () => { const LocalServerScreen = () => {
const [errorRangePort, setErrorRangePort] = useState(false) const [errorRangePort, setErrorRangePort] = useState(false)
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom) const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const showing = useAtomValue(showRightSideBarAtom) const showRightSideBar = useAtomValue(showRightSideBarAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const modelEngineParams = toSettingParams(activeModelParams) const modelEngineParams = toSettingParams(activeModelParams)
@ -66,7 +67,7 @@ const LocalServerScreen = () => {
const { openServerLog, clearServerLog } = useServerLog() const { openServerLog, clearServerLog } = useServerLog()
const { startModel, stateModel } = useActiveModel() const { startModel, stateModel } = useActiveModel()
const [selectedModel] = useAtom(selectedModelAtom) const selectedModel = useAtomValue(selectedModelAtom)
const [isCorsEnabled, setIsCorsEnabled] = useAtom(corsEnabledAtom) const [isCorsEnabled, setIsCorsEnabled] = useAtom(corsEnabledAtom)
const [isVerboseEnabled, setIsVerboseEnabled] = useAtom(verboseEnabledAtom) const [isVerboseEnabled, setIsVerboseEnabled] = useAtom(verboseEnabledAtom)
@ -116,7 +117,7 @@ const LocalServerScreen = () => {
<Button <Button
block block
themes={serverEnabled ? 'danger' : 'primary'} themes={serverEnabled ? 'danger' : 'primary'}
disabled={stateModel.loading || errorRangePort} disabled={stateModel.loading || errorRangePort || !selectedModel}
onClick={() => { onClick={() => {
if (serverEnabled) { if (serverEnabled) {
window.core?.api?.stopServer() window.core?.api?.stopServer()
@ -176,6 +177,7 @@ const LocalServerScreen = () => {
'w-[70px] flex-shrink-0', 'w-[70px] flex-shrink-0',
errorRangePort && 'border-danger' errorRangePort && 'border-danger'
)} )}
type="number"
value={port} value={port}
onChange={(e) => { onChange={(e) => {
handleChangePort(e.target.value) handleChangePort(e.target.value)
@ -275,7 +277,7 @@ const LocalServerScreen = () => {
{/* Middle Bar */} {/* Middle Bar */}
<ScrollToBottom className="relative flex h-full w-full flex-col overflow-auto bg-background"> <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> <h2 className="font-bold">Server Logs</h2>
<div className="space-x-2"> <div className="space-x-2">
<Button <Button
@ -345,15 +347,13 @@ const LocalServerScreen = () => {
<div <div
className={twMerge( className={twMerge(
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background transition-all duration-100 dark:bg-background/20', '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-80 translate-x-0 opacity-100'
: 'w-0 translate-x-full opacity-0' : 'w-0 translate-x-full opacity-0'
)} )}
> >
<div className="px-4"> <div className="px-4 pt-4">
<div className="mt-4"> <DropdownListSidebar strictedThread={false} />
<DropdownListSidebar />
</div>
{componentDataEngineSetting.filter( {componentDataEngineSetting.filter(
(x) => x.name === 'prompt_template' (x) => x.name === 'prompt_template'

View File

@ -11,20 +11,23 @@ import {
Button, Button,
} from '@janhq/uikit' } 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 ( return (
<Modal <Modal open={show} onOpenChange={setShow}>
open={isDirectoryConfirm}
onOpenChange={() => setIsDirectoryConfirm(false)}
>
<ModalPortal /> <ModalPortal />
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>
@ -32,18 +35,16 @@ const ModalChangeDirectory = () => {
</ModalHeader> </ModalHeader>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Are you sure you want to relocate Jan data folder to{' '} Are you sure you want to relocate Jan data folder to{' '}
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">{destinationPath}</span>
{newDestinationPath}
</span>
? A restart will be required afterward. ? A restart will be required afterward.
</p> </p>
<ModalFooter> <ModalFooter>
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<ModalClose asChild onClick={() => setIsDirectoryConfirm(false)}> <ModalClose asChild onClick={() => setShow(false)}>
<Button themes="ghost">Cancel</Button> <Button themes="ghost">Cancel</Button>
</ModalClose> </ModalClose>
<ModalClose asChild> <ModalClose asChild>
<Button onClick={applyNewDestination} autoFocus> <Button onClick={onUserConfirmed} autoFocus>
Yes, Proceed Yes, Proceed
</Button> </Button>
</ModalClose> </ModalClose>

View File

@ -10,16 +10,15 @@ import {
ModalClose, ModalClose,
Button, Button,
} from '@janhq/uikit' } from '@janhq/uikit'
import { atom, useAtom } from 'jotai'
import { useVaultDirectory } from '@/hooks/useVaultDirectory' export const showChangeFolderErrorAtom = atom(false)
const ModalErrorSetDestGlobal = () => { const ModalErrorSetDestGlobal = () => {
const { isErrorSetNewDest, setIsErrorSetNewDest } = useVaultDirectory() const [show, setShow] = useAtom(showChangeFolderErrorAtom)
return ( return (
<Modal <Modal open={show} onOpenChange={setShow}>
open={isErrorSetNewDest}
onOpenChange={() => setIsErrorSetNewDest(false)}
>
<ModalPortal /> <ModalPortal />
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>
@ -31,7 +30,7 @@ const ModalErrorSetDestGlobal = () => {
</p> </p>
<ModalFooter> <ModalFooter>
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<ModalClose asChild onClick={() => setIsErrorSetNewDest(false)}> <ModalClose asChild onClick={() => setShow(false)}>
<Button themes="danger">Got it</Button> <Button themes="danger">Got it</Button>
</ModalClose> </ModalClose>
</div> </div>

View File

@ -11,16 +11,15 @@ import {
Button, Button,
} from '@janhq/uikit' } from '@janhq/uikit'
import { useVaultDirectory } from '@/hooks/useVaultDirectory' import { atom, useAtom } from 'jotai'
export const showSamePathModalAtom = atom(false)
const ModalSameDirectory = () => { const ModalSameDirectory = () => {
const { isSameDirectory, setIsSameDirectory, setNewDestination } = const [show, setShow] = useAtom(showSamePathModalAtom)
useVaultDirectory()
return ( return (
<Modal <Modal open={show} onOpenChange={setShow}>
open={isSameDirectory}
onOpenChange={() => setIsSameDirectory(false)}
>
<ModalPortal /> <ModalPortal />
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>
@ -31,11 +30,11 @@ const ModalSameDirectory = () => {
</p> </p>
<ModalFooter> <ModalFooter>
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<ModalClose asChild onClick={() => setIsSameDirectory(false)}> <ModalClose asChild onClick={() => setShow(false)}>
<Button themes="ghost">Cancel</Button> <Button themes="ghost">Cancel</Button>
</ModalClose> </ModalClose>
<ModalClose asChild> <ModalClose asChild>
<Button themes="danger" onClick={setNewDestination} autoFocus> <Button themes="danger" onClick={() => setShow(false)} autoFocus>
Choose a different folder Choose a different folder
</Button> </Button>
</ModalClose> </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 { Button, Input } from '@janhq/uikit'
import { useSetAtom } from 'jotai'
import { PencilIcon, FolderOpenIcon } from 'lucide-react' import { PencilIcon, FolderOpenIcon } from 'lucide-react'
import { useVaultDirectory } from '@/hooks/useVaultDirectory' import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory'
import ModalChangeDirectory from './ModalChangeDirectory' import ModalChangeDirectory, {
import ModalErrorSetDestGlobal from './ModalErrorSetDestGlobal' showDirectoryConfirmModalAtom,
import ModalSameDirectory from './ModalSameDirectory' } from './ModalChangeDirectory'
import ModalErrorSetDestGlobal, {
showChangeFolderErrorAtom,
} from './ModalErrorSetDestGlobal'
import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory'
const DataFolder = () => { 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 ( 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 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-shrink-0 space-y-1.5">
<div className="flex gap-x-2"> <div className="flex gap-x-2">
@ -26,7 +81,11 @@ const DataFolder = () => {
</div> </div>
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
<div className="relative"> <div className="relative">
<Input value={currentPath} className="w-[240px] pr-8" disabled /> <Input
value={janDataFolderPath}
className="w-[240px] pr-8"
disabled
/>
<FolderOpenIcon <FolderOpenIcon
size={16} size={16}
className="absolute right-2 top-1/2 -translate-y-1/2" className="absolute right-2 top-1/2 -translate-y-1/2"
@ -36,16 +95,19 @@ const DataFolder = () => {
size="sm" size="sm"
themes="outline" themes="outline"
className="h-9 w-9 p-0" className="h-9 w-9 p-0"
onClick={setNewDestination} onClick={onChangeFolderClick}
> >
<PencilIcon size={16} /> <PencilIcon size={16} />
</Button> </Button>
</div> </div>
</div> </div>
<ModalSameDirectory /> <ModalSameDirectory />
<ModalChangeDirectory /> <ModalChangeDirectory
destinationPath={destinationPath ?? ''}
onUserConfirmed={onUserConfirmed}
/>
<ModalErrorSetDestGlobal /> <ModalErrorSetDestGlobal />
</> </Fragment>
) )
} }

View File

@ -182,6 +182,30 @@ const Advanced = () => {
/> />
</div> </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 */} {/* 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 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-shrink-0 space-y-1.5">

View File

@ -56,10 +56,8 @@ export default function RowModel(props: RowModelProps) {
stopModel() stopModel()
window.core?.api?.stopServer() window.core?.api?.stopServer()
setServerEnabled(false) setServerEnabled(false)
} else { } else if (!serverEnabled) {
if (serverEnabled) { startModel(modelId)
startModel(modelId)
}
} }
} }
@ -182,7 +180,7 @@ export default function RowModel(props: RowModelProps) {
)} )}
onClick={() => { onClick={() => {
setTimeout(async () => { setTimeout(async () => {
if (serverEnabled) { if (!serverEnabled) {
await stopModel() await stopModel()
deleteModel(props.data) deleteModel(props.data)
} }

View File

@ -37,6 +37,19 @@ export default function SystemMonitorScreen() {
<ScrollArea className="h-full w-full"> <ScrollArea className="h-full w-full">
<div className="h-full p-8" data-test-id="testid-system-monitor"> <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="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="rounded-xl border border-border p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-base font-bold uppercase"> <h4 className="text-base font-bold uppercase">
@ -53,19 +66,6 @@ export default function SystemMonitorScreen() {
/> />
</div> </div>
</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> </div>
{activeModel && ( {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'
}
}