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
|
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
|
||||||
|
|||||||
@ -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 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 './appConfigEntity'
|
||||||
|
export * from './appConfigEvent'
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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])
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
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